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

Strategy

Накратко: Strategy слага всяко правило (например начин на плащане) в отделна функция, за да добавяш нови правила, без да пипаш кода, който ги ползва.

Проблем

Касата, която знае всеки начин на плащане

Онлайн магазинът приема пет начина на плащане. Наложеният платеж струва 2,50 € повече, ваучерът вади 10 €, банковият превод дава 2% отстъпка, а разсроченото плащане е с 5% оскъпяване. Касата смята сумата така:

function total(method, amount) {
if (method === "карта") {
return amount;
} else if (method === "наложен платеж") {
return amount + 2.5;
} else if (method === "ваучер") {
return Math.max(amount - 10, 0);
} else if (method === "банков превод") {
return amount * 0.98;
} else if (method === "разсрочено") {
return amount * 1.05;
}
throw new Error("Непознат начин на плащане: " + method);
}
console.log(`Карта: ${total("карта", 80).toFixed(2)} €`);
console.log(`Наложен платеж: ${total("наложен платеж", 80).toFixed(2)} €`);
console.log(`Ваучер: ${total("ваучер", 80).toFixed(2)} €`);
console.log(`Банков превод: ${total("банков превод", 80).toFixed(2)} €`);
console.log(`Разсрочено: ${total("разсрочено", 80).toFixed(2)} €`);
Изход
Карта: 80.00 €
Наложен платеж: 82.50 €
Ваучер: 70.00 €
Банков превод: 78.40 €
Разсрочено: 84.00 €

Работи. Но всеки месец идва нов начин, и всеки е нов клон в тази функция. Същата верига има и във фактурата, и в имейла с потвърждението. Някой добавя Apple Pay на две от трите места и клиентът получава имейл с „Непознат начин на плащане“.

Ваучерите вече не са само за 10 €. За да подадеш стойността, функцията получава трети параметър, и се сменя всяко място, което я вика, дори за карта, където ваучер няма. Кодът, през който минава всяко плащане, се пипа при всяка промяна.

Идеята

Strategy: правилото отделно, касата пита

Каквото се сменя, е едно нещо: как се смята сумата. Каквото не се сменя, е касата: тя взима сумата, пита и закръгля.

Затова правилото отива в отделна функция. Всички начини имат една обща форма: приемат сума и връщат колко да се плати. Касата пази функцията в поле и я вика, без да знае коя точно е тя.

Това е шаблонът Strategy (стратегия). Има три роли:

  • касата държи стратегията и я вика. В книгите това се казва контекст;
  • общата форма: функция, която приема сума и връща сума;
  • конкретните стратегии: по една функция за всеки начин.

Нов начин е нова стратегия. Касата не се пипа.

Виж го

Касата и три начина

Три начина, всеки в своя функция. Ваучерът е функция, която връща функция: voucher(10) прави стратегия, която помни своите 10 €. Така стойността не е параметър на всеки начин:
function card(amount) {
return amount;
}
function cashOnDelivery(amount) {
return amount + 2.5;
}
function voucher(value) {
return (amount) => Math.max(amount - value, 0);
}
Касата пази функцията в поле method и я вика. Присвояване на checkout.method я сменя:
class Checkout {
constructor(method) {
this.method = method;
}
total(amount) {
return Math.round(this.method(amount) * 100) / 100;
}
}
const checkout = new Checkout(card);
console.log(`Карта: ${checkout.total(80).toFixed(2)} €`);
checkout.method = cashOnDelivery;
console.log(`Наложен платеж: ${checkout.total(80).toFixed(2)} €`);
checkout.method = voucher(10);
console.log(`Ваучер: ${checkout.total(80).toFixed(2)} €`);
Изход
Карта: 80.00 €
Наложен платеж: 82.50 €
Ваучер: 70.00 €

const checkout = new Checkout(card);
console.log(`Карта: ${checkout.total(80).toFixed(2)} €`);
checkout.method = cashOnDelivery;
console.log(`Наложен платеж: ${checkout.total(80).toFixed(2)} €`);
checkout.method = voucher(10);
console.log(`Ваучер: ${checkout.total(80).toFixed(2)} €`);
Стъпка по стъпка, с думи
  1. Касата получава карта: полето method сочи функцията card.
  2. total(80): касата не знае кой е начинът. Само пита стратегията колко е сумата и получава 80.00.
  3. Нов начин. checkout.method = … насочва полето към функцията cashOnDelivery. В Checkout не се пипа нито ред.
  4. Същото total(80), друг отговор: 82.50.
  5. voucher(10) връща функция, която помни value = 10. Старите функции вече никой не ги сочи.
  6. Ваучерът вади 10: 70.00. Три начина, а касата е същият обект със същия код през цялото време.

Касата е един обект с един код през цялата програма. Сменя се само това, към което сочи полето.

