Также известен как Abstract Factory

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

Суть паттерна

Абстрактная фабрика — это порождающий паттерн проектирования, который позволяет создавать семейства связанных объектов, не привязываясь к конкретным классам создаваемых объектов.

Паттерн «Абстрактная фабрика»

Проблема

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

  1. Семейство зависимых продуктов. Скажем, Кресло + Диван + Столик.

  2. Несколько вариаций этого семейства. Например, продукты Кресло, Диван и Столик представлены в трёх разных стилях: Ар-деко, Викторианском и Модерне.

Таблица соотвествия семейства продуктам к их вариациям

Семейства продуктов и их вариации.

Вам нужен такой способ создавать объекты продуктов, чтобы они сочетались с другими продуктами того же семейства. Это важно, так как клиенты расстраиваются, если получают несочетающуюся мебель.

Клиенты расстраиваются, если получают несочетающиеся продукты.

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

Решение

Для начала, паттерн Абстрактная фабрика предлагает выделить общие интерфейсы для отдельных продуктов, составляющих семейства. Так, все вариации кресел получат общий интерфейс Кресло, все диваны реализуют интерфейс Диван и так далее.

Схема иерархии классов кресел.

Все вариации одного и того же объекта должны жить в одной иерархии классов.

Далее, вы создаёте «абстрактную фабрику» — общий интерфейс, который содержит методы создания всех продуктов семейства (например, создатьКресло, создатьДиван и создатьСтолик). Эти операции должны возвращать абстрактные типы продуктов, представленные интерфейсами, которые мы выделили ранее — Кресла, Диваны и Столики.

Схема иерархии классов фабрик.

Конкретные фабрики соответствуют определённой вариации семейства продуктов.

Как насчёт вариаций продуктов? Для каждой вариации семейства продуктов мы должны создать свою собственную фабрику, реализовав абстрактный интерфейс. Фабрики создают продукты одной вариации. Например, ФабрикаМодерн будет возвращать только КреслаМодерн,ДиваныМодерн и СтоликиМодерн.

Клиентский код должен работать как с фабриками, так и с продуктами только через их общие интерфейсы. Это позволит подавать в ваши классы любой тип фабрики и производить любые продукты, ничего не ломая.

Клиентскому коду должно быть всё равно с какой фабрикой работать.

Например, клиентский код просит фабрику сделать стул. Он не знает какого типа фабрика это была. Он не знает, получит викторианский или модерновый стул. Для него важно, чтобы на этом стуле можно сидеть, и чтобы этот стул отлично смотрелся с диваном той же фабрики.

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

Структура

Структура классов паттерна «Абстрактная фабрика»
  1. Абстрактные продукты объявляют интерфейсы продуктов, которые связаны друг с другом по смыслу, но выполняют разные функции.

  2. Конкретные продукты — большой набор классов, которые относятся к различным абстрактным продуктам (кресло/столик), но имеют одни и те же вариации (Викториан./Модерн).

  3. Абстрактная фабрика объявляет методы создания различных абстрактных продуктов (кресло/столик).

  4. Конкретные фабрики относятся каждая к своей вариации продуктов (Викториан./Модерн) и реализуют методы абстрактной фабрики, позволяя создавать все продукты определённой вариации.

  5. Несмотря на то, что конкретные фабрики порождают конкретные продукты, сигнатуры их методов должны возвращать соответствующие абстрактные продукты. Это позволит клиентскому коду, использующему фабрику, не привязываться к конкретным классам продуктов. Клиент сможет работать с любыми вариациями продуктов через абстрактные интерфейсы.

Псевдокод

В этом примере Абстрактная фабрика создаёт кросс-платформенные элементы интерфейса и следит за тем, чтобы они соответствовали выбранной операционной системе.

Кросс-платформенная программа может показывать одни и те же элементы интерфейса, выглядящие чуточку по-другому в различных операционных системах. В такой программе, важно, чтобы все создаваемые элементы всегда соответствовали текущей операционной системе. Вы бы не хотели, чтобы программа, запущенная на Windows, вдруг начала показывать чекбоксы в стиле macOS.

Структура классов примера паттерна «Абстрактной фабрики»

Пример кросс-платформенного графического интерфейса пользователя.

Абстрактная фабрика объявляет список продуктов, которые может запрашивать клиентский код. Конкретные фабрики относятся к различным операционным системам и создают элементы одного и того же вида.

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

