Абстрактная фабрика

Абстрактная фабрика — это порождающий паттерн проектирования, который позволяет создавать семейства связанных объектов, не привязываясь к конкретным классам этих объектов.
Проблема
Продолжим пример с автомобилями из статьи про Фабричный метод. Допустим, кроме двигателя у машины есть ещё и шины, и оба компонента должны быть от одного бренда — нельзя поставить двигатель Mercedes с шинами от BMW, если мы собираем «чистый» автомобиль конкретной марки.
Если создавать двигатель и шины через отдельные независимые фабрики, ничто не мешает случайно перепутать бренды:
class CarAssembler
{
public function assemble(
IEngineFactory $engineFactory,
ITireFactory $tireFactory
): Car {
$engine = $engineFactory->make();
$tires = $tireFactory->make();
return new Car($engine, $tires);
}
}
// Ничто не мешает передать сюда несовместимую пару фабрик:
$assembler->assemble(new MercedesEngineFactory(), new BmwTireFactory());Компилятор такую ошибку не поймает — она всплывёт только в рантайме, а то и вовсе останется незамеченной. Проблема в том, что связность между продуктами одного семейства нигде не выражена явно.
Решение
Абстрактная фабрика вводит один интерфейс, который умеет создавать весь набор связанных продуктов сразу. Конкретная реализация этого интерфейса гарантирует, что все продукты будут из одного семейства.
Начнём с интерфейсов продуктов:
interface IEngine
{
public function describe(): string;
}
interface ITires
{
public function describe(): string;
}И их конкретных реализаций для каждого бренда:
class MercedesEngine implements IEngine
{
public function describe(): string
{
return 'Двигатель Mercedes';
}
}
class MercedesTires implements ITires
{
public function describe(): string
{
return 'Шины Mercedes';
}
}
class BmwEngine implements IEngine
{
public function describe(): string
{
return 'Двигатель BMW';
}
}
class BmwTires implements ITires
{
public function describe(): string
{
return 'Шины BMW';
}
}Теперь — абстрактная фабрика, объявляющая метод на каждый продукт семейства:
interface ICarPartsFactory
{
public function createEngine(): IEngine;
public function createTires(): ITires;
}И конкретные фабрики, каждая из которых производит только совместимые между собой продукты:
class MercedesPartsFactory implements ICarPartsFactory
{
public function createEngine(): IEngine
{
return new MercedesEngine();
}
public function createTires(): ITires
{
return new MercedesTires();
}
}
class BmwPartsFactory implements ICarPartsFactory
{
public function createEngine(): IEngine
{
return new BmwEngine();
}
public function createTires(): ITires
{
return new BmwTires();
}
}Клиентский код теперь работает с одной фабрикой и физически не может собрать машину из деталей разных брендов:
class CarAssembler
{
public function assemble(ICarPartsFactory $factory): Car
{
$engine = $factory->createEngine();
$tires = $factory->createTires();
return new Car($engine, $tires);
}
}
$assembler = new CarAssembler();
$mercedes = $assembler->assemble(new MercedesPartsFactory());
$bmw = $assembler->assemble(new BmwPartsFactory());Чтобы добавить новый бренд — например, Audi — нужно реализовать интерфейсы продуктов и одну новую фабрику. CarAssembler при этом не меняется вообще:
class AudiEngine implements IEngine
{
public function describe(): string
{
return 'Двигатель Audi';
}
}
class AudiTires implements ITires
{
public function describe(): string
{
return 'Шины Audi';
}
}
class AudiPartsFactory implements ICarPartsFactory
{
public function createEngine(): IEngine
{
return new AudiEngine();
}
public function createTires(): ITires
{
return new AudiTires();
}
}Из чего состоит паттерн
- Абстрактные продукты (
IEngine,ITires) — интерфейсы для каждого вида продукта в семействе. - Конкретные продукты (
MercedesEngine,BmwTires, ...) — реализации интерфейсов, сгруппированные по семействам (брендам). - Абстрактная фабрика (
ICarPartsFactory) — интерфейс с методом создания на каждый вид продукта. - Конкретная фабрика (
MercedesPartsFactory,BmwPartsFactory, ...) — реализация, производящая продукты только одного семейства. - Клиент (
CarAssembler) — работает исключительно через интерфейсыICarPartsFactory,IEngine,ITiresи не знает о конкретных классах.
Абстрактная фабрика vs Фабричный метод
Оба паттерна прячут new за интерфейсом и решают похожую задачу, но на разном уровне:
| Фабричный метод | Абстрактная фабрика | |
|---|---|---|
| Что создаёт | Один продукт | Семейство связанных продуктов |
| Как расширяется | Подкласс переопределяет один метод | Реализуется один интерфейс с несколькими методами |
| Механизм | Наследование | Композиция (фабрика передаётся как объект) |
| Типичный вопрос | «Какой объект создать?» | «Какой набор согласованных объектов создать?» |
На практике Абстрактную фабрику часто реализуют через несколько Фабричных методов внутри одного класса — как в примере выше, где createEngine() и createTires() в MercedesPartsFactory можно рассматривать как два фабричных метода, объединённых общим интерфейсом.
Когда применять
- Система должна работать с несколькими семействами продуктов, но использовать в моменте только одно семейство.
- Важно гарантировать, что связанные продукты используются вместе и не смешиваются между семействами.
- Хочется скрыть от клиентского кода не только классы продуктов, но и то, как они группируются.
Плюсы и минусы
Паттерн гарантирует совместимость продуктов внутри семейства и изолирует клиентский код от конкретных классов — переключиться на другое семейство можно, передав другую фабрику, без единой правки в бизнес-логике.
Минус — это самый «тяжёлый» из порождающих паттернов: на каждое семейство нужны отдельные классы продуктов и отдельная фабрика, а добавление нового вида продукта (не бренда, а, например, «коробка передач») требует правки интерфейса фабрики и всех её реализаций сразу.
Итог
Если Фабричный метод отвечает на вопрос «как создать один объект, не привязываясь к его классу», то Абстрактная фабрика отвечает на вопрос «как создать согласованный набор объектов, не позволяя собрать несовместимую комбинацию». Это тот случай, когда лишний код фабрик — разумная цена за невозможность допустить ошибку в рантайме.