что такое фабричный метод
Java. Factory Method Pattern in Game Server
Проблема
Представьте, что вы создаете модуль игровых наград. Первая версия вашего приложения может обрабатывать только награду ЗОЛОТО, поэтому основная часть вашего кода находится внутри класса GoldReward.
Через некоторое время ваша игра становится довольно популярной. Каждый день вы получаете десятки запросов от игроков о добавлении новой валюты в приложение и просьбы разнообразить контент.
Отличные новости, правда? А как насчет кода? В настоящее время большая часть вашего кода связана с классом GoldReward. Добавление ГЕМОВ в приложение потребует внесения изменений во всю кодовую базу. Более того, если позже вы решите добавить в приложение еще один вид НАГРАД, вам, вероятно, придется снова внести все эти изменения. В результате вы получите довольно неприятный код, пронизанный условными выражениями, которые переключают поведение приложения в зависимости от типа НАГРАД.
Решение
Шаблон фабричного метода предлагает заменить прямые вызовы построения объекта (с использованием оператора new) на вызовы специального фабричного метода. Не волнуйтесь: объекты по-прежнему создаются с помощью оператора new, но он вызывается из фабричного метода. Объекты, возвращаемые фабричным методом, часто называют продуктами.
На первый взгляд это изменение может показаться бессмысленным: мы просто переместили вызов конструктора из одной части программы в другую. Однако учтите следующее: теперь вы можете переопределить фабричный метод в подклассе и изменить класс продуктов, создаваемых этим методом.
Однако есть небольшое ограничение: подклассы могут возвращать разные типы продуктов, только если эти продукты имеют общий базовый класс или интерфейс. Кроме того, тип возвращаемого значения для фабричного метода в базовом классе должен быть объявлен как этот интерфейс.
Например, классы GoldReward и GemReward должны реализовывать интерфейс наград, в котором объявляется метод rewardFor. Каждый класс реализует этот метод по-разному: золотая награда увеличивает золото, награда с гемами увеличивает гемы в профиле игрока. Фабричный метод в классе GoldRewardService возвращает объекты золотой награды, тогда как фабричный метод в классе GemRewardService возвращает гемы.
Код, использующий фабричный метод (часто называемый клиентским кодом), не видит разницы между фактическими продуктами, возвращаемыми различными подклассами. Клиент рассматривает все продукты как абстрактную награду. Клиент знает, что все награды должны иметь метод применения награды, но то, как именно он работает, не имеет значения для клиента.
Structure
1. Интерфейс GameItem, который является общим для всех объектов наград, которые могут быть созданы создателем и его подклассами.
Класс ReawardCreator объявляет фабричный метод, который возвращает новые объекты наград. Важно, чтобы тип возвращаемого значения этого метода соответствовал интерфейсу продукта. Вы можете объявить фабричный метод абстрактным, чтобы заставить все подклассы реализовывать свои собственные версии метода. В качестве альтернативы базовый фабричный метод может возвращать некоторый тип награды по умолчанию. Обратите внимание: несмотря на название, создание продукта не является основной обязанностью создателя. Обычно класс создателя уже имеет некоторую базовую бизнес-логику, связанную с наградами. Фабричный метод помогает отделить эту логику от конкретных классов наград.
Конкретные создатели переопределяют базовый фабричный метод, поэтому он возвращает другой тип продукта. Обратите внимание, что фабричный метод не должен постоянно создавать новые экземпляры. Он также может возвращать существующие объекты из кеша, пула объектов или другого источника.
Используйте фабричный метод, если вы хотите предоставить пользователям вашей библиотеки или фреймворка способ расширения его внутренних компонентов.
Решение состоит в том, чтобы сократить код, который создает компоненты в рамках платформы, до единого фабричного метода и позволить любому переопределить этот метод в дополнение к расширению самого компонента.
Как реализовать
Создали базовый интерфейс GameItem:
Создали базовый интерфейс GameItem
2. Создадим пару наград и реализуем метод интерфейса:
3. Дальше нам потребуется ItemGenerator, который будет открывать награды и создавать их:
4. Давайте создадим конкретные реализации ItemGenerator:
5. Протестируем, то что у нас получилось. Я буду в цикле доставать “случайный” генератор и открывать награду. В терминале можно будет увидеть сообщение об открытии награды.
Ссылка на код будет вот тут. Можно посмотреть реализацию этого паттерна.
На этом разбор фабричного метода закончен. Хотелось бы узнать, встречали ли вы у себя в проектах фабричный метод? Или может вы не осознанно писали код, который получался как фабричный метод? Спасибо, что дочитали до конца.
Сравнение фабрик
В этой статье мы попробуем разобраться чем отличаются:
Во многих книгах и источниках определения «фабрик» даётся авторами по-своему. Это создаёт большую путаницу при чтении материалов в интернете.
Итак, давайте разберёмся в вариациях фабрик, чтобы раз и навсегда понять разницу между ними.
1. Фабрика
Фабрика — это общая концепция проектирования функций, методов и классов, когда какая-то одна часть программы отвечает за создание других частей программы.
Вы можете услышать слово Фабрика от других людей, когда они имеют в виду:
То, что человек имеет в виду, произнося Фабрика, проще всего понять из контекста, мы сейчас рассмотрим все вариации.
2. Создающий метод
Создающий метод Определён в книге Refactoring To Patterns. — это простой метод-обёртка над вызовом конструктора продукта. Выделив создающий метод, вы изолируете любые изменения в конструировании продуктов от основного кода. Например, вы можете вовсе убрать вызов конструктора из создающего метода, отдавая вместо нового какой-то существующий объект.
Многие называют его фабричным методом, только потому, что он создаёт новые объекты. Типичная логика: «этот метод создаёт объекты, а раз все фабрики создают что-то, значит этот метод — фабричный». И это вносит основную путаницу между понятием Создающего метода и паттерном Фабричный метод.
В этом примере, метод next является создающим методом:
3. Статический фабричный метод
Требуется создать разные по функциональности конструкторы, у которых бы совпадали сигнатуры (например, Random(int max) и Random(int min) ). Это невозможно во многих языках программирования, но создав статический метод, вы можете обойти это ограничение.
Хочется повторно использовать готовые объекты, вместо создания новых (например, паттерн Одиночка). При вызове конструктора вы всегда создаёте новый объект. Это можно обойти, если вынести вызов конструктора в новый метод. В этом методе вы можете сначала поискать готовый объект в каком-то кеше, и только если его нет, создать новый объект.
В следующем примере, метод load является статическим фабричным методом — он предоставляет удобный способ загрузить пользователя из базы данных.
4. Паттерн Простая фабрика
Паттерн Простая фабрика Определён в книге Head First Design Patterns. — это класс, в котором есть один метод с большим условным оператором, выбирающим создаваемый продукт. Этот метод вызывают с неким параметром, по которому определяется какой из продуктов нужно создать. У простой фабрики, обычно, нет подклассов.
Обычно, простую фабрику путают с общим понятием Фабрики или с любым из фабричных паттернов.
Если объявить класс простой фабрики абстрактным (Java, C#), это не сделает его одним и тем же, что и абстрактная фабрика!
Вот пример простой фабрики:
Простая фабрика находится в шаге от того, чтобы стать Фабричным методом.
5. Паттерн Фабричный метод
Паттерн Фабричный метод Определён в книге «банды четырёх» Design Patterns: Elements of Reusable Object-Oriented Software. — это устройство классов, при котором подклассы могут переопределять тип создаваемого в суперклассе продукта.
Если вы имеете иерархию продуктов и абстрактный создающий метод, который переопределяется в подклассах, то перед вами паттерн Фабричный метод.
6. Паттерн Абстрактная фабрика
Паттерн Абстрактная фабрика Определён в книге «банды четырёх» Design Patterns: Elements of Reusable Object-Oriented Software. — это устройство классов, облегчающее создание семейств продуктов.
Если у вас нет семейств продуктов, значит не может быть и абстрактной фабрики.
Послесловие
Теперь, когда вы окончательно разобрались в терминологии, почитайте наши описания фабричных паттернов:
Порождающие паттерны
Фабричный метод (Factory Method)
Когда надо применять паттерн
Когда заранее неизвестно, объекты каких типов необходимо создавать
Когда система должна быть независимой от процесса создания новых объектов и расширяемой: в нее можно легко вводить новые классы, объекты которых система должна создавать.
Когда создание новых объектов необходимо делегировать из базового класса классам наследникам
На языке UML паттерн можно описать следующим образом:
Формальное определение паттерна на языке C# может выглядеть следующим образом:
Участники
Абстрактный класс Product определяет интерфейс класса, объекты которого надо создавать.
Конкретные классы ConcreteProductA и ConcreteProductB представляют реализацию класса Product. Таких классов может быть множество
Таким образом, класс Creator делегирует создание объекта Product своим наследникам. А классы ConcreteCreatorA и ConcreteCreatorB могут самостоятельно выбирать какой конкретный тип продукта им создавать.
Теперь рассмотрим на реальном примере. Допустим, мы создаем программу для сферы строительства. Возможно, вначале мы захотим построить многоэтажный панельный дом. И для этого выбирается соответствующий подрядчик, который возводит каменные дома. Затем нам захочется построить деревянный дом и для этого также надо будет выбрать нужного подрядчика:
«Фабричный метод» и «Абстрактная фабрика» во вселенной «Swift» и «iOS»
Слово «фабрика» – безусловно одно из самых часто употребляемых программистами при обсуждении своих (или чужих) программ. Но смысл в него вкладываемый бывает очень разным: это может быть и класс, порождающий объекты (полиморфно или нет); и метод, создающий экземпляры какого-либо типа (статический или нет); бывает, и даже просто любой порождающий метод (включая, конструкторы).
Конечно, не все, что угодно, порождающее экземпляры чего-либо, может называться словом «фабрика». Более того, под этим словом могут скрываться два разных порождающих шаблона из арсенала «Банды четырех» – «фабричный метод» и «абстрактная фабрика», в подробности которых я и хотел бы немного углубиться, уделяя особое внимание классическим их пониманию и реализации.
А на написание этого очерка меня вдохновил Джошуа Керивски (глава «Industrial Logic»), а точнее, его книга «Refactoring to Patterns», которая вышла в начале века в рамках серии книг, основанной Мартином Фаулером (именитым автором современной классики программирования – книги «Рефакторинг»). Если кто-то не читал или даже не слышал о первой (а я знаю таких много), то обязательно добавьте ее себе в список для чтения. Это достойный «сиквел» как «Рефакторинга», так и еще более классической книги – «Приемов объектно-ориентированного проектирования. Паттерны проектирования».
Книга, помимо прочего, содержит в себе несколько десятков рецептов избавления от различных «запахов» в коде с помощью шаблонов проектирования. В том числе и три (как минимум) «рецепта» на обсуждаемую тему.
Абстрактная фабрика
Керивски в своей книге приводит два случая, когда применение этого шаблона будет полезным.
Первый – это инкапсуляция знаний о конкретных классах, связанных общим интерфейсом. В таком случае этими знаниями будет обладать лишь тип, являющейся фабрикой. Публичный API фабрики будет состоять из набора методов (статических или нет), возвращающих экземпляры типа общего интерфейса и имеющих какие-либо «говорящие» названия (чтобы понимать, какой метод необходимо вызвать для той или иной цели).
Второй пример очень похож на первый (и, в общем-то, все сценарии использования паттерна более-менее подобны друг другу). Речь идет о случае, когда экземпляры одного или нескольких типов одной группы создаются в разных местах программы. Фабрика в этом случае опять-таки инкапсулирует знания о создающем экземпляры коде, но с несколько иной мотивацией. Например, это особенно актуально, если процесс создания экземпляров этих типов сложный и не ограничивается вызовом конструктора.
Я постараюсь сохранять примеры кода как можно ближе к классической реализации из книги «Банды четырех», но в реальной жизни часто код бывает упрощенным тем или иным образом. И лишь достаточное понимание шаблона открывает двери для его более вольного использования.
Подробный пример
Предположим, мы в приложении торгуем средствами передвижения, и от типа конкретного средства зависит отображение: мы будем использовать разные подклассы UIViewController для разных средств передвижения. Помимо этого, все средства передвижения различаются состоянием (новые и б/у):
Таким образом, у нас есть семейство объектов одной группы, экземпляры типов которых создаются в одних и тех же местах в зависимости от какого-то условия (например, пользователь нажал на товар в списке, и в зависимости от того, самокат это или велосипед, мы создаем соответствующий контроллер). Конструкторы контроллеров имеют некоторые параметры, которые также необходимо каждый раз задавать. Не свидетельствуют ли эти два довода в пользу создания «фабрики», которая одна будет обладать знаниями о логике создания нужного контроллера?
Конечно, пример достаточно простой, и в реальном проекте в похожем случае вводить «фабрику» будет явным «overengineering». Тем не менее, если представить, что типов транспортных средств у нас не два, а параметров у конструкторов – не один, то преимущества «фабрики» станут более очевидными.
Итак, объявим интерфейс, который будет играть роль «абстрактной фабрики»:
(Довольно краткий «гайдлайн» по проектированию «API» на языке «Swift» рекомендует называть «фабричные» методы начиная со слова «make».)
(Пример в книге банды четырех приведен на «C++» и основывается на наследовании и «виртуальных» функциях. Используя «Swift» нам, конечно, ближе парадигма протокольно-ориентированного программирования.)
Интерфейс абстрактной фабрики содержит всего два метода: для создания контроллеров для продажи велосипедов и самокатов. Методы возвращают экземпляры не конкретных подклассов, а общего базового класса. Таким образом, ограничивается область распространения знаний о конкретных типах пределами той области, в которой это действительно необходимо.
В качестве «конкретных фабрик» будем использовать две реализации интерфейса абстрактной фабрики:
В данном случае, как видно из кода, конкретные фабрики отвечают за транспортные средства разного состояния (новые и подержанные).
Создание нужного контроллера отныне будет выглядеть примерно так:
Инкапусляция классов с помощью фабрики
Теперь вкратце пробежимся по примерам использования, которые предлагает в своей книге Керивски.
Первый «кейс» связан с инкапсуляцией конкретных классов. Для примера возьмем те же контроллеры для отображения данных о транспортных средствах:
Перемещение знаний о создании объекта внутрь фабрики
Второй «кейс» описывает сложную инициализацию объекта, и Керивски, в качестве одного из путей упрощения кода и оберегания принципов инкапсуляции, предлагает ограничение распространения знаний о процессе инициализации пределами фабрики.
Предположим, мы захотели продавать заодно уж и автомобили. А это, несомненно, более сложная техника, обладающая бóльшим числом характеристик. Для примера ограничимся типом используемого топлива, типом трансмиссии и размером колесного диска:
Пример инициализации соответствующего контроллера:
Мы можем ответственность за все эти «мелочи» водрузить на «плечи» специализированной фабрики:
И создавать контроллер уже таким образом:
Фабричный метод
Книга «Банды четырех» сообщает, что шаблон также известен под названием «виртуальный конструктор», и это не зря. В «C++» виртуальной называется функция, переопределяемая в производных классах. Возможности объявить виртуальным конструктор язык не дает, и не исключено, что именно попытка сымитировать нужное поведение привела к изобретению данного паттерна.
Полиморфное создание объектов
В качестве классического примера пользы шаблона рассмотрим случай, когда в иерархии разные типы имеют идентичную реализацию одного метода за исключением объекта, который в этом методе создается и используется. В качестве решения предлагается создание этого объекта вынести в отдельный метод и реализовывать его отдельно, а общий метод – поднять выше в иерархии. Таким образом, разные типы будут использовать общую реализацию метода, а объект, необходимый для этого метода, будет создаваться полиморфно.
Для примера вернемся к нашим контроллерам для отображения транспортных средств:
И предположим, что для их отображения используется некая сущность, например, координатор, который представляет эти контроллеры модально из другого контроллера:
При этом метод start() используется всегда одинаково, за исключением того, что в нем создаются разные контроллеры:
Предлагаемое решение – это вынести создание используемого объекта в отдельный метод:
А основной метод – снабдить базовой реализацией:
Конкретные типы в таком случае примут вид:
Заключение
Я попытался данную несложную тему осветить, совместив три подхода:
Как оказалось, найти подробные материалы на тему, содержащие прикладные примеры довольно сложно. Большинство существующих статей и руководств содержат лишь поверхностные обзоры и сокращенные примеры, уже довольно урезанные по сравнению с хрестоматийными версиями реализаций.
Надеюсь, хотя бы отчасти мне удалось достичь поставленных целей, а читателю – хотя бы отчасти было интересно или хотя бы любопытно узнать или освежить свои знания по данной теме.
Другие мои материалы на тему шаблонов проектирования:
Фабричный метод
Фабричный метод — это порождающий паттерн проектирования, который определяет общий интерфейс для создания объектов в суперклассе, позволяя подклассам изменять тип создаваемых объектов.
В какой-то момент ваша программа становится настолько известной, что морские перевозчики выстраиваются в очередь и просят добавить поддержку морской логистики в программу.
Добавить новый класс не так-то просто, если весь код уже завязан на конкретные классы.
В итоге вы получите ужасающий код, наполненный условными операторами, которые выполняют то или иное действие, в зависимости от класса транспорта.
Подклассы могут изменять класс создаваемых объектов.
На первый взгляд, это может показаться бессмысленным: мы просто переместили вызов конструктора из одного конца программы в другой. Но теперь вы сможете переопределить фабричный метод в подклассе, чтобы изменить тип создаваемого продукта.
Чтобы эта система заработала, все возвращаемые объекты должны иметь общий интерфейс. Подклассы смогут производить объекты различных классов, следующих одному и тому же интерфейсу.
Все объекты-продукты должны иметь общий интерфейс.
Пока все продукты реализуют общий интерфейс, их объекты можно взаимозаменять в клиентском коде.
Продукт определяет общий интерфейс объектов, которые может произвести создатель и его подклассы.
Конкретные продукты содержат код различных продуктов. Продукты будут отличаться реализацией, но интерфейс у них будет общий.
Создатель объявляет фабричный метод, который должен возвращать новые объекты продуктов. Важно, чтобы тип результата совпадал с общим интерфейсом продуктов.
Зачастую фабричный метод объявляют абстрактным, чтобы заставить все подклассы реализовать его по-своему. Но он может возвращать и некий стандартный продукт.
Несмотря на название, важно понимать, что создание продуктов не является единственной функцией создателя. Обычно он содержит и другой полезный код работы с продуктом. Аналогия: большая софтверная компания может иметь центр подготовки программистов, но основная задача компании — создавать программные продукты, а не готовить программистов.
Конкретные создатели по-своему реализуют фабричный метод, производя те или иные конкретные продукты.
Фабричный метод не обязан всё время создавать новые объекты. Его можно переписать так, чтобы возвращать существующие объекты из какого-то хранилища или кэша.
В этом примере Фабричный метод помогает создавать кросс-платформенные элементы интерфейса, не привязывая основной код программы к конкретным классам элементов.
Пример кросс-платформенного диалога.
Фабричный метод объявлен в классе диалогов. Его подклассы относятся к различным операционным системам. Благодаря фабричному методу, вам не нужно переписывать логику диалогов под каждую систему. Подклассы могут наследовать почти весь код из базового диалога, изменяя типы кнопок и других элементов, из которых базовый код строит окна графического пользовательского интерфейса.
Базовый класс диалогов работает с кнопками через их общий программный интерфейс. Поэтому, какую вариацию кнопок ни вернул бы фабричный метод, диалог останется рабочим. Базовый класс не зависит от конкретных классов кнопок, оставляя подклассам решение о том, какой тип кнопок создавать.
Такой подход можно применить и для создания других элементов интерфейса. Хотя каждый новый тип элементов будет приближать вас к Абстрактной фабрике.
Когда заранее неизвестны типы и зависимости объектов, с которыми должен работать ваш код.
Фабричный метод отделяет код производства продуктов от остального кода, который эти продукты использует.
Благодаря этому, код производства можно расширять, не трогая основной. Так, чтобы добавить поддержку нового продукта, вам нужно создать новый подкласс и определить в нём фабричный метод, возвращая оттуда экземпляр нового продукта.
Когда вы хотите дать возможность пользователям расширять части вашего фреймворка или библиотеки.
Пользователи могут расширять классы вашего фреймворка через наследование. Но как сделать так, чтобы фреймворк создавал объекты из этих новых классов, а не из стандартных?
Решением будет дать пользователям возможность расширять не только желаемые компоненты, но и классы, которые создают эти компоненты. А для этого создающие классы должны иметь конкретные создающие методы, которые можно определить.
Когда вы хотите экономить системные ресурсы, повторно используя уже созданные объекты, вместо порождения новых.
Такая проблема обычно возникает при работе с тяжёлыми ресурсоёмкими объектами, такими, как подключение к базе данных, файловой системе и т. д.
Представьте, сколько действий вам нужно совершить, чтобы повторно использовать существующие объекты:
Весь этот код нужно куда-то поместить, чтобы не засорять клиентский код.
Самым удобным местом был бы конструктор объекта, ведь все эти проверки нужны только при создании объектов. Но, увы, конструктор всегда создаёт новые объекты, он не может вернуть существующий экземпляр.
Значит, нужен другой метод, который бы отдавал как существующие, так и новые объекты. Им и станет фабричный метод.
Приведите все создаваемые продукты к общему интерфейсу.
В классе, который производит продукты, создайте пустой фабричный метод. В качестве возвращаемого типа укажите общий интерфейс продукта.
Затем пройдитесь по коду класса и найдите все участки, создающие продукты. Поочерёдно замените эти участки вызовами фабричного метода, перенося в него код создания различных продуктов.
В фабричный метод, возможно, придётся добавить несколько параметров, контролирующих, какой из продуктов нужно создать.
На этом этапе фабричный метод, скорее всего, будет выглядеть удручающе. В нём будет жить большой условный оператор, выбирающий класс создаваемого продукта. Но не волнуйтесь, мы вот-вот исправим это.
Для каждого типа продуктов заведите подкласс и переопределите в нём фабричный метод. Переместите туда код создания соответствующего продукта из суперкласса.
Если создаваемых продуктов слишком много для существующих подклассов создателя, вы можете подумать о введении параметров в фабричный метод, которые позволят возвращать различные продукты в пределах одного подкласса.
Если после всех перемещений фабричный метод стал пустым, можете сделать его абстрактным. Если в нём что-то осталось — не беда, это будет его реализацией по умолчанию.
- Избавляет класс от привязки к конкретным классам продуктов. Выделяет код производства продуктов в одно место, упрощая поддержку кода. Упрощает добавление новых продуктов в программу. Реализует принцип открытости/закрытости.
- Может привести к созданию больших параллельных иерархий классов, так как для каждого класса продукта надо создать свой подкласс создателя.
Многие архитектуры начинаются с применения Фабричного метода (более простого и расширяемого через подклассы) и эволюционируют в сторону Абстрактной фабрики, Прототипа или Строителя (более гибких, но и более сложных).
Классы Абстрактной фабрики чаще всего реализуются с помощью Фабричного метода, хотя они могут быть построены и на основе Прототипа.
Фабричный метод можно использовать вместе с Итератором, чтобы подклассы коллекций могли создавать подходящие им итераторы.
Прототип не опирается на наследование, но ему нужна сложная операция инициализации. Фабричный метод, наоборот, построен на наследовании, но не требует сложной инициализации.
Фабричный метод можно рассматривать как частный случай Шаблонного метода. Кроме того, Фабричный метод нередко бывает частью большого класса с Шаблонными методами.
Не втыкай в транспорте
Лучше почитай нашу книгу о паттернах проектирования.
Теперь это удобно делать даже во время поездок в общественном транспорте.
Эта статья является частью нашей электронной книги Погружение в Паттерны Проектирования.