Узнайте, как сделать ваш код более чистым и модульным с помощью шаблона Decorator
Предпосылки:
- Если вы не знакомы с термином шаблоны проектирования, обязательно ознакомьтесь с моим Введение в шаблоны проектирования.
- Вы понимаете Java или любой другой язык ООП.
- У вас есть базовое представление о наследовании, полиморфизме и интерфейсах.
Чтобы увидеть полный код, проверьте мой репозиторий GitHub:
Предположим, у вас есть кофейня, и это самый быстрорастущий магазин в мире. Просто посмотрите на другую сторону улицы, и вы увидите, что это другой филиал. Вы подаете четыре типа напитков: домашнюю смесь, темную обжарку, кофе без кофеина и эспрессо.
Когда вы только начинали свой бизнес, вы создавали свои классы следующим образом:
Beverage
- это абстрактный класс, и cost
метод в нем также является абстрактным. Подклассы будут реализовывать это. Beverage
имеет четыре подкласса: HouseBlend
, DarkRoast
, Decaf
и Espresso
. Все они отменяют cost
method и возвращают свой собственный cost
. Здесь мы использовали наследование.
Помимо кофе, вы также можете попросить приправы, такие как парное молоко, сою, мокко и взбитое молоко. Вы можете реализовать это так же, как и раньше, и использовать наследование. Таким образом, у нас будет огромное количество классов: HouseBlendMilk
, HouseBlendMocha
, HouseBlendSoy
, HouseBlendWhip
, HouseBlendMilkMocha
, HouseBlendMilkSoy
и т. Д. Список можно продолжить. Нам предстоит сделать около 100 классов.
Вот что происходит, когда мы переусердствуем с наследством. Это может легко привести к взрыву класса . Очевидно, что есть лучший способ реализовать это. Давай найдем.
Подход 1:
Чтобы увидеть код, относящийся к подходу 1, щелкните ниже:
или, если вы уже клонировали репозиторий, посмотрите simple approach
commit.
В этом подходе мы добавим несколько логических переменных в класс Beverage
в качестве флагов. Если это true
, значит, есть приправа, поэтому добавьте ее стоимость. На этот раз cost()
метод не является абстрактным.
В нашем конкретном Beverage
классе, таком как DarkRoast
, мы переопределим метод cost
и добавим cost
после расчета стоимости приправы, вызвав super.cost()
.
Допустим, для кофе темной обжарки мы добавим 20 рупий.
@Override public int cost(){ return super.cost() + 20; }
Вот как мы будем варить 25_ кофе с молоком. Его выходом будет стоимость домашнего кофе + стоимость пропаренного молока (приправы).
Beverage b = new HouseBlend(); b.setMilk(true); System.out.println("total cost = "+b.cost());
Наш код будет работать отлично, но с этим простым подходом есть несколько проблем:
- Если цена на приправу изменится или будет добавлена новая приправа, нам придется изменить существующий код и создать новые методы. Обратите внимание, что добавление новых вещей не должно изменять существующий код, так как это может привести к ошибкам.
- У нас могут быть новые напитки, например, чай со льдом. Для этого нет смысла добавлять молочную приправу, но мы унаследуем ее от класса Beverage.
- Что делать, если клиент хочет двойной мокко?
Подход 2:
Чтобы увидеть связанный с ним код, посмотрите этот коммит.
В этом подходе мы будем использовать шаблон декоратора. В этом шаблоне проектирования у нас есть компонент (например, DarkRoast
) и декораторы (например, Whip
, Mocha
и т. Д.). Оборачиваем этот компонент декораторами. Вот что я имею в виду под упаковкой:
Теперь, когда мы хотим вычислить стоимость, мы вызовем метод cost
самого внешнего декоратора, а затем он вызовет метод cost
своего внутреннего декоратора / компонента. Это будет продолжаться, пока не достигнет своего компонента (здесь DarkRoast
). Затем DarkRoast
вернет свою стоимость в Mocha
, Mocha
добавит к ней свою стоимость и вернет общую сумму в Whip
. Whip
добавит свою стоимость и вернет общую сумму. Это поток:
Стоимость DarkRoast
= 20, Mocha
= 50 и Whip
= 60.
Шаблон декоратора динамически прикрепляет к объекту дополнительные обязанности. Он предоставляет гибкую альтернативу созданию подклассов для расширения функциональности.
Теперь, когда у нас есть некоторые идеи о том, как работает этот шаблон, давайте посмотрим, как мы реализуем этот тип поведения.
- Компонент: это абстрактный класс; как конкретные компоненты, так и абстрактный класс Decorator унаследуют его поведение.
- Конкретный компонент: расширяет компонент. Это объект, к которому мы собираемся динамически добавлять новое поведение. Его можно использовать отдельно или в оболочке с помощью декоратора. (Например, мы можем заказать
DarkRoast
кофе илиDarkRoast
заMocha
.) - Декоратор: он также расширяет компонент и является абстрактным классом.
- Конкретный декоратор. Каждый декоратор имеет компонент, что означает, что у него есть переменная экземпляра, которая содержит ссылку на компонент (пунктирная линия представляет это). Он расширяет абстрактный класс Decorator, например
Milk
,Soy
и т. Д.
Теперь давайте посмотрим на наш абстрактный класс Component, в нашем случае Beverage.java
файл:
Метод cost
является абстрактным, потому что мы хотим, чтобы все наследующие его классы (конкретные компоненты и декораторы) переопределяли его и предоставляли свои собственные cost
.
Теперь давайте посмотрим на один из наших конкретных классов компонентов:
Каждый раз, когда мы определяем конкретный компонент, мы даем ему описание в конструкторе, переопределяем метод cost
и возвращаем его стоимость.
Пришло время взглянуть на основной класс, в котором творится волшебство. Давайте посмотрим на класс абстрактного декоратора; в нашем случае это CondimentDecorator.java
.
Обратите внимание, что он также расширяет Beverage
. Метод getDescription
является абстрактным, потому что мы хотим, чтобы все конкретные декораторы переопределили его и предоставили для него свою собственную реализацию. Поскольку мы не реализовали здесь метод cost
, каждый конкретный декоратор также должен его переопределить. Кроме того, у нас есть переменная экземпляра beverage
, потому что она нам понадобится в конкретном классе декоратора. Только благодаря этому мы сможем сформировать цепочку вызовов родительского метода cost
следующим образом:
Давайте посмотрим на один из конкретных классов декораторов, Mocha.java
:
В constructor
он получит Beverage
, установит для него переменную экземпляра и переопределит методы. Обратите внимание, что переменная beverage
унаследована от класса CondimentDecorator
.
Перед заказом кофе помните, что и декораторы (Whip
, Soy
), и компоненты (DarkRoast
, Decaf
) расширяют класс компонента (Beverage
). Итак, мы можем сказать, что:
DarkRoast
is aBeverage
.DarkRoast
сMocha
тожеBeverage
.DarkRoast
сMocha
иWhip
тожеBeverage
.
А теперь закажем кофе! 😃 В своем основном методе проверьте этот код:
Beverage b1 = new Espresso(); System.out.println(b1.getDescription() + " Rs." + b1.cost()); // ** OUTPUT ** // Espresso Rs.30
Заказать простой кофе легко и просто; давай добавим приправу.
Beverage b2 = new DarkRoast(); b2 = new Mocha(b2); System.out.println(b2.getDescription() + " Rs." + b2.cost()); // ** OUTPUT ** // Dark roast, Mocha Rs.70 // Rs 70 = Rs 20(of DarkRoast) + Rs 50(of Mocha)
b2 = new Mocha(b2)
. Это то, что мы подразумеваем под украшением объекта. Мы взяли объект DarkRoast
и украсили его классом Mocha
. Помните, что класс конкретного декоратора (здесь Mocha
) получает beverage
в своем constructor
? Здесь мы прошли b2
. Итак, вы можете сказать, что в Mocha
классе beverage = b2
, а в это время b2
был объектом классаDarkRoast
. Когда мы вычисляем стоимость, beverage.cost
вернет стоимость b2 (DarkRoast
= 20 рупий), а затем мы добавим к ней 50, так что результат составит 70 рупий.
Теперь добавим две приправы Mocha
и Whip
:
Beverage b2 = new DarkRoast(); b2 = new Mocha(b2); // wrap it with mocha b2 = new Whip(b2); // wrap it with whip System.out.println(b2.getDescription() + " Rs." + b2.cost()); // ** OUTPUT ** // Dark roast, Mocha, Whip Rs.130 // Rs 130 = Rs 20(of DarkRoast) + Rs 50(of Mocha) + Rs 60(of Whip)
Как я уже сказал вам, (_89 _ + _ 90_) тоже напиток. По этой причине мы можем передать b2
объект в Whip
конструктор. Опять же, поток такой же, как и раньше. Когда мы вызываем b2.cost()
, сначала вычисляется beverage.cost()
, а для Whip
напиток будет DarkRoast
+ Mocha
. Когда мы вызываем beverage.cost()
на этом DarkRoast
+ Mocha
, он будет вызывать его beverage.cost()
, а для (DarkRoast
+ Mocha
) системы напиток будет DarkRoast
. Здесь beverage.cost()
даст нам 20; мы добавляем к нему 50 (Mocha
стоимость) и возвращаем 70. Эти 70 получает Whip
, добавляет 60 и возвращает 130.
Beverage b2 = new DarkRoast(); b2 = new Mocha(b2); // wrap it with mocha b2 = new Whip(b2); // wrap it with whip
Я знаю, что приведенная выше часть кода немного сбивает с толку. Но вы научитесь украшать объекты намного лучше, когда узнаете о паттернах Factory и Builder. Некоторые из вас могут сказать, что никогда не видели такого кода. Но если вы когда-либо писали фрагмент кода на Java, чтобы принять вводимые пользователем данные, вы либо используете Scanner
, либо InputStreamReader
, либо какой-либо другой метод. Давайте посмотрим, как читать вводимые пользователем данные InputStreamReader
:
BufferedReader br =
new BufferedReader(new InputStreamReader(System.in));
String s = br.readLine();
Здесь мы оборачиваем InputStreamReader
в BufferedReader
, поэтому вы использовали шаблон декоратора с самого начала, даже не подозревая об этом!
Последний важный момент, который следует отметить, заключается в том, что, хотя Decorator
является подклассом класса Component
, мы не можем напрямую создавать объекты конкретного декоратора, не передавая конкретный компонент. Например, здесь мы не можем напрямую заказать только Mocha
, потому что для этого требуется Beverage
для передачи в конструктор. Mocha
- это декоратор, поэтому его нужно использовать для украшения Beverage
. Итак, сначала сделайте объект из конкретного компонента, такого как DarkRoast
, Decaf
или другой кофе, а затем украсьте его декораторами.
Большое спасибо за то, что оставались со мной так долго. Надеюсь, вы узнали о декораторах и компонентах.