Към съдържанието
wannacode

Защо шаблони

Накратко: Шаблонът за дизайн е назовано, проверено решение на често срещан проблем в кода; класическите 23 шаблона са в три семейства: създаващи, структурни и поведенчески.

Проблем

Два абзаца за една дума

Преглед на кода в онлайн магазин. Петър чете касата и оставя коментар на маркирания ред:

interface PaymentRule {
double total(double amount);
}
class Checkout {
private final Map<String, PaymentRule> rules = Map.of(
"карта", amount -> amount,
"наложен платеж", amount -> amount + 2.50,
"ваучер", amount -> Math.max(amount - 10, 0));
public double pay(String method, double amount) {
return rules.get(method).total(amount);
}
}
Checkout checkout = new Checkout();
System.out.printf("%.2f €%n", checkout.pay("наложен платеж", 80));
System.out.printf("%.2f €%n", checkout.pay("ваучер", 80));
Изход
82.50 €
70.00 €

Петър: Не разбирам какво е това. Касата държи таблица, в която всеки начин на плащане е отделно правило. Всички правила имат една и съща форма: взимат сума и връщат сума. По време на работа касата взима правилото по името, което е избрал клиентът, и го вика, без да знае кое е. Нарочно ли е така? Ако утре дойде разсрочено плащане, трябва ли да пипам pay?

Мария: Нарочно е. Не пипаш pay, добавяш ред в таблицата. Правилото само смята и нищо не помни, затова касата може да го пита по няколко пъти: за прегледа, за фактурата, за плащането. Иначе всяко питане би дало друга сума…

Двама души, два абзаца, а най-важното още не е казано: какво става, ако името го няма в таблицата?

Мария можеше да напише една дума: Strategy. Петър щеше да знае какво гледа и какво да провери. Правилото само смята. Новият начин е нов ред и касата не се пипа. Непознатото име се хваща, преди да стигне до таблицата. Тези въпроси вече са решени от много хора и са записани под това име.

Идеята

Шаблонът е име на решение

Шаблон за дизайн е проверено решение на задача, която се повтаря в много проекти, с име, по което всички го познават. Той не е библиотека и не е код за копиране: всеки проект го пише по своему. Шаблонът носи четири неща:

  • име: една дума вместо два абзаца;
  • задачата, която решава: например „правилото се сменя, а касата не трябва да се пипа“;
  • ролите: кой какво държи и кой кого вика;
  • цената: какво плащаш за него (още класове, още един скок при четене) и кога не си струва.

Повечето имена идват от книгата „Design Patterns“ (1994) на четирима автори, които програмистите наричат „бандата на четиримата“ (Gang of Four). Тя описва 23 шаблона в три семейства. Модулът минава през най-ползваните:

Repository и слоевете не е от книгата. Той е архитектурен шаблон: къде в програмата живее базата данни.

Модулът завършва с упражнение, Магазинът: кой шаблон къде: разхвърлян магазин, който сам подреждаш.

Виж го

Капанът: шаблон заради шаблона

Задачата е една: 10% отстъпка за поръчки над 100 €. Някой току-що е чел за шаблоните:

interface DiscountStrategy {
double apply(double amount);
}
class PercentOverThresholdDiscountStrategy implements DiscountStrategy {
private final double percent;
private final double threshold;
public PercentOverThresholdDiscountStrategy(double percent, double threshold) {
this.percent = percent;
this.threshold = threshold;
}
@Override
public double apply(double amount) {
return amount > threshold ? amount * (1 - percent / 100) : amount;
}
}
abstract class AbstractDiscountStrategyFactory {
public abstract DiscountStrategy createStrategy();
}
class DefaultDiscountStrategyFactory extends AbstractDiscountStrategyFactory {
@Override
public DiscountStrategy createStrategy() {
return new PercentOverThresholdDiscountStrategy(10, 100);
}
}
class DiscountService {
private final AbstractDiscountStrategyFactory factory;
public DiscountService(AbstractDiscountStrategyFactory factory) {
this.factory = factory;
}
public double discounted(double amount) {
return factory.createStrategy().apply(amount);
}
}
DiscountService service = new DiscountService(new DefaultDiscountStrategyFactory());
System.out.printf("Сума: %.2f €%n", service.discounted(120));
System.out.printf("Сума: %.2f €%n", service.discounted(80));
Изход
Сума: 108.00 €
Сума: 80.00 €

Пет класа, два шаблона (Strategy и фабрика) и име AbstractDiscountStrategyFactory. Същата задача, без шаблон:

static double discounted(double amount) {
return amount > 100 ? amount * 0.9 : amount;
}
System.out.printf("Сума: %.2f €%n", discounted(120));
System.out.printf("Сума: %.2f €%n", discounted(80));
Изход
Сума: 108.00 €
Сума: 80.00 €

Изходът е същият. Разликата е в четенето: за да разбереш колко е отстъпката, в първия вариант минаваш през пет класа, а във втория четеш един ред.

