Абстрактная фабрика (Abstract Factory)
Создаёт семейство связанных объектов целиком, чтобы клиент не мог смешать продукты разных семейств.
Проблема
Отчёт о продажах состоит из заголовка и таблицы и выгружается в HTML для браузера и в Markdown для чата. Если клиент сам создаёт части (new HtmlHeading(), new MarkdownTable()), то ничто не мешает собрать HTML-заголовок с Markdown-таблицей, а появление третьего формата означает правки во всех местах, где создаются части. Продукты связаны: они обязаны быть из одного семейства, и это правило нигде не записано.
Решение
Описать продукты интерфейсами (Heading, Table) и ввести фабрику семейства ReportFactory с методом на каждый продукт. Конкретные фабрики HtmlReportFactory и MarkdownReportFactory создают части одного формата. Клиент SalesReport получает фабрику извне и просит у неё обе части: смешать форматы он не может по построению, а новый формат — это новая фабрика и её продукты, без правки клиента.
Отличие от Фабричного метода: там один продукт и решение принимает наследник создателя; здесь несколько продуктов, которые должны сочетаться, и решение принимает объект-фабрика, передаваемый клиенту.
Структура
Продукты не знают друг о друге и о фабрике: HTML-части экранируют текст, Markdown-части нет. Это правило семейства, и оно живёт в продуктах, а не в клиенте. Два продукта на семейство достаточно, чтобы увидеть приём; в жизни их три-пять.
Как это собирается
Фабрику выбирает не SalesReport, а обработчик запроса на выгрузку. В примере это карта форматов: markdown даёт MarkdownReportFactory для чата, html даёт HtmlReportFactory для браузера. Формат приходит из параметра запроса, по нему берётся фабрика, и только потом создаётся SalesReport: дальше отчёт сам просит у фабрики заголовок и таблицу и не может взять их из разных семейств. Правило семейства видно в выводе: заголовок Tea & Coffee в HTML экранирован, в Markdown нет.
Неизвестный формат pdf отсекается до создания отчёта: обработчик отвечает ошибкой, а SalesReport никогда не видит формат, для которого нет семейства. В приложении карта собирается в DI-контейнере или main, и новый формат - это новая фабрика и одна строка в карте.
Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.
Реализации
Конкретные фабрики состоят из одних new: вся их ценность в том, что они собирают части одного формата в одном месте. Поэтому в TypeScript Markdown-фабрика — просто объектный литерал, а в Go — пустая структура с двумя методами.
| Язык | Особенность |
|---|---|
| PHP | interface ReportFactory + два final class; продукты экранируют через htmlspecialchars |
| Go | Фабрика — интерфейс с конструкторами NewHeading/NewTable, конкретные фабрики — пустые структуры; часто семейство сворачивают в один интерфейс с методами-продуктами |
| Python | Фабрика и продукты — Protocol; конкретные классы ничего не наследуют. В Python классы сами фабрики, поэтому семейство нередко задают словарём классов |
| TypeScript | HTML-фабрика — класс, Markdown-фабрика — объектный литерал: структурная типизация принимает оба |
Запуск: make test T=patterns/creational/abstract-factory
Плюсы и минусы
Тесты прогоняют один клиент с двумя фабриками на одних данных и сверяют результат целиком: так видно, что меняется семейство, а не клиент. Второй тест проверяет правило семейства: HTML экранирует и заголовок, и ячейки.
- ✅ Продукты одного семейства гарантированно сочетаются: клиент не может собрать смесь.
- ✅ Новое семейство — новая фабрика и её продукты, клиент не меняется.
- ✅ Семейство выбирается один раз, при создании фабрики (конфиг, окружение, тест).
- ❌ Новый продукт в семействе — правка интерфейса фабрики и всех конкретных фабрик.
- ❌ Много мелких классов ради двух-трёх продуктов; в динамических языках часто избыточно.
Когда не применять
- Продукт один: достаточно Фабричного метода или простой фабрики.
- Продукты не обязаны сочетаться между собой: отдельные фабрики или DI проще.
- Семейство одно и второе не предвидится: прямое создание честнее.
Отличия между языками
- PHP — классическая форма: интерфейс фабрики, интерфейсы продуктов, финальные реализации. Проверка «не смешал» — на уровне типов и в тестах.
- Go — фабрика как интерфейс конструкторов работает, но часто избыточна: семейство сворачивают в один интерфейс
Rendererс методамиHeading()иTable(), а «продукты» становятся его методами. Это та же гарантия сочетаемости с меньшим числом типов. - Python —
Protocolделает приём почти бесплатным: любая пара классов с нужными методами образует семейство. Нередко фабрику заменяют словарём{"html": (HtmlHeading, HtmlTable)}. - TypeScript — благодаря структурной типизации фабрика может быть объектным литералом; оба стиля показаны рядом, клиент разницы не видит.
Практика
Учебный пример выбирает семейство в тесте. В проекте семейство выбирают данные, выбор меняется со временем, а продукты одного семейства должны находить друг друга спустя месяцы. Практика: платёжные провайдеры показывает это на платежах: шлюз, возвраты и вебхуки одного провайдера, маршрутизация по валюте из конфига, и возврат, который всегда уходит провайдеру, принявшему платёж.
Связанные темы
- Практика: платёжные провайдеры — семейство выбирается данными и восстанавливается из них
- Фабричный метод (Factory Method) — один продукт и наследник вместо семейства и объекта-фабрики.
- Стратегия (Strategy) — фабрика, переданная клиенту, по сути стратегия создания.