Также известен как Виртуальный конструктор, Factory Method

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

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

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

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

Проблема

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

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

Проблема с добавлением нового класса в программу

Добавить новый класс не так то просто, если весь код уже завязан на конкретные классы.

Отличные новости, правда?! Но как насчёт кода? Большая часть существующего кода жёстко привязана к классам Грузовиков. Чтобы добавить в программу классы морских Судов понадобится перелопатить всю программу. Более того, если вы потом решите добавить ещё один вид транспорта, то всю эту работы придётся повторить.

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

Решение

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

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

Структура классов-создателей

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

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

Структура иерархии продуктов

Все объекты-продукты должны иметь общий интерфейс.

Например, классы Грузовик и Судно реализуют интерфейс Транспорт с методом доставить. Каждый из этих классов реализует метод по-своему: грузовики везут грузы по земле, а судна — по морю. Фабричный метод в классе ДорожнойЛогистики вернёт грузовик, а класс МорскойЛогистики — судно.

Программа после применения фабричного метода

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

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

Структура

Структура классов паттерна «Фабричный метод»
  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 фреймворк для своего приложения. Но вот беда, требуется иметь круглые кнопки, вместо стандартных прямоугольных. Вы создаёте класс RoundButton. Но как сказать главному классу фреймворка (UIFramework), чтобы он теперь создавал круглые кнопки, вместо стандартных.

Для этого вы создаёте подкласс UIWithRoundButtons из базового класса фреймворка, переопределяете в нём метод создания кнопки (createButton) и вписываете туда создание своего класса кнопок. Затем, используете UIWithRoundButtons вместо стандартного UIFramework.

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

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

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

  1. Сперва вам следует создать общее хранилище, чтобы хранить вы нём все создаваемые объекты.
  2. При запросе нового объекта, нужно будет заглянуть в хранилище и проверить, есть ли там неиспользуемый объект.
  3. А затем вернуть его клиентскому коду.
  4. Но если свободных объектов нет — создать новый, не забыв добавить его в хранилище.

Весь этот код нужно куда-то поместить, чтобы не засорять клиентский код.

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

Значит, должен другой метод, который бы отдавал как существующие, так и новые объекты. Ним и станет фабричный метод.

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

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

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

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

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

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

  4. Для каждого типа продуктов заведите подкласс и переопределите в нём фабричный метод. Переместите туда код создания соответствующего продукта из суперкласса.

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

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

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

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

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

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

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

Java

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

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