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