handbookНаписать

patterns/creational/abstract-factory/README.md · тема · 4 языка · 14 тестов

+ практика: платёжные провайдеры

Абстрактная фабрика (Abstract Factory) ​

Создаёт семейство связанных объектов целиком, чтобы клиент не мог смешать продукты разных семейств.

Проблема ​

Отчёт о продажах состоит из заголовка и таблицы и выгружается в HTML для браузера и в Markdown для чата. Если клиент сам создаёт части (new HtmlHeading(), new MarkdownTable()), то ничто не мешает собрать HTML-заголовок с Markdown-таблицей, а появление третьего формата означает правки во всех местах, где создаются части. Продукты связаны: они обязаны быть из одного семейства, и это правило нигде не записано.

Решение ​

Описать продукты интерфейсами (Heading, Table) и ввести фабрику семейства ReportFactory с методом на каждый продукт. Конкретные фабрики HtmlReportFactory и MarkdownReportFactory создают части одного формата. Клиент SalesReport получает фабрику извне и просит у неё обе части: смешать форматы он не может по построению, а новый формат — это новая фабрика и её продукты, без правки клиента.

Отличие от Фабричного метода: там один продукт и решение принимает наследник создателя; здесь несколько продуктов, которые должны сочетаться, и решение принимает объект-фабрика, передаваемый клиенту.

Структура ​

creates

creates

creates

creates

SalesReport

-factory: ReportFactory

+render(title, header, rows) : string

«interface»

ReportFactory

+createHeading() : Heading

+createTable() : Table

HtmlReportFactory

MarkdownReportFactory

«interface»

Heading

+render(text) : string

«interface»

Table

+render(header, rows) : string

HtmlHeading

HtmlTable

MarkdownHeading

MarkdownTable

Продукты не знают друг о друге и о фабрике: HTML-части экранируют текст, Markdown-части нет. Это правило семейства, и оно живёт в продуктах, а не в клиенте. Два продукта на семейство достаточно, чтобы увидеть приём; в жизни их три-пять.

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

Фабрику выбирает не SalesReport, а обработчик запроса на выгрузку. В примере это карта форматов: markdown даёт MarkdownReportFactory для чата, html даёт HtmlReportFactory для браузера. Формат приходит из параметра запроса, по нему берётся фабрика, и только потом создаётся SalesReport: дальше отчёт сам просит у фабрики заголовок и таблицу и не может взять их из разных семейств. Правило семейства видно в выводе: заголовок Tea & Coffee в HTML экранирован, в Markdown нет.

Неизвестный формат pdf отсекается до создания отчёта: обработчик отвечает ошибкой, а SalesReport никогда не видит формат, для которого нет семейства. В приложении карта собирается в DI-контейнере или main, и новый формат - это новая фабрика и одна строка в карте.

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

Реализации ​

Конкретные фабрики состоят из одних new: вся их ценность в том, что они собирают части одного формата в одном месте. Поэтому в TypeScript Markdown-фабрика — просто объектный литерал, а в Go — пустая структура с двумя методами.

ЯзыкОсобенность
PHPinterface ReportFactory + два final class; продукты экранируют через htmlspecialchars
GoФабрика — интерфейс с конструкторами NewHeading/NewTable, конкретные фабрики — пустые структуры; часто семейство сворачивают в один интерфейс с методами-продуктами
PythonФабрика и продукты — Protocol; конкретные классы ничего не наследуют. В Python классы сами фабрики, поэтому семейство нередко задают словарём классов
TypeScriptHTML-фабрика — класс, Markdown-фабрика — объектный литерал: структурная типизация принимает оба

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

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

Тесты прогоняют один клиент с двумя фабриками на одних данных и сверяют результат целиком: так видно, что меняется семейство, а не клиент. Второй тест проверяет правило семейства: HTML экранирует и заголовок, и ячейки.

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

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

  • Продукт один: достаточно Фабричного метода или простой фабрики.
  • Продукты не обязаны сочетаться между собой: отдельные фабрики или DI проще.
  • Семейство одно и второе не предвидится: прямое создание честнее.

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

  • PHP — классическая форма: интерфейс фабрики, интерфейсы продуктов, финальные реализации. Проверка «не смешал» — на уровне типов и в тестах.
  • Go — фабрика как интерфейс конструкторов работает, но часто избыточна: семейство сворачивают в один интерфейс Renderer с методами Heading() и Table(), а «продукты» становятся его методами. Это та же гарантия сочетаемости с меньшим числом типов.
  • Python — Protocol делает приём почти бесплатным: любая пара классов с нужными методами образует семейство. Нередко фабрику заменяют словарём {"html": (HtmlHeading, HtmlTable)}.
  • TypeScript — благодаря структурной типизации фабрика может быть объектным литералом; оба стиля показаны рядом, клиент разницы не видит.

Практика ​

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

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

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