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

Что это?
В прошлой статье мы узнали о паттерне фабричный метод . Затем мы увидели, как можно смоделировать варианты F-16, используя заводской метод. Но в этой статье будет рассказано, как представить множество других самолетов, кроме F-16. Например, предположим, что клиент покупает Boeing 747 для поездки своего генерального директора и хочет, чтобы ваше программное обеспечение поддерживало этот новый тип самолета.
Шаблон абстрактной фабрики решает проблему создания семейств связанных продуктов. Например, F-16 нужен двигатель, кабина и крылья. Для Boeing-747 потребуются те же детали, но они будут специфичными для Boeing. Любому самолету потребуются эти три взаимосвязанные части; однако детали будут специфичными для самолета. Вы видите, что здесь возникает закономерность? Во-первых, нам нужна структура для создания связанных частей для каждого самолета.
Шаблон абстрактной фабрики — это шаблон проектирования, позволяющий создавать семейства связанных или зависимых объектов без указания их конкретных классов.
Диаграмма классов
Диаграмма классов включает в себя следующие сущности:
- Абстрактная фабрика
- Бетонный завод
- Абстрактный продукт
- Бетонный продукт
- Клиент

Шаблон абстрактной фабрики позволяет создавать семейство связанных объектов без указания их конкретных классов. Рассмотрим пример. Допустим, вы создаете программное обеспечение для моделирования для авиационной отрасли и вам нужно представить различные самолеты в виде объектов. Но перед представлением самолета вам также необходимо определить другие части самолета как объекты. Давайте выберем три: крылья, кабину и двигатель. Теперь предположим, что первый самолет, который вы хотите изобразить, это могучий F-16. Вероятно, вы напишете три класса, по одному для каждой части, относящейся к F-16. В своем коде вы, скорее всего, будете использовать эти только что созданные три класса следующим образом:

Этот фрагмент кода кажется безобидным, но может вызвать сильные головные боли, если ваше программное обеспечение для моделирования взлетит, и вам нужно расширить его на другие самолеты. Вот некоторые из проблем с приведенным выше кодом:
- Конкретные классы для трех частей были представлены потребителю напрямую.
- F-16 имеет несколько вариантов с разными двигателями. Если вы хотите вернуть объект двигателя, соответствующий варианту, вам нужно создать подкласс класса F-16Engine, что также потребует изменения потребительского фрагмента.
- Список во фрагменте кода объявлен параметризованным с помощью конкретного класса; если вы добавите в свою программу еще один авиационный двигатель, он не будет распознан Списком, хотя двигатели для всех самолетов в чем-то похожи.
Относитесь к интерфейсу как к абстрактному понятию, а не как к реализации
Хороший объектно-ориентированный дизайн означает скрытие конкретных классов, используемых в вашем приложении, и предоставление интерфейсов клиентам. Объект отвечает на набор запросов, которые могут быть перехвачены интерфейсом, реализованным классом объекта. Клиенты должны знать, на какие требования отвечает объект, а не на реализацию.
В нашем примере мы создадим интерфейс, который предоставляет метод start() . Затем класс F16Engine изменится следующим образом:

Посмотрите, как изменится соответствующий потребительский код с вышеуказанным изменением.

Код потребителя теперь свободен от деталей реализации того, как работает двигатель F-16 и какой класс его реализует. Однако мы по-прежнему не хотим раскрывать часть кода F16Engine() . Мы хотим, чтобы наши потребители догадывались о том, экземпляр какого класса мы создаем. Это обсуждается далее.
Создание фабрики
Вместо создания объектов в клиентском коде у нас будет фабричный класс, который создает запрошенные объекты и возвращает их клиенту. Мы назовем этот класс F16Factory , потому что он может производить различные части военного самолета F16 и доставлять их заказчику. Класс будет иметь следующую форму.

Теперь предположим, что мы передаем объект F16Factory клиентскому коду в качестве аргумента конструктора, и тогда он может создавать такие объекты:

Обратите внимание, как эта настройка позволяет нам изменить конкретный класс, представляющий F16Engine , если он привязан к интерфейсу IEngine . Мы можем переименовывать, улучшать или модифицировать наш класс, не вызывая критических изменений в клиенте. Также обратите внимание, что изменив только класс фабрики, переданный в конструктор клиента, мы можем предоставить клиенту разные детали для совершенно нового самолета.
Фабрика фабрик: Большая фабрика, производящая другие фабрики.
Разве не было бы здорово использовать один и тот же клиентский фрагмент для разных типов самолетов, таких как Boeing 747 или российский МиГ-29 ? Если все фабрики, реализующие createEngine() , договорятся о стандартном интерфейсе, наш клиентский код продолжит работать для всех авиационных фабрик. Но все фабрики должны будут использовать этот общий интерфейс, методы которого они будут реализовывать, и этот знакомый интерфейс является абстрактной фабрикой .
Реализация
Начнем с интерфейса, определяющего методы, которые необходимо реализовать на разных авиационных заводах. Клиентский код написан для этого интерфейса, но будет составлен во время выполнения с конкретной фабрикой.

