Рассмотрим следующий сценарий.

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

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

Давайте думать снизу вверх.

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

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

Образец кода:

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

В приведенном выше фрагменте кода вызывающий метод getCoffee() может установить другое количество молока, сахара, воды и т. Д. После получения экземпляра кофе. Но правильно ли это делать? Должен ли вызывающий абонент заботиться о логике создания экземпляров кофейных объектов? Вместо этого внутри самого блока switch-case вы можете установить эти параметры при создании экземпляра. Ok! это немного лучший подход. Но код быстро становится громоздким, если я прошу вас добавить поддержку большего количества напитков. Пример - добавить опору для кокса, кокс можно смешивать с водой или содой, но не с молоком или кофейным порошком. Итак, кокс имеет совершенно другой набор ингредиентов. И ваш блок корпуса переключателя должен быть снова изменен. Что, если я скажу, что покупатель может настроить напиток - он может выбрать, нужен ли ему сахар или нет, - сколько сахара, нужно ли ему смешать воду и молоко в кофе или нет, или нужна ли ему сода в коксе или нет. Как вы это поддержите?

В этот момент вы, вероятно, думаете о создании объекта типа Customization, который содержит различные поля, например - milk, water, soda, sugar и т. Д., И передать его вашей фабрике или блоку switch-case. Итак, теперь ваш код для создания кофе выглядит примерно так:

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

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

Рассмотрим другой сценарий: что, если я скажу, что значения DEFAULT, определенные выше, должны поступать из другого файла конфигурации, источника данных или другой службы? Скажем, значения по умолчанию или формула приготовления кофе взяты из службы 1, а формула кокса - из службы 2. Как вы внесете эти изменения в приведенный выше код? В настоящее время вы предполагаете, что вся формула является постоянной, но владелец торгового автомата не хочет устанавливать в автомате какую-либо постоянную формулу, скорее у него есть собственный сервер, на котором размещена формула, и он хочет получить оттуда, вероятно, потому что формула секретная формула, и он не хочет, чтобы кто-либо видел эту формулу. Сценарий гипотетический, но в той или иной форме вполне возможен.

Ваш текущий дизайн не поддерживает это. Вам нужно снова и снова изменить один и тот же код. Это просто невозможно.

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

В нашем сценарии мы поддерживаем фактически разные продукты, но все они в широком смысле определяются как напитки, по крайней мере, для потребителя. Формулу любого напитка должно быть легко изменить. Таким образом, мы точно имеем дело с полиморфизмом времени выполнения, но в то же время логика создания экземпляра или логика установки параметров полностью отличаются друг от друга. Мы широко имеем дело с напитками, но их природа и ингредиенты принципиально отличаются. Под зонтиком напитков они принадлежат к разным категориям, например, черный кофе относится к категории кофе, CocaCola относится к категории кока-колы, апельсиновый / манговый сок принадлежит к категории соков и т. Д. Создание таких разных категорий объектов под одним и тем же зонтом поддерживается абстрактным фабричным методом. .

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

Давайте переделаем торговый автомат, как показано на рисунке ниже. Посмотрим, что предлагает нам это моделирование.

У нас есть Product интерфейс, который является супертипом конечного продукта, которого мы ожидаем от системы.

Кофейные продукты Cappuccino, BlackCoffee, соки, такие как Lemonade, продукты из колы, такие как CocaCola, другие продукты, такие как HotMilk - все они реализуют интерфейс Product, потому что, согласно предположению нашей системы, мы создаем разные продукты с одним и тем же интерфейсом для конечного пользователя.

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

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

Обратите внимание, как различаются методы установки для всех продуктов: Cappuccino & BlackCoffee имеют setMilk() метод, а Lemonade & CocaCola их нет. Точно так же у CocaCola есть setCoke(), а у других этого нет. Эти методы очень специфичны для продукта и необходимы для установки правильного количества ингредиентов.

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

  • Как уже упоминалось, вызывающему абоненту все равно, как создаются продукты.
  • Логика создания экземпляров сосредоточена в фабрике, поэтому, если вам нужно удалить какую-то существующую реализацию, удалите продукт и фабрику.
  • Если вам нужно добавить новый продукт, сначала проверьте, можно ли использовать какой-либо из существующих продуктов и фабрик для поддержки этого нового продукта без особых изменений, в противном случае добавьте новый продукт и фабрику - никаких изменений не требуется в большей части существующего кода, только вероятно вам нужно добавить корпус переключателя в ProductFactory, чтобы получить новую фабрику. Если вы тоже не хотите вносить это изменение, посмотрите мой предыдущий пост, чтобы понять, как динамически регистрировать классы в factory.
  • Поскольку все продукты реализуют интерфейс Product, даже если добавлен новый продукт, вызывающему абоненту не нужно ничего менять на своей стороне для его поддержки.

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

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

Customization & Preparation - это образцы общих объектов, показывающие возможные параметры настройки и общие ингредиенты в системе для разных продуктов.

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

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

Здесь, скажем, DAO1 предоставляет некоторые операции, связанные с сущностью клиента, такие как поиск, создание и т. Д., DAO2 раскрывает операции, связанные с их банковским счетом. У вас есть разные механизмы хранения данных, такие как реляционная база данных, хранилище файлов в формате Xml и хранилище объектов, которые могут хранить данные клиентов и учетных записей в другом формате, но предоставляют их клиенту одинаково. Каждая выделенная фабрика, такая как RdbDAOFactory, может создавать обе DAO реализации, такие как RdbDAO1 и RdbDAO2 и т. Д. Все фабрики расширяют DAOFactory абстрактный суперкласс, который используется клиентом / вызывающей стороной для получения очень конкретного экземпляра фабрики и затем доступа к их соответствующим DAO объектам.

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

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

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

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

Если вы достигли этого, поделитесь этой статьей, чтобы другие люди тоже получили пользу.