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

Фабричный метод

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

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

Проблема

Представьте, что вы создаёте программу управления грузовыми перевозками. Сперва вы рассчитываете перевозить товары только на автомобилях, поэтому весь ваш код работает с объектами класса Грузовик.

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

Вы радуетесь, потому что, на первый взгляд, всё просто — нужно создать несколько подклассов тут и там. Но вот беда, весь ваш код рассчитан только на грузовики, и для внедрения классов Кораблей придётся перелопатить весь код.

Решение

Паттерн Фабричный метод предлагает заменить непосредственное создание объекта-продукта (через оператор new) вызовом особого «фабричного» метода, который и будет создавать продукт.

// Было:
Truck t = new Truck();

// Стало:
Transport t = createVehicle(); // return new Truck()

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

class Logistics
    abstract method createVehicle(): Transport

class RoadLogistics
    method createVehicle(): Transport is
        return new Truck()

class SeaLogistics
    method createVehicle(): Transport is
        return new Ship()"

Чтобы эта система работала, все создаваемые продукты должны иметь общий интерфейс (например, Транспорт). Конкретные возвращаемые продукты могут быть разными, покуда они реализуют общий интерфейс (например, Грузовик и Корабль оба реализуют интерфейс Транспорт).

interface Transport
    method deliver(something, destination)

class Truck implements Transport
    method deliver(something, destination) is
        // Доставить по земле.

class Ship implements Transport
    method deliver(something, destination) is
        // Доставить по морю.

Пользователи фабричного метода всё равно какой конкретно продукт вернул метод, так как они начнут работать со всеми продуктами через общий интерфейс.

class Logistics
    abstract method createVehicle(): Transport

class SomeClientClass
    someClientMethod(Logistics logistics, package, destination) is
        // создаст какой-то транспорт
        Transport t = logistics.createVehicle();
        // не важно какой, т.к. все транспорты имеют метод deliver
        t.deliver(package, destination);

Структура

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

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

    Конкретные Создатели будут создать и возвращать экземпляры этих классов.

  3. Создатель содержит фабричный метод, который должен вернуть объект типа Продукт. Этот метод может содержать какую-то реализацию по умолчанию либо быть абстрактным. В последнем случае, все Конкретные Создатели должны будут определить свой фабричный метод.

    Несмотря на название, важно понимать, что создание продуктов не является единственной функцией Создателя. В нём содержится и другой полезный код работы с Продуктом. Аналогия: в большой софтверной компании может быть центр подготовки программистов, но основная задача компании — писать код, а не готовить программистов.

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

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

Псевдокод

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

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

// Паттерн Фабричный Метод применим тогда, когда есть иерархия
// классов продуктов.
interface Button is
    method render()
    method onClick(f)

class WindowsButton implements Button is
    method render(a, b) is
        Create and render a Windows looking button.
    method onClick(f) is
        Bind a native OS click event.

class HTMLButton implements Button is
    method render(a, b) is
        Return an HTML representation of a button.
    method onClick(f) is
        Bind a web browser click event.

// Базовый класс фабрики. Заметьте, что "фабрика" – это всего лишь
// дополнительная роль для класса. Он уже имеет какую-то бизнес-логику, в
// которой требуется создание разнообразных продуктов.
class Dialog is
    method renderWindow() is
        Render other window controls.

        Button okButton = createButton();
        okButton.onClick(closeDialog);
        okButton.render();

    // Мы выносим весь код создания продуктов в особый Фабричный метод.
    abstract method createButton()

// Конкретные фабрики переопределяют фабричный метод и возвращают из него
// собственные продукты.
class WindowsDialog extends Dialog is
    method createButton() is
        return new WindowsButton()

class WebDialog extends Dialog is
    method createButton() is
        return new HTMLButton()

class ClientApplication is
    field dialog: Dialog

    // Приложение создаёт определённую фабрику в зависимости от конфигурации
    // или окружения.
    method configure() is
        if (we are in windows environment) then
            dialog = new WindowsDialog()

        if (we are in web environment) then
            dialog = new WebDialog()

    // Весь остальной клиентский код работает с фабрикой и продуктами только
    // через общий интерфейс, поэтому для него неважно какая фабрика
    // была создана.
    method main() is
        dialog.initialize()
        dialog.render()

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

Вам заранее неизвестны типы и зависимости объектов, с которыми будет работать ваш код.

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

Вы делаете свой фреймворк и хотите дать возможность пользователям расширять его части.

Пользователи могут расширять классы вашего фреймворка через наследование, но как сделать так, чтобы фреймворк создавал объекты из этих новых классов вместо стандартных?

Ответ: дать пользователям возможность расширять не только желаемые компоненты, но и классы, которые создают эти компоненты. А для этого создающие классы должны иметь конкретные создающие методы, которые можно определить.

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

Для этого вы создаёте подкласс ФреймворкСКруглымиКнопками из базового класса фреймворка, переопределяете в нём метод создатьКнопку и вписываете туда создание своего класса кнопок. Затем, используете ФреймворкСКруглымиКнопками вместо обычного.

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

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

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

Но вы не можете делать эти проверки в конструкторе, так как он по определению должен вернуть новый объект.

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

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

  1. Приведите все создаваемые продукты к общему интерфейсу.

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

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

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

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

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

    Например, у вас есть иерархия классов Транспорт с подклассами Авиация и Автотранспорт, и продукты Самолёт, Автобус и Грузовик. Как видите, продукты не ложатся 1 в 1 в эту иерархию. Но судя по всему, Автобусы и Грузовики можно создавать из Автотранспорта, передавая в его фабричный метод информацию о том, какие грузы придётся возить.

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

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

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

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

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

Java

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

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