Замена конструктора фабричным методом

Обновлено: 09.05.2024

Ранее мы говорили про шаблоны проектирования Одиночка и Стратегия, про тонкости реализации на языке Golang.

Сегодня расскажу про Фабричный метод.

В чем суть?

Фабричный метод (Factory method) также известный как Виртуальный конструктор (Virtual Constructor) - пораждающий шаблон проектирования, определяющий общий интерфейс создания объектов в родительском классе и позволяющий изменять создаваемые объекты в дочерних классах.

Шаблон позволяет классу делегировать создание объектов подклассам. Используется, когда:

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

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

Создаваемые объекты родительского класса специализируются подклассами.

Какую задачу решает?

Представьте, что вы создали программу управления доставкой еды. В программе в качестве единственного средства доставки используется электро-самокат. Ваши курьеры на электро-самокатах развозят еду из пункта А в пункт Б. Все просто.

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

Вы обнаруживаете, что большая часть ваших сущностей в программе сильно связаны с объектом Самокат и чтобы заставить вашу программу работать с другими способами доставки, вам придется добавить связи в 80% вашей кодовой базы и так повторить для каждого нового транспорта. Знакомая ситуация?

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

И какое решение?

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

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

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

Посмотрим на диаграмму классов такого подхода.

Диаграмма классов Factory Method

Диаграмма классов Factory Method

Реализация на Golang

Пример реализации на PHP, можно изучить тут. Так как в Golang отсутствуют возможности ООП, такие как классы и наследование, то реализовать в классическом виде этот шаблон невозможно. Несмотря на это, мы можем реализовать базовую версию шаблона - Простая фабрика.

В нашем примере есть файл iTransport.go, который определяет методы создаваемых транспортных средств для доставки еды. Сущность транспорта будем хранить в структуре (struct), которая применяет интерфейс iTransport.

Также реализуем файл Factory.go, который представляет фабрику создания нужных объектов. Клиентский код реализован в файле main.go. Вместо прямого создания конкретных объектов транспорта клиентский код будет использовать для этого метод фабрики getTransport(t string) , передавая нужный тип объекта в виде аргумента функции.

Когда применять?

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

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

Какие преимущества?

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

Упрощает добавление новых продуктов в программу.

Реализует принцип открытости/закрытости (англ. open–closed principle, OCP) — принцип ООП, устанавливающий следующее положение: «программные сущности (классы, модули, функции и т. п.) должны быть открыты для расширения, но закрыты для изменения»

Какие недостатки?

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

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

Рад был поделиться материалом, Alex Versus. Публикация на английском.
Всем удачи!

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

Модернизация простой фабрики

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

От простой фабрики к фабричному методу

  1. Вернем метод createCoffee(CoffeeType type) в класс CoffeeShop .
  2. Данный метод сделаем абстрактным.
  3. Сам класс CoffeeShop станет абстрактным.
  4. У класса CoffeeShop появятся наследники.

Принцип работы фабричного метода

  1. Все продукты — реализации абстрактного класса Coffee .
  2. Все создатели — реализации абстрактного класса CoffeeShop .
  3. Мы наблюдаем две параллельные иерархии классов:
    • Иерархия продуктов. Мы видим итальянских потомков и американских потомков
    • Иерархия создателей. Мы видим итальянских потомков и американских потомков
  4. У суперкласса CoffeeShop нет информации о том, какая конкретно реализация продукта ( Coffee ) будет создана.
  5. Суперкласс CoffeeShop делегирует создание конкретного продукта своим потомкам.
  6. Каждый потомок класса CoffeeShop реализует фабричный метод createCoffee() в соответствии со своей спецификой. Иными словами, внутри реализаций классов-создателей принимается решение о приготовлении конкретного продукта, исходя из специфики класса создателя.

Структура фабричного метода

  1. Класс Creator содержит реализации всех методов, взаимодействующих с продуктами, кроме фабричного метода.
  2. Абстрактный метод factoryMethod() должен быть реализован всеми потомками класса Creator .
  3. Класс ConcreteCreator реализует метод factoryMethod() , непосредственно производящий продукт.
  4. Данный класс отвечает за создание конкретных продуктов. Это единственный класс с информацией о создании этих продуктов.
  5. Все продукты должны реализовывать общий интерфейс — быть потомками общего класса-продукта. Это нужно, чтобы классы, использующие продукты, могли оперировать ими на уровне абстракций, а не конкретных реализаций.

Домашнее задание

Итак, сегодня мы провели довольно большую работу и изучили паттерн проектирования фабричный метод. Самое время закрепить пройденный материал! Задание 1. Поработать над открытием еще одной кофейни. Она может быть выполнена в английском стиле или испанском. Или даже в стиле космического корабля. Добавим пищевых красителей в кофе, чтоб блестело, и вообще, кофе будет просто космос! Задание 2. На прошлой лекции у тебя было задание создать виртуальный суши-бар либо виртуальную пиццерию. Твоя задача — не стоять на месте. Сегодня ты узнал, как с помощью шаблона фабричный метод можно придти к успеху. Пора воспользоваться этими знаниями и расширить собственный бизнес ;)