Когда мы говорим об « интерфейсе », мы имеем в виду интерфейс Java или абстрактный класс. В этом случае мы могли бы использовать абстрактный класс, если бы для любого из продуктов существовали реализации по умолчанию. Методы создания не возвращают конкретные продукты — они возвращают интерфейсы, чтобы отделить потребителей фабрики от конкретной реализации частей.
Формальное определение шаблона абстрактной фабрики гласит, что это шаблон проектирования, определяющий интерфейс для создания семейств связанных продуктов без указания конкретных классов. В данном случае этим интерфейсом является IAircraftFactory , и его методы создания возвращают не существенные части, а интерфейсы, реализованные классами конкретных частей.
Далее, давайте настроим заводы для наших двух самолетов.

Бетонный завод будет производить двигатель, кабину и крылья для F-16 или Boeing. Каждая часть имеет соответствующий интерфейс продукта, который мы не приводим здесь для краткости. Интерфейсы продукта, представляющие части, будут следующими:
IEngine
ICockpit
IWings
В предыдущей статье мы создали класс для F-16, который включал метод fly() . Этот метод вызывает makeF16() , который создает экземпляр F-16, а затем вызывает Taxi() . Как только он оказался на взлетно-посадочной полосе, он улетел. Мы можем применить тот же подход к нашему примеру с Боингом-747. Все самолеты действуют по одной и той же схеме: производятся, выруливают на взлетно-посадочную полосу, а затем улетают. Таким образом, мы можем создать класс самолета, который выполняет эти три задачи. Обратите внимание, что мы не создаем отдельные классы для представления двух самолетов (например, F-16 и Boeing-747), а создаем один класс Aircraft , который может представлять оба.

Пока что мы оставим метод makeAircraft пустым. Итак, давайте сначала посмотрим, как клиент будет запрашивать объекты F-16 и Boeing-747.

Чтобы добавить конструктор в наш класс Aircraft , нам нужно создать новую переменную экземпляра с именем factory. Фабрика хранит объект, который будет отвечать за создание частей самолета. Мы можем получить другой самолет, составив объект самолета с другой фабрикой. Полная версия класса Aircraft приведена ниже:

Клиент должен создать экземпляр правильной фабрики, которую он передает классу Aircraft . Класс Aircraft представляет клиента. Мы могли бы создать интерфейс IAircraft для представления всех самолетов, которые может создать наша фабрика, но для нашего ограниченного примера в этом нет необходимости.
Полученный код легко расширяется и модифицируется.
Чтобы продолжить наш пример из статьи о шаблоне фабричного метода , мы можем использовать либо подкласс класса F16Factory для создания фабрик для вариантов A и B F-16, либо параметризовать его с помощью перечисления, определяющего вариантную модель, и вернуть правильную часть в переключателе. утверждение.
Другие примеры
- Шаблон абстрактной фабрики помогает создавать семейства связанных продуктов. Например, если ваша библиотека предоставляет модные виджеты пользовательского интерфейса и поддерживает macOS и Windows, вам может понадобиться семейство продуктов, которые работают на этих платформах. Точно так же темы, используемые в IDE, могут быть еще одним примером. Например, если ваша среда IDE поддерживает светлые и темные темы, она может использовать шаблон абстрактной фабрики для создания виджетов, относящихся к светлой или темной теме, путем изменения конкретной фабрики, создающей виджеты.
javax.xml.parsers.DocumentBuilderFactory.newInstance()
вернет вам фабрику.javax.xml.transform.TransformerFactory.newInstance()
вернет вам фабрику.
- Фабричный метод и абстрактный фабричный шаблон похожи в том, что они помогают управлять созданием объектов. Разница между этими двумя моделями заключается в их мотивации. Шаблон фабричный метод обычно отвечает за создание одного продукта, тогда как абстрактный шаблон фабрики создает целые семейства связанных продуктов. Кроме того, мы используем наследование в шаблоне фабричных методов для создания более специализированных продуктов. Наконец, мы практикуем композицию объектов в абстрактном фабричном шаблоне, передавая фабрики, потребляемые для создания желаемых результатов.
- Мы создаем бетонный завод, когда добавляем новый самолет в наш флот. Но предположим, что к парку добавляется вертолет, и ему требуется деталь, которой нет у самолета. В этом случае нам потребуется расширить интерфейс IAircraftFactory другим творческим методом для той части, которая требуется только для вертолета. Это приведет к каскадному изменению существующих фабрик, которые должны возвращать значение null, поскольку новый компонент не является частью струй.
- Бетонные фабрики лучше всего реализовывать как одноэлементные объекты.