Клиентский код не зависит от конкретных класс фабрик и элементов интерфейса. Он общается с ними через абстрактные интерфейсы. Благодаря этому, клиент может работать любой разновидностью фабрик и элементов интерфейса.

Чтобы добавить в программу новую вариацию элементов (например, для поддержки Linux), вам не нужно трогать клиентский код. Достаточно создать ещё одну фабрику, производящую эти элементы.

// Этот паттерн предполагает, что у вас есть несколько семейств продуктов,
// находящихся в отдельных иерархиях классов (Button/Checkbox). Продукты одного
// семейства должны иметь общий интерфейс.
interface Button is
    method paint()

// Все семейства продуктов имеют одинаковые вариации (macOS/Windows).
class WinButton implementing Button is
    method paint() is
        Render a button in a Windows style

class MacButton implementing Button is
    method paint() is
        Render a button in a Mac OS X style


interface Checkbox is
    method paint()

class WinCheckbox implementing Checkbox is
    method paint() is
        Render a checkbox in a Windows style

class MacCheckbox implementing Checkbox is
    method paint() is
        Render a checkbox in a Mac OS X style


// Абстрактная фабрика знает обо всех (абстрактных) типах продуктов.
interface GUIFactory is
    method createButton():Button
    method createCheckbox():Checkbox


// Каждая конкретная фабрика знает и создаёт только продукты своей вариации.
class WinFactory implementing GUIFactory is
    method createButton():Button is
        return new WinButton
    method createCheckbox():Checkbox is
        return new WinCheckbox

// Несмотря на то что фабрики оперируют конкретными классами, их методы
// возвращают абстрактные типы продуктов. Благодаря этому, фабрики можно
// взаимозаменять, не изменяя клиентский код.
class MacFactory implementing GUIFactory is
    method createButton():Button is
        return new MacButton
    method createCheckbox():Checkbox is
        return new MacCheckbox


// Код, использующий фабрику, не волнует с какой конкретно фабрикой он работает.
// Все получатели продуктов работают с продуктами через абстрактный интерфейс.
class Application is
    private field button: Button;
    constructor Application(factory: GUIFactory) is
        this.factory = factory
    method createUI();
        this.button = factory.createButton()
    method paint();
        button.paint()


// Приложение выбирает тип и создаёт конкретные фабрики динамически исходя из
// конфигурации или окружения.
class ApplicationConfigurator is
    method main() is
        Read the configuration file
        If the OS specified in the configuration file is Windows, then
            Construct a WinFactory
            Construct an Application with WinFactory
        else
            Construct an MacFactory
            Construct an Application with MacFactory

Применимость

Когда бизнес-логика программы должна работать с разными видами связанных друг с другом продуктов, не завися от конкретных классов продуктов.

Абстрактная фабрика скрывает от клиентского кода подробности того, как и какие конкретно объекты будут созданы. Но при этом клиентский код может работать со всеми типами создаваемых продуктов, так как их общий интерфейс был заранее определён.

Когда в программе уже используется Фабричный метод, но очередные изменения предполагают введение новых типов продуктов.

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

Шаги реализации

  1. Создайте таблицу соотношений типов продуктов к вариациям семейств продуктов.

  2. Сведите все вариации продуктов к общим интерфейсам.

  3. Определите интерфейс абстрактной фабрики. Он должен иметь фабричные методы для создания каждого из типов продуктов.

  4. Создайте классы конкретных фабрик, реализовав интерфейс абстрактной фабрики. Этих классов должно быть столько же, сколько и вариаций семейств продуктов.

  5. Измените код инициализации программы так, чтобы она создавала определённую фабрику и передавала её в клиентский код.

  6. Замените в клиентском коде участки создания продуктов через конструктор вызовами соответствующих методов фабрики.

Преимущества и недостатки

  • Гарантирует сочетаемость создаваемых продуктов.
  • Избавляет клиентский код от привязки к конкретным классам продуктов.
  • Выделяет код производства продуктов в одно место, упрощая поддержку кода.
  • Упрощает добавление новых продуктов в программу.
  • Реализует принцип открытости/закрытости.
  • Усложняет код программы за счёт множества дополнительных классов.
  • Требует наличия всех типов продуктов в каждой вариации.

Отношения с другими паттернами

Реализация в различных языках программирования

Java

Дополнительные материалы

  • Если вы уже слышали о Фабрике, Фабричном методе и Абстрактной фабрике, но с трудом их различаете — почитайте нашу статью Сравнение фабрик.