Магазинът: кой шаблон къде
Накратко: Упражнение по шаблони за дизайн: магазин с плащане, известия и съхранение в един метод се подрежда със Strategy, Observer и Repository, а изходът остава същият.
Проблем
Магазин в един метод
Ето малък онлайн магазин. Методът за поръчка смята сумата по начина на плащане, пази поръчката, праща имейл и пише на склада:
class Shop { orders = [];
placeOrder(id, customer, amount, method) { let total; if (method === "карта") { total = amount; } else if (method === "наложен платеж") { total = amount + 2.5; } else { throw new Error("Непознат начин на плащане: " + method); } this.orders.push(`${id};${customer};${total}`); console.log(`Имейл до ${customer}: поръчка ${id}, ${total.toFixed(2)} €`); console.log("Склад: подготви " + id); }
count() { return this.orders.length; }}const shop = new Shop();shop.placeOrder("A-1", "ana@example.com", 80, "карта");shop.placeOrder("A-2", "ivan@example.com", 45, "наложен платеж");console.log("Поръчки: " + shop.count());Имейл до ana@example.com: поръчка A-1, 80.00 €Склад: подготви A-1Имейл до ivan@example.com: поръчка A-2, 47.50 €Склад: подготви A-2Поръчки: 2Работи. Но през следващия месец идват три промени: банков превод с 2% отстъпка, SMS след всяка поръчка и база вместо списък. И трите са в един и същ метод. Всяка от тях го пипа, а тест за сумата без да тръгне имейл не може да се напише.
В този урок няма нов шаблон. Той е упражнение: ти разпознаваш кой от шаблоните в модула подрежда всяко от тези неща и кой тук не трябва.
Идеята
Симптомът казва шаблона
Шаблон не се избира от списък. Избира се от това, което боли:
| Симптом в кода | Шаблон |
|---|---|
верига от if по вид, която расте с всеки нов вариант |
Strategy, или Factory, ако веригата избира какво да създаде |
| след едно събитие кодът сам вика всеки, който трябва да разбере | Observer |
| съхранението е вътре в правилата и тест без база не става | Repository и подаване отвън |
switch по текста на състоянието във всеки метод |
State |
| конструктор с много незадължителни параметри | Builder |
| чужда библиотека с неудобен интерфейс | Adapter |
| пет подсистеми, които трябва да се викат в точен ред | Facade |
| поведение „около“ метод: логване, кеш, добавки към цената | Decorator |
| отмяна на последното действие | Command |
| дърво, в което част и цяло се ползват еднакво | Composite |
Ако не намираш симптома, най-вероятно шаблон не ти трябва.
Пробвай
Разпознай
Погледни отново магазина от първата карта.
Виж го
Едно възможно подреждане
Три от симптомите са в магазина, затова и шаблоните са три.
Начините на плащане са стратегии: функции вMap. Поръчката е обикновен обект с три полета, който магазинът прави. Съхранението е клас с два метода, save и count:class InMemoryOrders { #orders = [];
save(order) { this.#orders.push(order); }
count() { return this.#orders.length; }}class Shop { #listeners = [];
constructor(orders, methods) { this.orders = orders; this.methods = methods; }
subscribe(listener) { this.#listeners.push(listener); }
placeOrder(id, customer, amount, method) { const payment = this.methods.get(method); if (payment === undefined) { throw new Error("Непознат начин на плащане: " + method); } const order = { id, customer, total: payment(amount) }; this.orders.save(order); for (const listener of this.#listeners) { listener(order); } }
count() { return this.orders.count(); }}Какво хранилище, кои начини и кои абонати, се решава на едно място, там, където магазинът се сглобява:
const methods = new Map([ ["карта", (amount) => amount], ["наложен платеж", (amount) => amount + 2.5],]);const shop = new Shop(new InMemoryOrders(), methods);shop.subscribe((o) => console.log(`Имейл до ${o.customer}: поръчка ${o.id}, ${o.total.toFixed(2)} €`));shop.subscribe((o) => console.log("Склад: подготви " + o.id));
shop.placeOrder("A-1", "ana@example.com", 80, "карта");shop.placeOrder("A-2", "ivan@example.com", 45, "наложен платеж");console.log("Поръчки: " + shop.count());Имейл до ana@example.com: поръчка A-1, 80.00 €Склад: подготви A-1Имейл до ivan@example.com: поръчка A-2, 47.50 €Склад: подготви A-2Поръчки: 2Изходът е същият, ред по ред. Подреждането не е сменило поведението, а това е първото, което проверяваш. Тук и двете програми се пускат при всяка проверка на сайта и дават един и същ изход.
Виж го
Трите промени сега
Банковият превод е нов ред в таблицата. SMS е нов абонат. Базата ще е нова имплементация на хранилището (методи save и count). В класа на магазина не се пипа нито ред:
const methods = new Map([ ["карта", (amount) => amount], ["наложен платеж", (amount) => amount + 2.5], ["банков превод", (amount) => amount * 0.98],]);const shop = new Shop(new InMemoryOrders(), methods);shop.subscribe((o) => console.log("SMS до " + o.customer + ": " + o.id));
shop.placeOrder("A-3", "ana@example.com", 100, "банков превод");SMS до ana@example.com: A-3Сега и тест за сумата е лесен: подаваш хранилище в паметта и не абонираш имейла.
Идеята
Къде да спреш
В модула има повече от десет шаблона, а тук са ползвани три. Останалите не са забравени, просто тук не помагат:
- Builder: поръчката има три задължителни полета. Конструкторът (record) стига;
- Singleton: нищо не трябва да е единствено. Хранилището се подава отвън, затова тестът и магазинът могат да имат свое;
- Factory: начините на плащане са функции в таблица, а таблицата вече е мястото, където се избира;
- State: поръчката още няма състояния. Когато дойдат „платена“ и „изпратена“, ще дойде и той (виж задачите).
Спираш, когато промяната, която идва, стане лесна. Повече шаблони не значи по-добър код.
Упражнение
Подреди магазина нататък
Реши задачите в редактора си, като тръгнеш от подредения магазин. След всяка стъпка пусни програмата от картата „Едно възможно подреждане“: изходът ѝ трябва да остане същият.
1. Състоянията на поръчката. Поръчката става нова → платена → изпратена и може да е отказана, но не и след като е изпратена. Направи го с State (или с изброим тип и таблица на преходите, ако състоянията нямат свое поведение) и покажи грешката при отказ на изпратена поръчка.
2. Отстъпка над 100 €. Поръчка над 100 € получава 5% отстъпка, каквото и да е плащането. Помисли къде е мястото ѝ: в стратегия, в Decorator около стратегията или в магазина. Защо?
3. Хранилище, което помни. Направи второ хранилище, което записва поръчките във файл (по една на ред), и го подай на магазина вместо това в паметта. Класът на магазина не се пипа.
4. Чужд куриер. Складът вече се вика през библиотека на куриер с метод createShipment(reference, weightGrams). Абонатът за склада става Adapter към нея.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Decorator · Command · State · Repository
- Задачи по темата: Шаблони за дизайн