handbookНаписать

patterns/behavioral/template-method/README.md · тема · 4 языка · 17 тестов

+ практика: экспорт платёжного реестра

Шаблонный метод (Template Method) ​

Порядок шагов алгоритма фиксирует базовый класс, а наследники подставляют только сами шаги и, если нужно, меняют поведение крючков.

Проблема ​

Автомат готовит чай и кофе. Процесс одинаковый: вскипятить воду, заварить, налить в чашку, добавить добавки. Различаются два шага: как заваривать и что добавлять. Если написать Tea::prepare() и Coffee::prepare() целиком, порядок шагов живёт в двух местах. Когда добавится третий напиток или общий шаг (например, «прогреть чашку»), его придётся дописывать в каждую копию, и рано или поздно одна из них разойдётся с остальными.

Решение ​

Порядок шагов записать один раз в базовом классе Beverage в методе prepare(), который нельзя переопределить. Изменчивые шаги объявить абстрактными (brew(), addCondiments()), а точки необязательной настройки сделать крючками (hook) с поведением по умолчанию: wantsCondiments() возвращает true, и наследник переопределяет его только если хочет.

Это «голливудский принцип»: не наследник вызывает базовый класс, а базовый класс вызывает шаги наследника, когда сочтёт нужным. Управление потоком остаётся у шаблона.

Структура ​

«abstract»

Beverage

+prepare() : list<string>

#brew() : string

#addCondiments() : string

#wantsCondiments() : bool

Tea

#brew() : string

#addCondiments() : string

Coffee

-withMilk: bool

#brew() : string

#addCondiments() : string

#wantsCondiments() : bool

В шаблоне три вида методов: сам шаблонный метод (final, порядок шагов), абстрактные шаги (наследник обязан реализовать) и крючки (наследник может переопределить). prepare() возвращает список выполненных шагов, чтобы тест видел порядок, а не только результат.

Как это собирается ​

Напитки создаёт прошивка автомата при запуске: меню из конфига связывает кнопку на экране с готовым объектом. Чёрный кофе - не отдельный класс, а тот же Coffee с выключенным крючком, поэтому новая позиция меню часто добавляется строкой конфига, а не наследником.

Дальше автомат на каждое нажатие находит напиток в меню и вызывает prepare(). Порядок шагов автомат не знает и не может перепутать: его держит шаблон. Кнопку, которой нет в меню (cocoa), автомат не передаёт в шаблон, а сразу отвечает "нет в меню".

Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.

Реализации ​

Наследники содержат только шаги. Tea оставляет крючок как есть, Coffee переопределяет wantsCondiments() по флагу из конструктора: чёрный кофе пропускает последний шаг, а порядок остальных шагов наследник поменять не может.

ЯзыкОсобенность
PHPabstract class Beverage с final public function prepare(), шаги abstract protected, крючок обычный protected
GoНаследования нет: шаблон — функция Prepare(r Recipe), шаги — интерфейс Recipe, умолчание крючка — встраиваемая структура DefaultHooks
PythonABC + @abstractmethod для шагов, @final из typing на prepare() (проверяет тайпчекер, не рантайм)
TypeScriptabstract 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, случайно затереть метод базы не получится.

Практика ​

Учебный пример показывает механику, но в нём нечего защищать: шаги — строки, ошибок нет. В проекте шаблон держит правила, которые наследник не должен обойти. Практика: экспорт платёжного реестра собирает реестр для разных банков: проверка платежей, итоговая строка и контрольная сумма общие и не переопределяются, банки задают поля заголовка и строк, а крючки — разделитель и минимальную сумму.

Связанные темы ​

© 2014-2026 Тимур ТурдыевСайт и справочник работают на VPS от AdminVPS