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

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

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

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

Проблема

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

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

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

Вам нужен такой способ создавать объекты продуктов, чтобы:

  1. Они сочетались с другими продуктами того же семейства.

    Покупатели расстраиваются, если получают несочитающуюся мебель.

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

    Вы часто меняете поставщиков, а они предлагают совершенно разные модели мебели.

Решение

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

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

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

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

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

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

Структура

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

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

  2. Конкретный продукт создаётся внутри методов конкретных фабрик.

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

    Каждому конкретному продукту соответствует своя конкретная фабрика.

  3. Абстрактная фабрика объявляет интерфейс взаимодействия с фабриками, общий для всех конкретных фабрик.

    Здесь вы описываете методы производства всех типов продуктов. Но эти методы должны возвращать абстрактные продукты. Реализации методов будут определены в конкретных фабриках.

  4. Конкретные фабрики реализуют методы, создающие конкретные объекты-продукты. Каждой конкретной фабрике соответствует своя вариация семейства продуктов.

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

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

Псевдокод

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

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

class OSXButton 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 OSXCheckbox 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 OSXFactory implementing GUIFactory is
    method createButton():Button is
        return new OSXButton
    method createCheckbox():Checkbox is
        return new OSXCheckbox


// Код, использующий фабрику, не волнует с какой конкретно фабрикой он работает.
// Все получатели продуктов работают с продуктами через абстрактный интерфейс.
class Application is
    constructor Application(factory: GUIFactory) is
        Button button = factory.createButton()
        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 OSXFactory
            Construct an Application with OSXFactory

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

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

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

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

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

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

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

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

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

  4. Создайте код инициализации фабрики где-то в клиентском коде.

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

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

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

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

Примеры использования

Кросс-платформенные приложения

В каждой операционной системе окна, кнопки и переключатели выглядят немножко по-другому, хотя следуют одному принципу.

Зная это, вы можете построить кросс-платформенное приложение с использованием абстрактной фабрики:

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

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

Java

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

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