(звезда) Кроме того известен как: Виртуальный конструктор, Factory Method

Сущность паттерна

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

Проблема

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

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

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

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

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

Решение

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

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

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

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

Все объекты-товары обязаны иметь общий интерфейс

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

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

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

Структура

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

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

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

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

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

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

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

Псевдокод

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

Пример кросс-платформенного общения

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

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

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

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

class WindowsButton implements Button is
    method render(a, b) is
        // Отрисовать кнопку в стиле Windows.
    method onClick(f) is
        // Навесить на кнопку обработчик событий Windows.

class HTMLButton implements Button is
    method render(a, b) is
        // Вернуть HTML-код кнопки.
    method onClick(f) is
        // Навесить на кнопку обработчик события браузера.


// Базовый класс фабрики. Заметьте, что "фабрика" — это всего
// лишь дополнительная роль для класса. Скорее всего, он уже
// имеет какую-то бизнес-логику, в которой требуется создание
// разнообразных продуктов.
class Dialog is
    method render() is
        // Чтобы использовать фабричный метод, вы должны
        // убедиться в том, что эта бизнес-логика не зависит от
        // конкретных классов продуктов. Button — это общий
        // интерфейс кнопок, поэтому все хорошо.
        Button okButton = createButton()
        okButton.onClick(closeDialog)
        okButton.render()

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


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

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


class Application is
    field dialog: Dialog

    // Приложение создаёт определённую фабрику в зависимости от
    // конфигурации или окружения.
    method initialize() is
        config = readApplicationConfigFile()

        if (config.OS == "Windows") then
            dialog = new WindowsDialog()
        else if (config.OS == "Web") then
            dialog = new WebDialog()
        else
            throw new Exception("Error! Unknown operating system.")

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

Код на PHP

<?php

namespace RefactoringGuruFactoryMethodConceptual;

/**
 * Паттерн Фабричный Метод
 *
 * Назначение: Определяет общий интерфейс для создания объектов в суперклассе,
 * позволяя подклассам изменять тип создаваемых объектов.
 */

/**
 * Класс Создатель объявляет фабричный метод, который должен возвращать объект
 * класса Продукт. Подклассы Создателя обычно предоставляют реализацию этого
 * метода.
 */
abstract class Creator
{
    /**
     * Обратите внимание, что Создатель может также обеспечить реализацию
     * фабричного метода по умолчанию.
     */
    abstract public function factoryMethod(): Product;

    /**
     * Также заметьте, что, несмотря на название, основная обязанность Создателя
     * не заключается в создании продуктов. Обычно он содержит некоторую базовую
     * бизнес-логику, которая основана на объектах Продуктов, возвращаемых
     * фабричным методом. Подклассы могут косвенно изменять эту бизнес-логику,
     * переопределяя фабричный метод и возвращая из него другой тип продукта.
     */
    public function someOperation(): string
    {
        // Вызываем фабричный метод, чтобы получить объект-продукт.
        $product = $this->factoryMethod();
        // Далее, работаем с этим продуктом.
        $result = "Creator: creator's code has just worked with " . $product->operation();

        return $result;
    }
}

/**
 * Конкретные Создатели переопределяют фабричный метод для того, чтобы изменить
 * тип результирующего продукта.
 */
class ConcreteCreator1 extends Creator
{
    /**
     * Обратите внимание, что сигнатура метода по-прежнему использует тип
     * абстрактного продукта, хотя фактически из метода возвращается конкретный
     * продукт. Таким образом, Создатель может оставаться независимым от
     * конкретных классов продуктов.
     */
    public function factoryMethod(): Product
    {
        return new ConcreteProduct1();
    }
}

class ConcreteCreator2 extends Creator
{
    public function factoryMethod(): Product
    {
        return new ConcreteProduct2();
    }
}

/**
 * Интерфейс Продукта объявляет операции, которые должны выполнять все
 * конкретные продукты.
 */
interface Product
{
    public function operation(): string;
}

/**
 * Конкретные Продукты предоставляют различные реализации интерфейса Продукта.
 */
class ConcreteProduct1 implements Product
{
    public function operation(): string
    {
        return "{Result of the ConcreteProduct1}";
    }
}

class ConcreteProduct2 implements Product
{
    public function operation(): string
    {
        return "{Result of the ConcreteProduct2}";
    }
}

/**
 * Клиентский код работает с экземпляром конкретного создателя, хотя и через его
 * базовый интерфейс. Пока клиент продолжает работать с создателем через базовый
 * интерфейс, вы можете передать ему любой подкласс создателя.
 */
function clientCode(Creator $creator)
{
    // ...
    echo "Client: I'm not aware of the creator's class, but it still works.n"
        . $creator->someOperation();
    // ...
}

/**
 * Приложение выбирает тип создателя в зависимости от конфигурации или среды.
 */
echo "App: Launched with the ConcreteCreator1.n";
clientCode(new ConcreteCreator1());
echo "nn";

echo "App: Launched with the ConcreteCreator2.n";
clientCode(new ConcreteCreator2());

Запустить этот код можно тут: https://3v4l.org/YRu5A

Результат:

App: Launched with the ConcreteCreator1.
Client: I'm not aware of the creator's class, but it still works.
Creator: creator's code has just worked with {Result of the ConcreteProduct1}

App: Launched with the ConcreteCreator2.
Client: I'm not aware of the creator's class, but it still works.
Creator: creator's code has just worked with {Result of the ConcreteProduct2}

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

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

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

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

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

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

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

К примеру, вы используете готовый UI-фреймворк для собственного приложения. Однако вот неудача — требуется иметь круглые клавиши, заместо обычных прямоугольных. Вы создаёте класс RoundButton. Однако как сообщить основному классу фреймворка UIFramework, чтоб он сейчас создавал круглые клавиши, заместо обычных?

С этой целью вы создаёте подкласс UIWithRoundButtons из базисного класса фреймворка, переопределяете в нём метод сотворения клавиши (а-ля createButton) и вписываете туда создание собственного класса клавиш. Потом используете UIWithRoundButtons заместо обычного UIFramework.

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

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

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

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

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

А потом возвратить его клиентскому коду.

Однако если вольных объектов нет — сделать новый, не забыв добавить его в хранилище.

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

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

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

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

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

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

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

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

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

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

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

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

Достоинства и недочеты

(плюс) Устраняет класс от привязки к определенным классам товаров.

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

(плюс) Облегчает добавление новых товаров в программку.

(плюс) Реализует принцип открытости/закрытости.

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

Отношения с иными паттернами

  • Почти все архитектуры начинаются с внедрения Фабричного метода (больше обычного и расширяемого через подклассы) и эволюционируют в сторону Абстрактной фабрики, Прототипа либо Строителя (больше гибких, но и поболее трудных).
  • Классы Абстрактной фабрики почаще всего реализуются при помощи Фабричного метода, однако они могут быть построены и на базе Прототипа.
  • Фабричный метод можно применять вместе с Итератором, чтоб подклассы коллекций могли создавать пригодные им итераторы.
  • Прототип не опирается на наследование, но ему необходима непростая операция инициализации. Фабричный метод, напротив, построен на наследовании, но не просит трудной инициализации.
  • Фабричный метод можно изучать как частный случай Шаблонного метода. А также, Фабричный метод часто бывает частью большого класса с Шаблонными методами.

Источники:

(1) https://refactoring.guru/ru/design-patterns/factory-method

Tags

Нет комментариев

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

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