Скорее всего до этого момента ты уже сталкивался с паттернами проектирования. Например, с одиночкой (singleton).

Давай вспомним, что такое паттерны, зачем они нужны, что такое порождающие паттерны (к которым и относится одиночка), и изучим новый паттерн — фабричный метод.

Шаблон проектирования или паттерн (design pattern) в разработке программного обеспечения — это повторяемая архитектурная конструкция, которая представляет собой решение проблемы проектирования в рамках некоторого часто возникающего контекста.

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

Порождающие шаблоны (creational patterns) — шаблоны проектирования, которые имеют дело с процессом создания объектов. Они позволяют сделать систему независимой от способа создания, композиции и представления объектов.

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

Какую проблему решает паттерн?

Представь, что ты решил создать программу доставки. Изначально ты будешь нанимать курьеров с автомобилями и в программе в качестве средства доставки использовать объект Автомобиль . Курьеры развозят посылки из пункта А в пункт Б, В и так далее. Всё просто.

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

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

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

Решение проблемы

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

Посмотрим на диаграмму классов такого подхода:

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

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

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

Реализация на Java:

Если мы захотим создать новый объект доставки, то программа автоматически от её вида создаст нам объект транспорта.

Когда применять паттерн?

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

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

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

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

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

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

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

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

Вернуть объект клиентскому коду.

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

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

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

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

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

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

Преимущества

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

Недостатки

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

Подведем итог

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

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

Фабричный метод (Factory Method) - это паттерн, который определяет интерфейс для создания объектов некоторого класса, но непосредственное решение о том, объект какого класса создавать происходит в подклассах. То есть паттерн предполагает, что базовый класс делегирует создание объектов классам-наследникам.

Когда надо применять паттерн

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

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

Когда создание новых объектов необходимо делегировать из базового класса классам наследникам

На языке UML паттерн можно описать следующим образом:

Участники

Абстрактный класс Product определяет интерфейс класса, объекты которого надо создавать.

Конкретные классы ConcreteProductA и ConcreteProductB представляют реализацию класса Product. Таких классов может быть множество

Абстрактный класс Creator определяет абстрактный фабричный метод FactoryMethod() , который возвращает объект Product.

Конкретные классы ConcreteCreatorA и ConcreteCreatorB - наследники класса Creator, определяющие свою реализацию метода FactoryMethod() . Причем метод FactoryMethod() каждого отдельного класса-создателя возвращает определенный конкретный тип продукта. Для каждого конкретного класса продукта определяется свой конкретный класс создателя.

Таким образом, класс Creator делегирует создание объекта Product своим наследникам. А классы ConcreteCreatorA и ConcreteCreatorB могут самостоятельно выбирать какой конкретный тип продукта им создавать.

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

В качестве абстрактного класса Product здесь выступает класс House. Его две конкретные реализации - PanelHouse и WoodHouse представляют типы домов, которые будут строить подрядчики. В качестве абстрактного класса создателя выступает Developer, определяющий абстрактный метод Create() . Этот метод реализуется в классах-наследниках WoodDeveloper и PanelDeveloper. И если в будущем нам потребуется построить дома какого-то другого типа, например, кирпичные, то мы можем с легкостью создать новый класс кирпичных домов, унаследованный от House, и определить класс соответствующего подрядчика. Таким образом, система получится легко расширяемой. Правда, недостатки паттерна тоже очевидны - для каждого нового продукта необходимо создавать свой класс создателя.

Game Server

Game Server

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

Проблема

Представьте, что вы создаете модуль игровых наград. Первая версия вашего приложения может обрабатывать только награду ЗОЛОТО, поэтому основная часть вашего кода находится внутри класса GoldReward.


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


Решение

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

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

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

Например, классы GoldReward и GemReward должны реализовывать интерфейс наград, в котором объявляется метод rewardFor. Каждый класс реализует этот метод по-разному: золотая награда увеличивает золото, награда с гемами увеличивает гемы в профиле игрока. Фабричный метод в классе GoldRewardService возвращает объекты золотой награды, тогда как фабричный метод в классе GemRewardService возвращает гемы.

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

Structure

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

Конкретные награды - это разные реализации интерфейса GameItem.

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

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

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

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

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

Как реализовать

Создали базовый интерфейс GameItem:

Создали базовый интерфейс GameItem

2. Создадим пару наград и реализуем метод интерфейса:

3. Дальше нам потребуется ItemGenerator, который будет открывать награды и создавать их:

4. Давайте создадим конкретные реализации ItemGenerator:

Ссылка на код будет вот тут. Можно посмотреть реализацию этого паттерна.

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

Читайте также: