Узнайте, как сделать ваш код более чистым и модульным с помощью шаблона Decorator

Предпосылки:

  1. Если вы не знакомы с термином шаблоны проектирования, обязательно ознакомьтесь с моим Введение в шаблоны проектирования.
  2. Вы понимаете Java или любой другой язык ООП.
  3. У вас есть базовое представление о наследовании, полиморфизме и интерфейсах.

Чтобы увидеть полный код, проверьте мой репозиторий 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.

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

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

  1. Компонент: это абстрактный класс; как конкретные компоненты, так и абстрактный класс Decorator унаследуют его поведение.
  2. Конкретный компонент: расширяет компонент. Это объект, к которому мы собираемся динамически добавлять новое поведение. Его можно использовать отдельно или в оболочке с помощью декоратора. (Например, мы можем заказать DarkRoast кофе или DarkRoast за Mocha.)
  3. Декоратор: он также расширяет компонент и является абстрактным классом.
  4. Конкретный декоратор. Каждый декоратор имеет компонент, что означает, что у него есть переменная экземпляра, которая содержит ссылку на компонент (пунктирная линия представляет это). Он расширяет абстрактный класс 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). Итак, мы можем сказать, что:

  1. DarkRoast is a Beverage.
  2. DarkRoast с Mocha тоже Beverage.
  3. 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 или другой кофе, а затем украсьте его декораторами.

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

использованная литература