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

Защо шаблони

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

Проблем

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

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

class Checkout
{
private readonly Dictionary<string, Func<decimal, decimal>> rules = new()
{
["карта"] = amount => amount,
["наложен платеж"] = amount => amount + 2.50m,
["ваучер"] = amount => Math.Max(amount - 10, 0),
};
public decimal Pay(string method, decimal amount)
{
return rules[method](amount);
}
}
var checkout = new Checkout();
Console.WriteLine($"{checkout.Pay("наложен платеж", 80):F2} €");
Console.WriteLine($"{checkout.Pay("ваучер", 80):F2} €");
Изход
82.50 €
70.00 €

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

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

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

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

Идеята

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

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

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

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

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

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

Виж го

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

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

interface IDiscountStrategy
{
decimal Apply(decimal amount);
}
class PercentOverThresholdDiscountStrategy : IDiscountStrategy
{
private readonly decimal percent;
private readonly decimal threshold;
public PercentOverThresholdDiscountStrategy(decimal percent, decimal threshold)
{
this.percent = percent;
this.threshold = threshold;
}
public decimal Apply(decimal amount)
{
return amount > threshold ? amount * (1 - percent / 100) : amount;
}
}
abstract class AbstractDiscountStrategyFactory
{
public abstract IDiscountStrategy CreateStrategy();
}
class DefaultDiscountStrategyFactory : AbstractDiscountStrategyFactory
{
public override IDiscountStrategy CreateStrategy()
{
return new PercentOverThresholdDiscountStrategy(10, 100);
}
}
class DiscountService
{
private readonly AbstractDiscountStrategyFactory factory;
public DiscountService(AbstractDiscountStrategyFactory factory)
{
this.factory = factory;
}
public decimal Discounted(decimal amount)
{
return factory.CreateStrategy().Apply(amount);
}
}
var service = new DiscountService(new DefaultDiscountStrategyFactory());
Console.WriteLine($"Сума: {service.Discounted(120):F2} €");
Console.WriteLine($"Сума: {service.Discounted(80):F2} €");
Изход
Сума: 108.00 €
Сума: 80.00 €

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

static decimal Discounted(decimal amount)
{
return amount > 100 ? amount * 0.9m : amount;
}
Console.WriteLine($"Сума: {Discounted(120):F2} €");
Console.WriteLine($"Сума: {Discounted(80):F2} €");
Изход
Сума: 108.00 €
Сума: 80.00 €

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

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

Пробвай

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

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

Идеята

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

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

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

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

Упражнение

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

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

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

А.

class Order
{
private readonly List<Action<string>> listeners = new();
public void OnShipped(Action<string> listener)
{
listeners.Add(listener);
}
public void Ship(string id)
{
foreach (var listener in listeners)
{
listener(id);
}
}
}
var order = new Order();
order.OnShipped(id => Console.WriteLine($"Имейл: поръчка {id} е изпратена"));
order.OnShipped(id => Console.WriteLine($"Склад: махни {id} от опашката"));
order.Ship("1042");
Изход
Имейл: поръчка 1042 е изпратена
Склад: махни 1042 от опашката

Б.

class CourierApi
{
public string SendParcel(string city, int grams)
{
return $"Пратка за {city}, {grams} г";
}
}
interface IShipping
{
string Ship(string city, decimal kilograms);
}
class CourierShipping : IShipping
{
private readonly CourierApi api = new();
public string Ship(string city, decimal kilograms)
{
return api.SendParcel(city, (int)Math.Round(kilograms * 1000));
}
}
IShipping shipping = new CourierShipping();
Console.WriteLine(shipping.Ship("Пловдив", 1.25m));
Изход
Пратка за Пловдив, 1250 г

В.

static string PriceLabel(decimal price, bool withVat)
{
if (withVat)
{
return $"{price * 1.20m:F2} € с ДДС";
}
return $"{price:F2} € без ДДС";
}
Console.WriteLine(PriceLabel(25, true));
Console.WriteLine(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 за достъпно обучение по програмиране ·