Эй, добро пожаловать обратно в серию шаблонов дизайна, которые я начал распространять с радостью! Не жалейте времени и проверяйте мои предыдущие посты, чтобы получить максимальную отдачу:

Кроме того, поддержите O'Reilly Media, купив Шаблоны дизайна: https://www.oreilly.com/library/view/head-first-design/9781492077992/

Состав:

  • вступление
  • Проблема (в этой статье будет 2 раздела)
  • Принципы дизайна, которым нужно следовать
  • Окончательный код решения (в этой статье будет 2 раздела)
  • Дополнительный раздел
  • Рисунок

Небольшое предисловие: эта модель довольно длинная. На самом деле, он включает в себя 3 варианта, которые я попытаюсь разложить👹 Но чтобы получить максимальную отдачу, ознакомьтесь с упомянутой книгой👀

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

Заводская выкройка

вступление

В предыдущих статьях я много раз упоминал: проектируйте свое программное обеспечение таким образом, чтобы оно было гибким и обслуживаемым. т.е. нам не нужно менять существующий код или разбрасывать кучу операторов if/else, которые делают код хрупким и подверженным изменениям.

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

Давайте посмотрим на плохой дизайн:

На первый взгляд, мы программируем на супертип и можем использовать полиморфизм. Увы, а что, если реализация ParentClass изменится?🧐 К тому же, упомянутые выше if/else попадают сюда =› плохо!

Найдите вещи, которые различаются, и инкапсулируйте их

Как его рефакторить? Давайте сделаем это шаг за шагом:

  1. Что меняется? Создание объекта
  2. Что остается прежним? Методы, применяемые к объектам, поскольку они относятся к одному супертипу

Решение❓ Инкапсулировать то, что меняется (создание объекта), и оставить вызов методов. Также добавьте интерфейс/абстрактный класс.

Часть с созданием объекта нарушает принцип открытия-закрытия из SOLID, так как нам нужно повторно открыть код, чтобы внести изменения

Встречайте так называемую Простую фабрику, которая приходит на помощь! Вы можете найти исходный код в репозитории ниже:



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

  1. PizzaStore.kt — наш основной файл, напоминающий функцию main(). Мы вызываем orderPizza() с type, чтобы начать наше создание. Но здесь мы перемещаем эту изменяющуюся логику в другой класс с именем SimplePizzaFactory, где можно найти изменяющуюся часть.
  2. Pizza.kt — это наш abstract class, который служит родителем для других видов пиццы: Cheese и Pepperoni
  3. После того, как мы создали желаемую пиццу, мы возвращаемся к нашему PizzaStore, где используются методы супертипа (он же Pizza). В связи с тем, что наша бетонная пицца является подвидами, нам все равно, какая из них была возвращена. Следовательно, мы просто применяем к нему методы.

На данный момент у нас есть одно место, SimplePizzaFactory, где сосредоточены все возможные изменения, поэтому нам не нужно повторно открывать наш Store, чтобы внести их туда🔝

Но…

Простая фабрика сама по себе не является шаблоном. Однако многие разработчики считают, что это так‼️

А пока, после этого тяжелого вступления, давайте перейдем к пикантной части и исследуем 2 реальных паттерна🕵🏼‍♂️

Проблема: Первая часть

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

Видите ли, в нашем SimpleFactory наш объект, Фабрика, создает все виды продуктов. Но что, если мы хотим большего разнообразия?

Например, у нас есть abstract creator, также известный как Клиент, со многими concrete creators, также известный как субклиенты. В Клиенте у нас есть абстрактный фабричный метод, который затем реализуется в подклиентах.

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

Вау…🤯 И я знаю, что это кажется размытым, поэтому давайте пошагово.

  1. Мы делаем фабричный метод в наших субклиентах. Этот фабричный метод реализует abstract фабричный метод в абстрактном создателе.
  2. Эти субклиенты имеют дело с созданием экземпляров
  3. Они возвращают объект (который является подклассом абстрактного объекта)
  4. Наш абстрактный клиент обрабатывает его дальше и возвращает обработанный объект нашему конкретному субклиенту, который, в свою очередь, представляет его нам (на самом деле метод был вызван из субклиента, следовательно, этот метод возвращает нам объект)
              abstract creator
               /           \
           sub-creator1    sub-creator2
           /   |    \        /   |   \
                                            
concrete product1 ..2     another concrete product1 ..2
Those products inherit from abstract product

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

