Рад снова тебя увидеть! Надеюсь, вы не пропустили два моих предыдущих поста о паттернах Strategy & Observer👻:
- Стратегия: https://medium.com/towardsdev/strategy-pattern-for-independent-algorithms-kotlin-70ed24c7bd8b
- Наблюдатель: 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/другом языке). Сначала посмотрим на это «модифицированное» решение:
Проблемы здесь? Перечислим их:
- Что, если
price
для ингредиента изменится? Нам нужно изменить существующий код => плохо! - Новые ингредиенты заставят нас снова переделывать код
- Все дети
Beverage
унаследуют эти чеки на ингредиенты, но что, если они нам не нужны?
и т.д…❗️
Принципы дизайна
Все мы слышали о SOLID (надеюсь😰). Что означает «О»?
- Принцип Open-Closed: наши объекты/классы должны быть открыты для расширения, но закрыты для модификации.
Да, да, это звучит как наоборот. Я так и подумал, когда впервые услышал об этом, но позвольте мне рассказать вам настоящее значение:
Наш класс не должен позволять изменять другие: например, некоторые методы или свойства должны оставаться неизменными, однако наш класс:
- может быть расширен без внесения изменений в исходный код. Как? Вспомним шаблон Observer (ссылка в начале), где мы расширяем Subject конкретным наблюдателем. Мы использовали методы из Subject и далее можем изменять результаты из этого класса без внесения изменений в сам класс Subject.
- Мы можем специально сделать некоторые методы открытыми, другие закрытыми/защищенными.
Вы можете спросить себя: как я могу разработать системы, которые будут следовать этому правилу🧐? Отвечать:
- с опытом
- вам не следует этого делать, так как новые уровни абстракции добавляют сложности =› сконцентрируйтесь на фрагментах, которые, вероятно, будут изменяться, и примените принцип там
- используйте уже созданные шаблоны, чтобы упростить разработку
Окончательный код решения
Перейдя по ссылке ниже, вы можете посмотреть мой код, который я объясню далее и сопоставлю с теорией шаблонов.
Во-первых, позвольте мне представить другое использование наследования🙌: наследование для сопоставления типов, а не простое наследование поведения. Как это работает?
- Дочерний элемент ЯВЛЯЕТСЯ родителем, но не использует методы прямо. На самом деле, это для сопоставления типов, т.е. для создания этого класса родителем. ЗАЧЕМ ИСПОЛЬЗОВАТЬ ТАКОЙ ТРЮК??
Позвольте мне нарисовать схему шаблона 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)
, а затем добавляются декораторы, как новые слои.
Информация об этих декораторах:
- Они того же супертипа, что и объект, который они украшают.
- Может быть применено несколько декораторов
- Мы можем передать уже декорированный объект (и собственно так и делаем)
- Мы можем украшать объекты во время выполнения.
- Декораторы могут добавлять свое поведение до/после декорирования объекта.
В моем примере мы не будем слишком углубляться в новые методы и тому подобное, чтобы уменьшить умственную нагрузку.
Мы приобретаем новое поведение, не наследуя от супертипа/класса, а комбинируя объекты вместе
Проанализируем код:
mainComponent.kt
это нашеabstract class
, из которого все рождаетсяconcreteComponent.kt
ребенок, который будет украшенmainDecorator.kt
этоabstract class
для декораторов. Он наследует тип, который обсуждался выше.concreteDecorator.kt
иconcretteDecorator2.kt
— примеры декораторов, которые будут применены кconcreteComponent
Как все работает?
- посмотрите на
main.kt
: мы инициализируем новый компонент и оборачиваем его в 2 декоратора. Когда мы вызываемolivesPizza.cost()
, срабатывает цепочка:
- мы вводим
concreteDecorator.kt cost()
, который, в свою очередь, вызывает егоcurrentPizza
. - Это
currentPizza
естьconcreteDecorator2.kt
: Сыр. Он называетсяcurrentPizza
, что означаетFreshPizza
, также известный как бетонный компонент. - Этот конкретный компонент возвращает 25. Затем эти 25 + 5 возвращаются из
Cheese
, также известного какconcreteDecorator2.kt
, вconcreteDecorator.kt
, где добавляется последнее 4🙌
То же самое относится и к методу currentDescription()
. Сможешь распутать? Если что-то неясно, дайте мне знать в комментариях!
Рисование✍🏻
Здесь вы можете наблюдать описанную выше схему во плоти. Слева находится общий план, а справа пример кода из моего репозитория GitHub.
Оставьте сообщение в комментарии, если вы хотите, чтобы я объяснил это
IS-A означает наследование. Но не забывайте о наследовании для сопоставления типов, а не о простом наследовании поведения☝🏼
Аутро😪
Академическое определение шаблона декоратора: динамически добавляет объекту дополнительные обязанности. Он обеспечивает возможность расширения функциональности без чрезмерного использования подклассов.
Я знаю, что этот шаблон немного более расплывчатый по сравнению с предыдущим, но надеюсь, что вы поняли суть👋
Ты можешь меня найти:
- LinkedIn: www.linkedin.com/in/sleeplesschallenger
- GitHub: https://github.com/SleeplessChallenger
- Литкод: https://leetcode.com/SleeplessChallenger/
- Телеграмма: @SleeplessChallenger