Фабричный метод (Factory Method)
Общий алгоритм живёт в базовом классе, а решение «какой именно объект создать» отдаётся наследникам.
Проблема
Сервис уведомлений отправляет сообщение о статусе заказа. Сам процесс одинаков для всех каналов: почистить текст, проверить, что он не пустой, отправить, вернуть квитанцию. Отличается только канал: email, SMS, позже push. Если создавать канал прямо в notify() через new EmailChannel() или match ($type), то каждый новый канал требует правки общего кода, а сам notify() знает обо всех конкретных классах.
Решение
Вынести создание продукта в отдельный метод createChannel(), а общий процесс оставить в базовом классе-создателе Notifier. Наследники (EmailNotifier, SmsNotifier) переопределяют только фабричный метод. notify() работает с абстракцией Channel и не знает, какой канал ему достался.
Ключевое отличие от простой фабрики: создатель сам использует созданный продукт внутри своего алгоритма. Фабричный метод — это «шаг шаблонного метода», отвечающий за создание.
Структура
Правила разделены по уровням: пустой текст отклоняет создатель (общее правило), неверный адрес и обрезку до 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() подставляют нужную |
| Python | ABC + @abstractmethod; продукт описан через Protocol, каналы его не наследуют |
| TypeScript | abstract 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 с настройками поставщика из фабричного метода, подмена парсера в тестах и единственное место выбора импортёра по расширению файла.
- Практика: разбор пакетов приборов учёта - вариант приходит в данных: тип прибора в заголовке пакета передаётся параметром в фабричный метод, общий конвейер проверяет длину и контрольную сумму, а новый тип прибора добавляется наследником без правки конвейера.
Связанные темы
- Практика: импорт прайс-листов — фабричный метод с настройками продукта и подменой в тестах
- Практика: разбор пакетов приборов учёта - параметризованный фабричный метод: парсер выбирает тип прибора из пакета
- Стратегия (Strategy) — Go-версия фабричного метода по сути передаёт стратегию создания.