Не волнуйтесь, мы снова пойдем шаг за шагом по линии

Также во всей этой схеме abstract creator является нашим Клиентом. Мы инкапсулируем изменения в субклиентах и ​​изолируем в них все. Таким образом, нашему клиенту не нужно много знать, просто иметь этот продукт для обработки (клиент знает только, что этот продукт будет подтипом abstract product)

Окончательный код решения: часть первая



Давайте посмотрим на конкретный пример, чтобы прояснить всю эту суматоху🙌

  1. У нас есть abstractCreator.kt с абстрактным методом createObject() . Но мы не используем этот метод напрямую.
  2. У нас есть подклассы (субклиенты): concreteCreator и anotherConcreteCreator из этого abstractCreator, которые являются нашими локальными создателями. Эти создатели переопределяют/внедряют этот абстрактный метод из родителя, который на самом деле является нашей фабрикой. Таким образом, те субклиенты, которые являются дочерними элементами нашего abstractCreator, имеют доступ к методам родителя. Мы вызываем метод из Клиента (родительского), в моем случае это giveType, который запускает createObject конкретного субклиента.
  3. Этот createObject, опять же, является самой нашей фабрикой, которая занимается созданием объектов. Он возвращает конкретный продукт, который является подпродуктом нашего абстрактного продукта, в наш метод abstractClient giveType.
  4. В этом родительском методе мы ограничены abstract class (родительским) продукта, что позволяет нам применять методы из абстрактного продукта (вспомним полиморфизм и его использование).
  5. Подпродукты различаются в зависимости от клиента. т.е. concreteCreator имеет 2 concreteProducts (которые происходят от абстрактного продукта), anotherConcreteCreator имеет еще 2 anotherConcreteProducts (которые снова наследуют от абстрактного продукта).
  6. Эти продукты, являющиеся подклассами, возвращаются к этому методу giveType в abstractCreator, где применяются методы из abstractProduct.
  7. Затем нам возвращается обработанный объект, и мы можем заниматься им дальше. (Я объяснил выше, почему мы можем использовать метод из родителя и что этот метод дает нам результат напрямую)
  8. Наше giveType() в abstractCreator не связано с конкретным типом продукта. Нам все равно, какой клиент на самом деле возвращает нам продукт, если этот продукт является подклассом абстрактного продукта.

Ухх…😲 Было тяжело. Надеюсь, вы уловили основную красоту такой развязанной фабрики.

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

Используя фабричный шаблон, мы:

- инкапсулировать часть, которая варьируется в классе/методе

- программа для интерфейса, а не для конкретной реализации. т.е. мы знаем, что factory возвращает подтип ofabstractProduct

Принципы дизайна, которым нужно следовать

Еще один принцип от SOLID😈

  1. Принцип инверсии зависимостей: мы зависим от абстракций, а не от конкретных классов.

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

Ниже приведено нарушение принципа:

                   abstractCreator
                     /   |     \
       concreteProduct1        concreteProduct3
                   concreteProduct2

А теперь поступим по принципу:

                      abstractCreator
                           |
                       abstractProduct
                     /      |       \
         concreteProduct1        concreteProduct3
                   concreteProduct2

Посмотрите, как компонент высокого уровня (abstractCreator) и компоненты низкого уровня (concreteProducts) зависят от одной абстракции. Вспомните giveType из пошагового руководства по коду. Это зависит от абстрактного типа продукта. Продукты, созданные в субклиентах, также зависят от одного и того жеабстрактного типа продукта.

Проблема: Вторая часть

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

Плюс, что, если у нас есть тот же concreteProduct, который отличается только типом субпродуктов? т.е. Colour — это абстрактный класс, который имеет 2 похожих дочерних элемента: Red и Blue. В этом случае нам не нужно дублировать код, как в шаблоне Factory.

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

В моем коде семейство продуктов — это тесто, сыр, начинки.

Пожалуйста, прочитайте следующий абзац, пока он не отложится в вашем мозгу:

Абстрактная фабрика позволяет нам создавать семейство продуктов, предоставляя интерфейс для дочерних фабрик (а не для субклиентов/суб-создателей). В результате наш код отделен от бетонного завода, который производит бетонный продукт, наполненный так называемыми ингредиентами. Таким образом, у нас может быть несколько фабрик, которые создают наши одинаковые продукты с различным содержанием. Говоря о развязке, ее можно проследить в слабой связи продуктов и другого кода, т.е. субклиентского/клиентского кода (который в нашей случай конкретные создатели/абстрактные) остается прежним. Таким образом, каждая фабрика создает собственное семейство продуктов.

