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

Защо шаблони

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

Проблем

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

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

class Checkout {
rules = new Map([
["карта", (amount) => amount],
["наложен платеж", (amount) => amount + 2.5],
["ваучер", (amount) => Math.max(amount - 10, 0)],
]);
pay(method, amount) {
return this.rules.get(method)(amount);
}
}
const checkout = new Checkout();
console.log(`${checkout.pay("наложен платеж", 80).toFixed(2)} €`);
console.log(`${checkout.pay("ваучер", 80).toFixed(2)} €`);
Изход
82.50 €
70.00 €

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

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

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

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

Идеята

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

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

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

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

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

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

В JavaScript някои шаблони почти изчезват, защото езикът вече ги има. Функцията е стойност, затова Strategy и Command често са просто функция. Обхождането (Iterator) е вградено в for. Всеки урок от модула казва къде е така, вместо да пише Java с друг синтаксис.

Виж го

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

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

class DiscountStrategy {
apply(amount) {
throw new Error("apply is not implemented");
}
}
class PercentOverThresholdDiscountStrategy extends DiscountStrategy {
constructor(percent, threshold) {
super();
this.percent = percent;
this.threshold = threshold;
}
apply(amount) {
return amount > this.threshold ? amount * (1 - this.percent / 100) : amount;
}
}
class AbstractDiscountStrategyFactory {
createStrategy() {
throw new Error("createStrategy is not implemented");
}
}
class DefaultDiscountStrategyFactory extends AbstractDiscountStrategyFactory {
createStrategy() {
return new PercentOverThresholdDiscountStrategy(10, 100);
}
}
class DiscountService {
constructor(factory) {
this.factory = factory;
}
discounted(amount) {
return this.factory.createStrategy().apply(amount);
}
}
const service = new DiscountService(new DefaultDiscountStrategyFactory());
console.log(`Сума: ${service.discounted(120).toFixed(2)} €`);
console.log(`Сума: ${service.discounted(80).toFixed(2)} €`);
Изход
Сума: 108.00 €
Сума: 80.00 €

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

function discounted(amount) {
return amount > 100 ? amount * 0.9 : amount;
}
console.log(`Сума: ${discounted(120).toFixed(2)} €`);
console.log(`Сума: ${discounted(80).toFixed(2)} €`);
Изход
Сума: 108.00 €
Сума: 80.00 €

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

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

В JavaScript това е още по-ясно: клас, който хвърля „not implemented“, за да прилича на абстрактен, е Java, написана на JavaScript. Функцията вече е стойност, която можеш да подадеш и да пазиш.

Пробвай

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

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

Идеята

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

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

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

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

Упражнение

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

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

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

А.

class Order {
listeners = [];
onShipped(listener) {
this.listeners.push(listener);
}
ship(id) {
for (const listener of this.listeners) {
listener(id);
}
}
}
const order = new Order();
order.onShipped((id) => console.log(`Имейл: поръчка ${id} е изпратена`));
order.onShipped((id) => console.log(`Склад: махни ${id} от опашката`));
order.ship("1042");
Изход
Имейл: поръчка 1042 е изпратена
Склад: махни 1042 от опашката

Б.

class CourierApi {
sendParcel(city, grams) {
return `Пратка за ${city}, ${grams} г`;
}
}
class CourierShipping {
api = new CourierApi();
ship(city, kilograms) {
return this.api.sendParcel(city, Math.round(kilograms * 1000));
}
}
const shipping = new CourierShipping();
console.log(shipping.ship("Пловдив", 1.25));
Изход
Пратка за Пловдив, 1250 г

В.

function priceLabel(price, withVat) {
if (withVat) {
return `${(price * 1.2).toFixed(2)} € с ДДС`;
}
return `${price.toFixed(2)} € без ДДС`;
}
console.log(priceLabel(25, true));
console.log(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 за достъпно обучение по програмиране ·