Виж го

Нов начин, без да пипаш касата

Банковият превод и разсроченото плащане идват като две нови функции:

function bankTransfer(amount) {
return amount * 0.98;
}
function installments(amount) {
return amount * 1.05;
}
const checkout = new Checkout(bankTransfer);
console.log(`Банков превод: ${checkout.total(80).toFixed(2)} €`);
checkout.method = installments;
console.log(`Разсрочено: ${checkout.total(80).toFixed(2)} €`);
Изход
Банков превод: 78.40 €
Разсрочено: 84.00 €

В касата не е сменен нито ред. Не може да счупиш картата, докато добавяш превод, защото кодът на картата не е пред теб.

Тук компилатор, който да провери формата, няма. Ако новата функция приема две стойности вместо една, грешката излиза чак когато касата я извика. Затова всяка нова стратегия се пуска поне веднъж, преди да стигне до клиент.

Виж го

Кой начин избира клиентът

Клиентът пише начина като текст. Веригата от if, която избираше правилото, става таблица: име → стратегия.

const methods = new Map([
["карта", card],
["наложен платеж", cashOnDelivery],
["ваучер", voucher(10)],
]);
const choice = await ask("");
rl.close();
const method = methods.get(choice);
if (method === undefined) {
console.log("Няма такъв начин на плащане: " + choice);
} else {
const checkout = new Checkout(method);
console.log(`${choice}: ${checkout.total(80).toFixed(2)} €`);
}
Вход
наложен платеж
Изход
наложен платеж: 82.50 €

Непознат начин не стига до касата:

Вход
PayPal
Изход
Няма такъв начин на плащане: PayPal

Изборът пак е на едно място, но вече е данни. Нов начин е нов ред в таблицата и нова функция.

По-точно, за любопитните

Защо функция, а не клас? В JavaScript функцията е стойност: пази се в поле и в таблица като всеки друг обект. Стратегия с един метод е точно функция. Клас (или обект с методи) си заслужава, когато стратегията има повече от едно действие: например сума и текст за фактурата. В Java и C# същото се пише с интерфейс и клас за всеки начин.

Пробвай

Предвиди изхода

Поръчката е за 8 €, а ваучерът е за 10 €. После касата минава на наложен платеж. Какво печата програмата?

const checkout = new Checkout(voucher(10));
console.log(`${checkout.total(8).toFixed(2)} €`);
checkout.method = cashOnDelivery;
console.log(`${checkout.total(8).toFixed(2)} €`);
Отговор

Пробвай

Намери грешката

Ваучерът трябва да се използва веднъж. Някой го „изхабява“ още в стратегията:

function voucher(value) {
return (amount) => {
const result = Math.max(amount - value, 0);
value = 0;
return result;
};
}
const checkout = new Checkout(voucher(10));
console.log(`Преглед на поръчката: ${checkout.total(80).toFixed(2)} €`);
console.log(`Плащане: ${checkout.total(80).toFixed(2)} €`);
Изход
Преглед на поръчката: 70.00 €
Плащане: 80.00 €

Прегледът показа 70 €, а плащането поиска 80 €. На кой ред е грешката?

Избери реда с грешката.

Идеята

Кога не ти трябва

Strategy има цена: отделна функция за всеки вариант и таблица, вместо един if. Не си струва, когато:

  • вариантите са два и не се сменят. „С ДДС или без“ е един if;
  • правилото се ползва на едно място и никой не го пипа;
  • има само една функция, а таблицата е „за всеки случай“.

Струва си, когато вариантите растат, сменят се по време на работа (клиентът избира) или различни части от програмата искат различни правила.

Упражнение

Apple Pay, без да пипаш касата

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

Apple Pay. Магазинът добавя Apple Pay с такса 1% от сумата. Добави функция applePay и плати с него поръчка за 80 €. Касата и другите начини не се пипат.

Изход
Apple Pay: 80.80 €

Още задачи

  • Безплатен наложен платеж над 100 €. Направи стратегия за наложен платеж, която не взима 2,50 € при поръчка над 100 €. Пусни я с 80 € и със 120 €.
  • Начинът от входа. Добави банковия превод и разсроченото плащане в таблицата от картата „Кой начин избира клиентът“. Прочети и сумата от входа.
  • Подаръчна карта с баланс. Картата има баланс 50 €: плаща колкото може, а останалото е с карта. Помисли къде се записва новият баланс. Стратегията ли го сменя, или поръчката след плащането?

Тест

Провери се

1. Магазинът добавя плащане с Apple Pay. Какво пишеш при Strategy?
2. Какво пази касата при Strategy?
3. Функцията за ваучер нулира стойността си при всяко извикване. Касата пита веднъж за прегледа и веднъж за плащането. Какво става?
4. Кога Strategy не ти трябва?

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