Шаблонный метод (Template Method)
Порядок шагов алгоритма фиксирует базовый класс, а наследники подставляют только сами шаги и, если нужно, меняют поведение крючков.
Проблема
Автомат готовит чай и кофе. Процесс одинаковый: вскипятить воду, заварить, налить в чашку, добавить добавки. Различаются два шага: как заваривать и что добавлять. Если написать Tea::prepare() и Coffee::prepare() целиком, порядок шагов живёт в двух местах. Когда добавится третий напиток или общий шаг (например, «прогреть чашку»), его придётся дописывать в каждую копию, и рано или поздно одна из них разойдётся с остальными.
Решение
Порядок шагов записать один раз в базовом классе Beverage в методе prepare(), который нельзя переопределить. Изменчивые шаги объявить абстрактными (brew(), addCondiments()), а точки необязательной настройки сделать крючками (hook) с поведением по умолчанию: wantsCondiments() возвращает true, и наследник переопределяет его только если хочет.
Это «голливудский принцип»: не наследник вызывает базовый класс, а базовый класс вызывает шаги наследника, когда сочтёт нужным. Управление потоком остаётся у шаблона.
Структура
В шаблоне три вида методов: сам шаблонный метод (final, порядок шагов), абстрактные шаги (наследник обязан реализовать) и крючки (наследник может переопределить). prepare() возвращает список выполненных шагов, чтобы тест видел порядок, а не только результат.
Как это собирается
Напитки создаёт прошивка автомата при запуске: меню из конфига связывает кнопку на экране с готовым объектом. Чёрный кофе - не отдельный класс, а тот же Coffee с выключенным крючком, поэтому новая позиция меню часто добавляется строкой конфига, а не наследником.
Дальше автомат на каждое нажатие находит напиток в меню и вызывает prepare(). Порядок шагов автомат не знает и не может перепутать: его держит шаблон. Кнопку, которой нет в меню (cocoa), автомат не передаёт в шаблон, а сразу отвечает "нет в меню".
Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.
Реализации
Наследники содержат только шаги. Tea оставляет крючок как есть, Coffee переопределяет wantsCondiments() по флагу из конструктора: чёрный кофе пропускает последний шаг, а порядок остальных шагов наследник поменять не может.
| Язык | Особенность |
|---|---|
| PHP | abstract class Beverage с final public function prepare(), шаги abstract protected, крючок обычный protected |
| Go | Наследования нет: шаблон — функция Prepare(r Recipe), шаги — интерфейс Recipe, умолчание крючка — встраиваемая структура DefaultHooks |
| Python | ABC + @abstractmethod для шагов, @final из typing на prepare() (проверяет тайпчекер, не рантайм) |
| TypeScript | abstract class с protected abstract шагами; final в языке нет, переопределение крючка помечено override |
Запуск: make test T=patterns/behavioral/template-method
Плюсы и минусы
Тесты прогоняют один и тот же prepare() для чая, кофе и чёрного кофе: первые три шага совпадают у всех, меняются только подставленные наследником строки и наличие последнего шага. PHP-тест отдельно проверяет, что prepare() объявлен final, Python-тест — что без шагов базовый класс не создать.
- ✅ Порядок шагов в одном месте: общий шаг добавляется один раз для всех наследников.
- ✅ Наследник пишет только то, что отличается, и не может нарушить порядок.
- ✅ Крючки дают точки расширения без обязательной реализации.
- ❌ Наследование: наследник жёстко привязан к базовому классу и его защищённым методам.
- ❌ Чем больше шагов и крючков, тем труднее понять, что именно происходит, не читая базу и наследника вместе.
- ❌ Комбинировать варианты шагов нельзя: «чай как у Tea, добавки как у Coffee» — это ещё один наследник.
Когда не применять
- Меняется один шаг, а остальной алгоритм тривиален: передайте функцию или стратегию.
- Шаги нужно комбинировать независимо друг от друга: несколько стратегий в конструкторе лучше одной иерархии.
- Порядок шагов сам по себе меняется от случая к случаю: это уже конвейер или цепочка, а не фиксированный шаблон.
Отличия между языками
- PHP — ближе всего к классике:
finalна шаблоне,abstract protectedна шагах, крючок — обычный защищённый метод. Нарушениеfinal— фатальная ошибка при загрузке класса. - Go — наследования нет, и это меняет форму, а не суть. Шаблон — обычная функция
Prepare(r Recipe): переопределить её невозможно в принципе, так чтоfinalполучается бесплатно. Умолчание крючка даёт встраиваниеDefaultHooks: его методWantsCondiments()поднимается вTea. Важная ловушка: шаблон нельзя сделать методом встроенной структуры, потому что встроенная структура не видит методов внешней и вызвала бы свои, а не наследника. Поэтому шаги приходят через интерфейс. - Python —
@abstractmethodне даст создатьBeverage()без шагов, а@finalтолько подсказка для mypy/pyright: в рантайме переопределитьprepare()можно. - TypeScript —
finalнет, компилятор не мешает переопределитьprepare(). Защиту дают соглашения иnoImplicitOverride: переопределение крючка требует явногоoverride, случайно затереть метод базы не получится.
Практика
Учебный пример показывает механику, но в нём нечего защищать: шаги — строки, ошибок нет. В проекте шаблон держит правила, которые наследник не должен обойти. Практика: экспорт платёжного реестра собирает реестр для разных банков: проверка платежей, итоговая строка и контрольная сумма общие и не переопределяются, банки задают поля заголовка и строк, а крючки — разделитель и минимальную сумму.
Связанные темы
- Практика: экспорт платёжного реестра — общая проверка и контрольная сумма в шаблоне, формат в наследниках
- Фабричный метод (Factory Method) — частный случай шаблонного метода, где шаг отвечает за создание объекта
- Стратегия (Strategy) — тот же выбор поведения через композицию вместо наследования; Go-версия шаблонного метода по сути передаёт стратегию шагов