Найти в Дзене
ZDG

Шаблоны проектирования: Фабрика

Оглавление

Данный материал требует знакомства с ООП, в частности с наследованием, интерфейсами, абстрактными и статическими методами.

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

Бывает, что одни и другие шаблоны делают практически одно дело, что тоже вызывает вопрос – почему у них разные названия? Действительно, грань иногда очень тонка, но различия проявляются в ваших намерениях, даже если они отсутствуют в коде. Осознанно используя тот или иной шаблон, вы следуете определённой парадигме, которая не должна позволить заплутать в дальнейшем.

Сейчас мы рассмотрим шаблон Фабрика (Factory). На самом деле есть три варианта: Фабрика (Factory), Фабричный Метод (Factory Method) и Абстрактная Фабрика (Abstract Factory), которые часто путают.

Какая задача решается

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

Вы пишете игру с самолётиком, навстречу которому вылетают враги: оса, жук, муха (ну а почему нет, если они - инопланетные мутанты).

Каждый из них в вашей программе представлен своим классом:

  • оса - класс Wasp
  • жук - класс Bug
  • муха - класс Fly

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

var enemy = new Wasp();

Это создалась оса, но как узнать, какого конкретно врага создавать, если его тип станет известен только в процессе работы программы?

Вы можете генерировать врагов случайным образом, или читать их из карты уровня, но в любом случае вы получите не готовый класс, а лишь его идентификатор. Например, если вы считаете, что оса это 0, жук это 1, а муха это 2, то можете сгенерировать случайное число от 0 до 2, или получить его из карты уровня. Вы также можете использовать вместо чисел строковые идентификаторы, вроде 'wasp', 'bug', 'fly', но всё равно из этих строк нельзя создать объект, вам нужен класс.

Поэтому в коде появляются условия вроде:

Если это ровно одно место в вашем коде, то по сути это личинка фабрики и вы можете забить на улучшения. Но как правило, создавать объекты таким способом вам придётся в нескольких местах (например, на экране результатов, в справочнике монстров, и т.д.) По этой причине, или просто для красоты кода условия можно вынести в отдельный класс-фабрику.

Вместо создания объекта напрямую мы будем обращаться к фабрике, например: произведи продукт с идентификатором 'wasp'. Или, произведи продукт с идентификатором 'bug'. И фабрика сама создаст объект соответствующего класса и вернёт его нам.

Условия, конечно, никуда не денутся, они просто перекочуют внутрь фабрики (это типа JavaScript):

И тут мы как бы думаем – и всё? А разговоров-то было. Просто перенесли условия в другое место. Скорее всего, мы так делали уже сто раз без всяких там шаблонов (на самом деле это и был шаблон). Но это уже хорошо. Механизм создания объектов инкапсулирован в одном классе и более нигде. Кроме того, фабрика может решать дополнительные задачи. Во-первых, она может не просто производить объект, но и делать какую-то сложную и громоздкую его инициализацию, перед тем как отдать нам. Мы будем освобождены от этого во всех других местах программы. Во-вторых, фабрика даже не обязана создавать новый объект, она может вернуть уже существующий. Эта логика также будет инкапсулирована внутри неё и клиенту (то есть тому, кто обращается к фабрике) это знать не нужно.

Обращаясь к методу getEnemy(), клиент не в курсе вообще, какие классы там участвуют в создании объекта, он просто ожидает получить объект с известным клиенту интерфейсом. Поэтому и класс Wasp, и класс Bug, и класс Fly должны либо реализовывать один интерфейс, либо наследоваться от одного родительского класса.

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

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

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

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

В каких случаях это нужно? Предположим, что в другой игре-бродилке герой заходит в разные случайно сгенерированные пещеры. Там появляются враги соответствующего пещере типа. Допустим, враги бывают: тролль, орк, имп. Если мы зашли в пещеру троллей, то нужно делать enemy = new Troll(), чтобы создать тролля. Если зашли в пещеру орков, то enemy = new Orc(), чтобы создать орка, и т.д.

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

