Рад снова тебя увидеть! Надеюсь, вы не пропустили два моих предыдущих поста о паттернах Strategy & Observer👻:

  1. Стратегия: https://medium.com/towardsdev/strategy-pattern-for-independent-algorithms-kotlin-70ed24c7bd8b
  2. Наблюдатель: https://towardsdev.com/observer-pattern-for-loose-coupling-kotlin-f5ab804609bb

Обязательно прочтите их, чтобы лучше понять, как раскрывается эта серия🤟 Как и прежде, за супер-пупер подробным объяснением покупайте книгу: https://www.oreilly.com/library/view/head-first-design/ 9781492077992/

Состав:

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

Давайте скорее погрузимся в суть!

Шаблон декоратора

вступление

Когда разрабатываешь что-то на скорую руку, неудивительно, что такая работа принесет много скрытых проблем👎🏻 Самая коварная из них — это плохой дизайн, из-за которого довольно сложно решить всю эту неразбериху (пример вы увидите в раздел Проблема).

Решение? Ах…😪 Нет заклинания, которое волшебным образом мгновенно распутывает беспорядок. Однако🖐 Знание шаблонов проектирования и принципов ООП сэкономит вам время и нервы в будущем, поскольку вы примерно сравните реализацию своей бизнес-задачи с этими двумя упомянутыми вещами и решите: как сделать мой софт не просто рабочим, а расширяемым и управляемым в будущем🛠 Да, звучит расплывчато, но с опытом такие моменты заметишь быстрее.

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

Проблема

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

  • базовый класс Beverage
  • дети являются типами этого Beverage

Итак, в какой-то момент вы можете подумать, что это даже нормально. А если у нас кафе с десятками, сотнями Beverage с? А главное отличие в ингредиентах❓

  • Поэтому нам нужно добавить новые подобные классы
  • Что если изменится реализация cost() или появится новый метод?

Помните, я говорил о плохом дизайне в разделе Введение?

Тогда вы можете подумать: поместить ingredients переменных внутри родительского класса и разбросать getters и setters (версия Java || некоторые методы на Kotlin/другом языке). Сначала посмотрим на это «модифицированное» решение:

Проблемы здесь? Перечислим их:

  1. Что, если price для ингредиента изменится? Нам нужно изменить существующий код => плохо!
  2. Новые ингредиенты заставят нас снова переделывать код
  3. Все дети Beverage унаследуют эти чеки на ингредиенты, но что, если они нам не нужны?

и т.д…❗️

Принципы дизайна

Все мы слышали о SOLID (надеюсь😰). Что означает «О»?

  1. Принцип Open-Closed: наши объекты/классы должны быть открыты для расширения, но закрыты для модификации.

Да, да, это звучит как наоборот. Я так и подумал, когда впервые услышал об этом, но позвольте мне рассказать вам настоящее значение:

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

  1. может быть расширен без внесения изменений в исходный код. Как? Вспомним шаблон Observer (ссылка в начале), где мы расширяем Subject конкретным наблюдателем. Мы использовали методы из Subject и далее можем изменять результаты из этого класса без внесения изменений в сам класс Subject.
  2. Мы можем специально сделать некоторые методы открытыми, другие закрытыми/защищенными.

Вы можете спросить себя: как я могу разработать системы, которые будут следовать этому правилу🧐? Отвечать:

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

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

Перейдя по ссылке ниже, вы можете посмотреть мой код, который я объясню далее и сопоставлю с теорией шаблонов.



Во-первых, позвольте мне представить другое использование наследования🙌: наследование для сопоставления типов, а не простое наследование поведения. Как это работает?

  • Дочерний элемент ЯВЛЯЕТСЯ родителем, но не использует методы прямо. На самом деле, это для сопоставления типов, т.е. для создания этого класса родителем. ЗАЧЕМ ИСПОЛЬЗОВАТЬ ТАКОЙ ТРЮК??

Позвольте мне нарисовать схему шаблона Decorator:

                     Main Component
                /                     \
           Concrete Component     Main Decorator
                                    /      \
                     Concrete Decorator 1  Concrete Decorator 2

Здесь Main Decorator должен быть того же типа, что и бетонный компонент, так как первый будет украшать второй. Следовательно, мы используем наследование для сопоставления типов.

Как это выглядит в более похожем на картинку варианте?

                 DarkCoffee
                   decorated by Mocha
                       decorated by Whip
                         etc

Это как снежный ком. В центре у нас есть наш Concrete Component (потомок основного класса, т.е. Beverage) , а затем добавляются декораторы, как новые слои.

Информация об этих декораторах:

  1. Они того же супертипа, что и объект, который они украшают.
  2. Может быть применено несколько декораторов
  3. Мы можем передать уже декорированный объект (и собственно так и делаем)
  4. Мы можем украшать объекты во время выполнения.
  5. Декораторы могут добавлять свое поведение до/после декорирования объекта.

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

Мы приобретаем новое поведение, не наследуя от супертипа/класса, а комбинируя объекты вместе

Проанализируем код:

  1. mainComponent.kt это наше abstract class, из которого все рождается
  2. concreteComponent.kt ребенок, который будет украшен
  3. mainDecorator.kt это abstract class для декораторов. Он наследует тип, который обсуждался выше.
  4. concreteDecorator.kt и concretteDecorator2.kt — примеры декораторов, которые будут применены к concreteComponent

Как все работает?

  • посмотрите на main.kt: мы инициализируем новый компонент и оборачиваем его в 2 декоратора. Когда мы вызываем olivesPizza.cost(), срабатывает цепочка:
  1. мы вводим concreteDecorator.kt cost(), который, в свою очередь, вызывает его currentPizza.
  2. Это currentPizza есть concreteDecorator2.kt : Сыр. Он называется currentPizza, что означает FreshPizza, также известный как бетонный компонент.
  3. Этот конкретный компонент возвращает 25. Затем эти 25 + 5 возвращаются из Cheese, также известного как concreteDecorator2.kt, в concreteDecorator.kt, где добавляется последнее 4🙌

То же самое относится и к методу currentDescription(). Сможешь распутать? Если что-то неясно, дайте мне знать в комментариях!

Рисование✍🏻

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

Оставьте сообщение в комментарии, если вы хотите, чтобы я объяснил это

IS-A означает наследование. Но не забывайте о наследовании для сопоставления типов, а не о простом наследовании поведения☝🏼

Аутро😪

Академическое определение шаблона декоратора: динамически добавляет объекту дополнительные обязанности. Он обеспечивает возможность расширения функциональности без чрезмерного использования подклассов.

Я знаю, что этот шаблон немного более расплывчатый по сравнению с предыдущим, но надеюсь, что вы поняли суть👋

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