Я серьезно, もう一度読んでください!👺

Окончательный код решения: часть вторая

А теперь давайте просмотрим код Abstract factory.



  1. GeneralFactory.kt наша любимая фабрика. Он определяет методы для реализации всех остальных фабрик.
  2. TokyoIngredientFactory.kt — бетонная фабрика, реализующая все методы. Напомним из написанного выше: эти методы сами по себе являются небольшими фабриками.
  3. AbstractPizzaStore.kt — это наш абстрактный создатель/клиент, который определяет абстрактный фабричный метод и один метод, который мы будем использовать для получения продукта (createPizza() — наш фабричный метод)
  4. TokyoPizzaStore.kt является субклиентом/создателем, у которого есть конкретный завод. Этот суб-клиент поместил завод внутрь конкретного изделия.
  5. Pepperoni.kt и другие пиццы являются побочными продуктами AbstractPizza . Вспомните Зависимость инверсии здесь.
  6. Cheese , Dough — это интерфейсы для конкретных ингредиентов.

Итак, как все работает?

В main.kt мы создаем экземпляр субклиента и используем метод из абстрактного клиента. Шаги☝️:

  1. Из субклиента мы вызываем метод родительского клиента (с типом пиццы) (в моем случае это orderPizza()), который запускает фабричный метод, реализованный в этом субклиенте.
  2. Фабричный метод createPizza() определяет конкретную пиццу (например, Pepperoni) с фабрикой внутри (в моем случае — TokyoIngredientFactory)
  3. Эта фабрика реализует абстрактную фабрику.
  4. Затем этот метод в родительском с самого начала orderPizza() использует абстрактный метод Pizza prepare(), который реализован в конкретной пицце. В этом методе используется фабрика, помещенная внутрь бетонной пиццы.
  5. А затем в родительском методе orderPizza() происходит дальнейшая обработка пиццы. Наконец, мы получаем нашу пиццу.
  6. Итак, как было сказано в начале, нам не нужно n количество классов для одинаковых пицц, если они одинаковы по внутреннему содержанию. Просто поместите factory внутри, и это отделит код.

Тьфу ... Я знаю, много информации и трудно отвлечься от всего.

Не стесняйтесь читать еще раз и задавать вопросы, если вы ничего не поняли

Дополнительный раздел

  • В коде шаблона Abstract Factory нашим Store является Client с createPizza() в качестве фабричного метода. Потом в Pepperoni/MeatPizza у нас по очереди маленькие фабрики.
  • Pizza не связан с региональными различиями и выглядит как схема, заполненная фабриками со своим набором ингредиентов. Например, Pepperoni может быть заполнен различными фабриками, которые используют разные ингредиенты. Пока эти фабрики придерживаются определенных interface ингредиентов, т. е. они создают вспомогательные ингредиенты (например, ThickCrust реализуют Dough и т. д.), все в порядке.
  • Посмотрите, каждый метод в abstract factory сам по себе является небольшой фабрикой. Почему? -› эти методы создают экземпляр объекта внутри себя. Кроме того, бетонные фабрики переопределяют абстрактные методы абстрактной фабрики. Это чем-то напоминает переопределение метода в Factory Pattern, но там мы переопределяем только один метод.
  • В исходном коде Abstract Factory вы можете найти папку: Modified Version . Он был изменен старшим разработчиком на моей работе. И, честно говоря, мне понравились его аугментации👐 Если хотите, чтобы я сделал прохождение кода — пишите в комментариях!

Рисование🎨

Простые шаблоны factory и Factory

Заводской шаблон

Шаблон "Абстрактная фабрика"

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

Аутро👣

В заключительном фрагменте давайте рассмотрим академическое определение шаблонов Factory и Abstract Factory.

Factory Pattern: определяет интерфейс для создания объекта, но заставляет подклассы реализовать его. Этот шаблон позволяет основному классу откладывать создание экземпляра объекта до подклассов.

Шаблон абстрактной фабрики: определяет интерфейс для создания семейства связанных продуктов без указания конкретных классов продуктов.

Эта модель определенно была длинной. Читайте внимательно столько раз, сколько вам нужно. И не стесняйтесь задавать вопросы!👍 Надеюсь, вы поняли разницу между двумя паттернами, а если нет — вы знаете, что делать и где спросить😇

Ты можешь меня найти: