Абстрактная Фабрика
Абстрактная фабрика — это порождающий паттерн проектирования, который предоставляет интерфейс для создания семейств взаимосвязанных или взаимозависимых объектов, не специфицируя их конкретные классы.
Паттерн решает проблему создания объектов, когда система должна быть независима от процесса их генерации, композиции и представления. Он используется, когда клиентскому коду необходимо работать с несколькими семействами продуктов, но при этом он не должен зависеть от конкретных классов этих продуктов.
Для этого создаётся абстрактный класс-фабрика с набором абстрактных методов для создания каждого продукта в семействе. Затем для каждой вариации семейства реализуется конкретная фабрика, которая возвращает определённые, но гарантированно совместимые между собой, экземпляры продуктов. Клиентский код оперирует только интерфейсами (абстрактной фабрики и абстрактных продуктов), что позволяет легко заменять целые семейства объектов, просто подставляя другую реализацию фабрики.
Другими словами
Представьте, что вы пришли в IKEA. Вам нужен не просто стол, а полный комплект мебели для комнаты: стол, стул, шкаф и кровать. Главное — чтобы всё сочеталось по стилю.
Вы можете бегать по всему магазину и пытаться подобрать предметы по отдельности, рискуя ошибиться (например, взять стул в стиле хай-тек к столу в стиле прованс). А можете просто выбрать одну мебельную серию, например, «ХЕМНЭС». В этой серии все предметы уже созданы так, чтобы идеально подходить друг другу по цвету, материалу и дизайну.
Абстрактная фабрика — это и есть этот «каталог серий» в мире программирования.
Вы выбираете одну «серию» (например, ФабрикаСтиляАр-деко), и она поставляет вам полный, гарантированно совместимый набор объектов (креслоАр-деко, диванАр-деко, столикАр-деко). Вам не нужно думать о деталях — только выбрать нужный стиль. А если захотите сменить его на «Модерн», вы просто возьмёте другой «каталог» (ФабрикуСтиляМодерн), не меняя свой основной код.
Проблема
Представьте, что мы пишем симулятор выживания в мире после апокалипсиса. Важная часть игры — постройка убежищ. Код содержит:
- Семейство зависимых компонентов. Скажем, любое убежище состоит из Стены + Дверь + Источник питания.
- Несколько вариаций этого семейства. Например, компоненты Стены, Дверь и Источник питания могут быть собраны в разных стилях, в зависимости от доступных ресурсов и технологий: Хлам-тек (из подручных материалов), Военный (из армейских компонентов) и Высокотехнологичный (из довоенных научных разработок).
Вам нужно, чтобы компоненты убежища были совместимы друг с другом. Это важно, потому что выжившие не смогут установить бронированную дверь военного образца в хлипкую стену из металлолома. Несоответствие технологий может привести к катастрофе.
Кроме того, вы не хотите переписывать основной код игры каждый раз, когда добавляете новый тип убежищ или их компоненты. В мире игры могут найтись чертежи для постройки, например, Био-убежища, и вы бы не хотели менять существующую логику строительства при добавлении новых возможностей.
Решение
Для начала паттерн Абстрактная фабрика предлагает выделить общие интерфейсы для отдельных компонентов, составляющих убежище. Так, все вариации стен получат общий интерфейс Стена, все двери реализуют интерфейс Дверь и так далее.
Далее вы создаёте абстрактную фабрику убежищ — общий интерфейс, который содержит методы создания всех компонентов семейства (например, создатьСтену, создатьДверь и создатьИсточникПитания). Эти операции должны возвращать абстрактные типы компонентов, представленные интерфейсами, которые мы выделили ранее — Стена, Дверь и ИсточникПитания.
Как насчёт вариаций убежищ? Для каждой вариации (стиля) мы должны создать свою собственную фабрику, реализовав абстрактный интерфейс. Фабрики создают компоненты одной вариации. Например, ФабрикаВоенногоБункера будет возвращать только ЖелезобетонныеСтены, БронированнуюДверь и ДизельныйГенератор. Таким образом, клиентский код, отвечающий за постройку, получает фабрику и использует её для создания совместимых частей, не вникая в детали их реализации.
Пример
Для примера, предположим, что мы создаём убежища двух типов: из подручных материалов (Хлам-тек) и укрепленный военный бункер. Каждый тип убежища требует своих стен, двери и источника питания.
// Интерфейс для стен убежища
interface Wall {
void getDescription();
}
// Интерфейс для двери
interface Door {
void getDescription();
}
// Интерфейс для источника питания
interface PowerSource {
void getDescription();
}Теперь создадим конкретные реализации компонентов для убежища в стиле Хлам-тек:
// Стена из металлолома
class ScrapWall implements Wall {
@Override
public void getDescription() {
System.out.println("Стена, собранная из ржавого листового металла и мусора.");
}
}
// Дверь из досок и арматуры
class ScrapDoor implements Door {
@Override
public void getDescription() {
System.out.println("Дверь, сколоченная из досок и укрепленная арматурой.");
}
}
// Генератор из старого автомобильного двигателя
class CarEngineGenerator implements PowerSource {
@Override
public void getDescription() {
System.out.println("Источник питания: шумный генератор на базе автомобильного двигателя.");
}
}Теперь создадим аналогичные реализации для Военного бункера:
// Железобетонная стена
class ReinforcedConcreteWall implements Wall {
@Override
public void getDescription() {
System.out.println("Стена из армированного железобетона, выдержит небольшой взрыв.");
}
}
// Бронированная дверь-шлюз
class BlastDoor implements Door {
@Override
public void getDescription() {
System.out.println("Герметичная бронированная дверь-шлюз.");
}
}
// Военный дизельный генератор
class MilitaryDieselGenerator implements PowerSource {
@Override
public void getDescription() {
System.out.println("Источник питания: надежный и мощный военный дизельный генератор.");
}
}Теперь определим интерфейс абстрактной фабрики, которая будет создавать наборы компонентов:
// Интерфейс абстрактной фабрики убежищ
interface ShelterFactory {
Wall createWall();
Door createDoor();
PowerSource createPowerSource();
}И, наконец, создадим конкретные фабрики для каждого типа убежища:
// Фабрика для создания убежища из хлама
class ScrapShelterFactory implements ShelterFactory {
@Override
public Wall createWall() {
return new ScrapWall();
}
@Override
public Door createDoor() {
return new ScrapDoor();
}
@Override
public PowerSource createPowerSource() {
return new CarEngineGenerator();
}
}
// Фабрика для создания военного бункера
class MilitaryBunkerFactory implements ShelterFactory {
@Override
public Wall createWall() {
return new ReinforcedConcreteWall();
}
@Override
public Door createDoor() {
return new BlastDoor();
}
@Override
public PowerSource createPowerSource() {
return new MilitaryDieselGenerator();
}
}Теперь код, отвечающий за строительство, может получить нужную фабрику (например, на основе найденных чертежей) и построить цельное, совместимое убежище, не зная, из каких именно деталей оно состоит.
Шаги реализации
- Создайте таблицу соотношений типов продуктов к вариациям семейств продуктов.
- Сведите все вариации продуктов к общим интерфейсам.
- Определите интерфейс абстрактной фабрики. Он должен иметь фабричные методы для создания каждого из типов продуктов.
- Создайте классы конкретных фабрик, реализовав интерфейс абстрактной фабрики. Этих классов должно быть столько же, сколько и вариаций семейств продуктов.
- Измените код инициализации программы так, чтобы она создавала определённую фабрику и передавала её в клиентский код.
- Замените в клиентском коде участки создания продуктов через конструктор вызовами соответствующих методов фабрики.
Вопросы
1. В чем разница между Абстрактной фабрикой и Фабричным методом?
Фабричный метод — это один метод для создания одного объекта. Решение о том, какой класс создавать, делегируется подклассам (наследование).
Абстрактная фабрика — это интерфейс для создания семейства взаимосвязанных объектов. Использует композицию: клиентский код работает с объектом фабрики.
Аналогия: Фабричный метод — это станок, производящий один тип детали (гвозди). Абстрактная фабрика — это целый завод (например, «Ивановский мебельный завод»), который производит всё для комплекта мебели (столы, стулья, шкафы).
2. В чем разница между Абстрактной фабрикой и Строителем (Builder)?
Абстрактная фабрика фокусируется на создании семейства объектов. Продукты создаются и возвращаются сразу.
Строитель фокусируется на пошаговом создании одного сложного объекта. Продукт возвращается только в конце, после вызова метода build().
Пример: Абстрактная фабрика создаст вам сразу Двигатель, Кузов и Колеса для автомобиля Ford, в зависимости от выбранной реализации/семейства. Строитель будет по шагам собирать один сложный объект Дом, вызывая методы добавитьПотолок(), добавитьПол(), добавитьСтены(); вы можете выбирать части из разных семейств, постепенно создавая свой дом мечты
3. Назовите главный недостаток Абстрактной фабрики
Главный недостаток — сложность добавления нового типа продукта. Если вы захотите добавить в семейство Лампу (к Креслу, Дивану, Столику), вам придётся изменить интерфейс Абстрактной Фабрики, а затем — все её конкретные реализации (ФабрикаАр-деко, ФабрикаМодерн и т.д.).
Задача
«Представьте, что у нас есть код, который напрямую создает объекты: new WindowsButton(), new WindowsCheckbox(). Проведите рефакторинг этого кода с использованием паттерна Абстрактная фабрика».
- Создать интерфейсы Button, Checkbox.
- Создать интерфейс GUIFactory с методами createButton() и createCheckbox().
- Создать классы WindowsButton, WindowsCheckbox и WindowsFactory.
- Создать классы MacButton, MacCheckbox и MacFactory.
- В клиентском коде заменить new WindowsButton() на factory.createButton().