Защо шаблони
Накратко: Шаблонът за дизайн е назовано, проверено решение на често срещан проблем в кода; класическите 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 шаблона в три семейства. Модулът минава през най-ползваните:
- Създаващи: как и къде се правят обекти. Factory Method и Simple Factory, Builder, Singleton и Dependency Injection.
- Структурни: как обекти се сглобяват в по-голямо цяло. Adapter и Facade, Decorator, Composite.
- Поведенчески: кой кого вика и кой какво решава. Strategy, Observer, Command, State, Template Method и Iterator.
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. Функцията вече е стойност, която можеш да подадеш и да пазиш.Пробвай
Кое семейство и трябва ли шаблон
Идеята
Кога шаблонът вреди
Шаблонът е отговор на проблем. Без проблема остава само цената:
- „Може да ни потрябва.“ Клас с един метод там, където стига функция, фабрика за един клас, таблица с едно правило. Всеки, който чете кода, плаща за това, а никой не го ползва.
- Името казва шаблона, а не работата.
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, която се повтаря на две места. С коя дума би я назовал в прегледа?
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Масиви: push, splice и sort · Map и Set · Стрелкови функции · Шаблони за дизайн
- Задачи по темата: Шаблони за дизайн