handbookНаписать

patterns/creational/factory-method/README.md · тема · 4 языка · 24 теста

Фабричный метод (Factory Method) ​

Общий алгоритм живёт в базовом классе, а решение «какой именно объект создать» отдаётся наследникам.

Проблема ​

Сервис уведомлений отправляет сообщение о статусе заказа. Сам процесс одинаков для всех каналов: почистить текст, проверить, что он не пустой, отправить, вернуть квитанцию. Отличается только канал: email, SMS, позже push. Если создавать канал прямо в notify() через new EmailChannel() или match ($type), то каждый новый канал требует правки общего кода, а сам notify() знает обо всех конкретных классах.

Решение ​

Вынести создание продукта в отдельный метод createChannel(), а общий процесс оставить в базовом классе-создателе Notifier. Наследники (EmailNotifier, SmsNotifier) переопределяют только фабричный метод. notify() работает с абстракцией Channel и не знает, какой канал ему достался.

Ключевое отличие от простой фабрики: создатель сам использует созданный продукт внутри своего алгоритма. Фабричный метод — это «шаг шаблонного метода», отвечающий за создание.

Структура ​

uses

creates

creates

«abstract»

Notifier

#createChannel() : Channel

+notify(recipient, text) : Receipt

EmailNotifier

#createChannel() : Channel

SmsNotifier

#createChannel() : Channel

«interface»

Channel

+name() : string

+send(recipient, text) : string

EmailChannel

SmsChannel

Правила разделены по уровням: пустой текст отклоняет создатель (общее правило), неверный адрес и обрезку до 160 символов делает конкретный продукт (правило канала). Receipt — значение, которое возвращает notify(): по нему тест видит, какой канал сработал и что именно ушло.

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

Конкретных создателей создаёт DI-контейнер, по одному на канал, и кладёт в карту email/sms. Сервис уведомлений берёт из профиля пользователя предпочтительный канал и контакты, выбирает по ним Notifier и вызывает notify(). Дальше всё как в разделе выше: общий процесс в создателе, канал создаёт фабричный метод, и сервис не знает, какой Channel сработал, пока не прочитает Receipt.

У Eve в профиле номер без кода страны: SmsChannel отвергает его, и сервис ловит ошибку получателя и шлёт то же сообщение через EmailNotifier. Запасной канал - решение вызывающего кода, создатели и продукты о нём не знают.

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

Реализации ​

Наследники содержат по одному методу: они знают конкретный класс продукта, а создатель нет. Именно поэтому в Go, где наследования нет, их место занимают конструкторы NewEmailNotifier() и NewSMSNotifier().

ЯзыкОсобенность
PHPКлассика: abstract class Notifier с abstract protected function createChannel()
GoНаследования нет: фабричный метод — поле-функция newChannel func() Channel, конструкторы NewEmailNotifier() / NewSMSNotifier() подставляют нужную
PythonABC + @abstractmethod; продукт описан через Protocol, каналы его не наследуют
TypeScriptabstract class с protected abstract createChannel(); Channel — интерфейс, Receipt — простой объектный тип

Запуск: make test T=patterns/creational/factory-method

Плюсы и минусы ​

Тесты проверяют оба уровня правил: один набор случаев на оба канала (процесс общий), отдельно обрезку SMS (правило канала) и отдельно пустой текст (правило создателя).

  • ✅ Общий алгоритм не зависит от конкретных классов продуктов.
  • ✅ Новый канал = новый продукт + новый наследник, без правки Notifier (открытость/закрытость).
  • ✅ Создание продукта в одном месте, его легко подменить в тестах.
  • ❌ Параллельная иерархия: на каждый продукт по наследнику-создателю.
  • ❌ Наследование ради одного метода — жёсткая связь; часто проще передать фабрику в конструктор (см. Go-версию).

Когда не применять ​

  • Продукт один и не меняется: new прямо в коде честнее.
  • Создателю не нужен общий алгоритм вокруг продукта: тогда это просто фабрика или DI.
  • Вариант выбирается по данным во время выполнения ($type из запроса): нужна простая фабрика или реестр, а не иерархия наследников.

Отличия между языками ​

  • PHP, TypeScript — паттерн ложится один в один: абстрактный класс, защищённый абстрактный метод. В TS protected abstract проверяется компилятором, в PHP — в рантайме и PHPStan.
  • Go — классической формы нет, потому что нет наследования. Тот же эффект даёт композиция: создатель хранит функцию-фабрику, а конструкторы NewXxxNotifier() её подставляют. Это фактически переход от «наследуй и переопредели» к «передай зависимость» — в Go это обычный способ.
  • Python — классическая форма работает через ABC, и Python не даст создать Notifier() без реализации фабричного метода (это проверяет отдельный тест). Но в Python классы сами являются фабриками: часто достаточно передать channel_factory=SmsChannel в конструктор, и иерархия наследников не нужна.

Практика ​

Учебный пример показывает механику: наследник выбирает класс продукта. В проекте продукту нужны настройки, конвейер хочется тестировать без внешних файлов, а где-то должно жить решение «какой создатель для этих данных».

  • Практика: импорт прайс-листов собирает это вместе: общий конвейер проверки записей, парсер CSV или JSON с настройками поставщика из фабричного метода, подмена парсера в тестах и единственное место выбора импортёра по расширению файла.
  • Практика: разбор пакетов приборов учёта - вариант приходит в данных: тип прибора в заголовке пакета передаётся параметром в фабричный метод, общий конвейер проверяет длину и контрольную сумму, а новый тип прибора добавляется наследником без правки конвейера.

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

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