Вместо этого мы делаем абстрактный класс пещеры (например AbstractCave), который содержит все необходимые общие свойства и методы для пещер, а также абстрактный метод createEnemy(). Абстрактный метод обязывает потомков реализовать его.

И теперь вместо enemy = new Troll() и пр. мы будем обращаться к методу сгенерированной пещеры: enemy = cave.createEnemy()

Вот так мы избавились от необходимости писать new и конкретный класс. Но... но ведь... createEnemy() – абстрактный метод, а значит нужно его реализовать. Для этого мы отнаследуем от AbstractCave конкретные пещеры: TrollCave, OrcCave, ImpCave. И в каждой из них сделаем конкретную реализацию метода createEnemy (теперь на Java):

Шаблон называется "Фабричный метод", потому что "фабричное" поведение имеет один метод createEnemy.

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

cave = new TrollCave();

и далее мы попросим у него создать нам врага:

enemy = cave.createEnemy();

в результате чего конкретная реализация TrollCave вернёт нам объект класса Troll.

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

Но если вы пытливы, то заметите одну тревожную деталь: а как именно мы будем создавать конкретный класс пещеры, если мы его заранее тоже не знаем? Фактически, нам опять нужны условия: если получилась пещера троллей, то нужно создать TrollCave, если орков, то OrcCave, и т.д. За что боролись?

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

И ещё: а разве это не то же самое, что шаблон Стратегия? Ведь в Стратегии мы тоже устанавливаем объекту разное поведение во время исполнения программы.

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

Абстрактная Фабрика

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

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

Например, в вашем лабиринте герой входит в пещеры, которые делятся по стихиям: Огненная пещера, Водяная пещера, Железная пещера и т.д.

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

У интерфейса пещеры есть методы для генерации каждого врага:

cave.createTroll()
cave.createOrc()
cave.createImp()

Соответственно, конкретные реализации пещер будут написаны так: у огненной пещеры метод createTroll() возвращает огненного тролля. У водяной пещеры метод createTroll() возвращает водяного тролля, и т.д.

Для запуска шаблона в дело вам нужно, как в Фабричном Методе, на этапе генерации пещеры определить её тип и породить соответствущую конкретную реализацию (возможно, используя для этого другой фабричный шаблон). А далее всё как по маслу: если вам нужно в этой пещере создать 5 троллей, 3 орка и 10 импов, то вы просто вызовете 5 раз метод createTroll(), 3 раза createOrc() и 10 раз createImp(), и порождённые объекты будут соответствовать стихии пещеры. Ну и конечно, для работы с этими объектами они должны иметь один интерфейс или наследоваться от общего предка, т.е. FireTroll, WaterTroll и IronTroll имеют общий интерфейс Troll или наследуются от общего предка Troll.

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

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

  • Фабрика: класс-фабрика создаёт разные объекты по их идентификатору или другим признакам (самый известный случай).
  • Фабричный Метод: несколько классов-фабрик имеют общий метод для создания объекта, но тип объекта определяется типом фабрики. Класс-фабрика конкретного типа выбирается предварительно через условия.
  • Абстрактная Фабрика: несколько классов-фабрик имеют общие методы для порождения семейства объектов. То есть это как Фабричный Метод, но тот имеет только один метод для одного объекта, а Абстрактная Фабрика имеет несколько методов для нескольких разных объектов, объединённых каким-то принципом (семейство).

Альтернативные варианты порождения объектов

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

if (enemy_type == 'mosquito') return new Mosquito();

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

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

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

Посмотрим пример на Java. Там есть такой метод:

Class.forName()

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

var enemy = Class.forName("Mosquito");
return enemy.newInstance();

Мы нашли класс с именем Mosquito и вернули объект этого класса. Теперь, передавая любые имена классов, мы будем получать в ответ объекты этих классов.

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

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

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

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

Читайте также: Шаблоны проектирования, MVC, Стратегия, Синглтон, Визитёр