Шаблоните в първия вариант не решават нито един проблем. Отстъпката е една, никой не я сменя по време на работа и никой не избира между отстъпки. Когато дойде втората отстъпка и клиентът трябва да избира, тогава функцията става стратегия. Не по-рано.

Пробвай

Кое семейство и трябва ли шаблон

1. Builder, Singleton и Factory Method са от едно семейство. Кое?
2. Библиотеката на куриера иска sendParcel(city, grams), а твоят код вика ship(city, kilograms). От кое семейство е решението?
3. Етикетът показва цената „с ДДС“ или „без ДДС“. Решава се на едно място, а двата варианта не са се променяли от години. Трябва ли шаблон?
4. Клиентът избира между пет начина на плащане, а нов начин идва всеки месец. Трябва ли шаблон?

Идеята

Кога шаблонът вреди

Шаблонът е отговор на проблем. Без проблема остава само цената:

  • „Може да ни потрябва.“ Интерфейс с една имплементация, фабрика за един клас, стратегия с един вариант. Всеки, който чете кода, плаща за това, а никой не го ползва.
  • Името казва шаблона, а не работата. AbstractDiscountStrategyFactory казва как е направено, discounted казва какво прави. Ако не можеш да опишеш класа без думите на шаблона, вероятно не ти трябва.
  • Шаблон от друг език. Където стига ламбда, интерфейс и клас за всяко правило са излишни.
  • Шаблонът преди болката. Добрият ред е обратният. Пишеш простото. Когато същият switch се появи на трето място или правилата станат пет, правиш refactor към шаблона. Тогава вече знаеш точно какво се сменя.

Учиш шаблоните, за да ги разпознаваш и назоваваш в чужд код и в прегледа. Не за да ги слагаш навсякъде.

Упражнение

Познай шаблона

Реши задачата в редактора си.

Три фрагмента от един магазин. Пусни ги и за всеки кажи: има ли шаблон, кой е (Strategy, Observer, Factory Method, Singleton или Adapter от урока „Шаблони за дизайн“ в модула „Модерна Java“) и с едно изречение: какъв проблем решава. Един от трите няма нужда от шаблон.

А.

class Order {
private final List<Consumer<String>> listeners = new ArrayList<>();
public void onShipped(Consumer<String> listener) {
listeners.add(listener);
}
public void ship(String id) {
for (Consumer<String> listener : listeners) {
listener.accept(id);
}
}
}
Order order = new Order();
order.onShipped(id -> System.out.println("Имейл: поръчка " + id + " е изпратена"));
order.onShipped(id -> System.out.println("Склад: махни " + id + " от опашката"));
order.ship("1042");
Изход
Имейл: поръчка 1042 е изпратена
Склад: махни 1042 от опашката

Б.

class CourierApi {
public String sendParcel(String city, int grams) {
return "Пратка за " + city + ", " + grams + " г";
}
}
interface Shipping {
String ship(String city, double kilograms);
}
class CourierShipping implements Shipping {
private final CourierApi api = new CourierApi();
@Override
public String ship(String city, double kilograms) {
return api.sendParcel(city, (int) Math.round(kilograms * 1000));
}
}
Shipping shipping = new CourierShipping();
System.out.println(shipping.ship("Пловдив", 1.25));
Изход
Пратка за Пловдив, 1250 г

В.

static String priceLabel(double price, boolean withVat) {
if (withVat) {
return String.format("%.2f € с ДДС", price * 1.20);
}
return String.format("%.2f € без ДДС", price);
}
System.out.println(priceLabel(25, true));
System.out.println(priceLabel(25, false));
Изход
30.00 € с ДДС
25.00 € без ДДС
Отговорите
  • А е Observer. Поръчката не знае кой я слуша. Имейлът и складът се абонират, а ship ги вика по реда на абонирането. Нов слушател не пипа Order.
  • Б е Adapter. CourierShipping превежда твоето ship (килограми) към чуждото sendParcel (грамове), без да пипа нито едното.
  • В няма нужда от шаблон. Два варианта, на едно място, никой не ги сменя. Strategy тук би бил интерфейс и два класа вместо един if.

Още задачи

  • Кога В иска шаблон. Магазинът започва да продава в пет държави с различен ДДС, а клиентът избира държавата. Какво се променя? Напиши кой шаблон би помогнал и защо, без код.
  • Шаблонът, който изчезва. Вземи петте класа от картата „Капанът“ и ги замени с една функция. Преброй редовете преди и след. Изходът трябва да остане същият.
  • Твоят код. Намери в свой проект switch или верига от if, която се повтаря на две места. С коя дума би я назовал в прегледа?

Тест

Провери се

1. Какво е шаблон за дизайн?
2. Защо името на шаблона помага в прегледа на кода?
3. Кое е знак, че шаблонът е излишен?
4. Кой е добрият ред?
5. Adapter, Decorator и Composite са:

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·