Защо шаблони
Накратко: Шаблонът за дизайн е назовано, проверено решение на често срещан проблем в кода; класическите 23 шаблона са в три семейства: създаващи, структурни и поведенчески.
Проблем
Два абзаца за една дума
Преглед на кода в онлайн магазин. Петър чете касата и оставя коментар на маркирания ред:
class Checkout: RULES = { "карта": lambda amount: amount, "наложен платеж": lambda amount: amount + 2.50, "ваучер": lambda amount: max(amount - 10, 0), }
def pay(self, method, amount): return self.RULES[method](amount)checkout = Checkout()print(f"{checkout.pay("наложен платеж", 80):.2f} €")print(f"{checkout.pay("ваучер", 80):.2f} €")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 и слоевете не е от книгата. Той е архитектурен шаблон: къде в програмата живее базата данни.
Модулът завършва с упражнение, Магазинът: кой шаблон къде: разхвърлян магазин, който сам подреждаш.
В Python някои шаблони почти изчезват, защото езикът вече ги има. Функцията е стойност, затова Strategy и Command често са просто функция. Обхождането (Iterator) е вградено вfor. Всеки урок от модула казва къде е така, вместо да пише Java с друг синтаксис.Знакът @ над функция в Python се казва декоратор. Той е роднина на шаблона Decorator, но не е същото нещо; урокът за Decorator показва и двете.Виж го
Капанът: шаблон заради шаблона
Задачата е една: 10% отстъпка за поръчки над 100 €. Някой току-що е чел за шаблоните:
from abc import ABC, abstractmethod
class DiscountStrategy(ABC): @abstractmethod def apply(self, amount): pass
class PercentOverThresholdDiscountStrategy(DiscountStrategy): def __init__(self, percent, threshold): self.percent = percent self.threshold = threshold
def apply(self, amount): if amount > self.threshold: return amount * (1 - self.percent / 100) return amount
class AbstractDiscountStrategyFactory(ABC): @abstractmethod def create_strategy(self): pass
class DefaultDiscountStrategyFactory(AbstractDiscountStrategyFactory): def create_strategy(self): return PercentOverThresholdDiscountStrategy(10, 100)
class DiscountService: def __init__(self, factory): self.factory = factory
def discounted(self, amount): return self.factory.create_strategy().apply(amount)service = DiscountService(DefaultDiscountStrategyFactory())print(f"Сума: {service.discounted(120):.2f} €")print(f"Сума: {service.discounted(80):.2f} €")Сума: 108.00 €Сума: 80.00 €Пет класа, два шаблона (Strategy и фабрика) и име AbstractDiscountStrategyFactory. Същата задача, без шаблон:
def discounted(amount): return amount * 0.9 if amount > 100 else amountprint(f"Сума: {discounted(120):.2f} €")print(f"Сума: {discounted(80):.2f} €")Сума: 108.00 €Сума: 80.00 €Изходът е същият. Разликата е в четенето: за да разбереш колко е отстъпката, в първия вариант минаваш през пет класа, а във втория четеш един ред.
Шаблоните в първия вариант не решават нито един проблем. Отстъпката е една, никой не я сменя по време на работа и никой не избира между отстъпки. Когато дойде втората отстъпка и клиентът трябва да избира, тогава функцията става стратегия. Не по-рано.
В Python това е още по-ясно: абстрактен клас сABC за правило от един ред е Java, написана на Python. Функцията вече е стойност, която можеш да подадеш и да пазиш.Пробвай
Кое семейство и трябва ли шаблон
Идеята
Кога шаблонът вреди
Шаблонът е отговор на проблем. Без проблема остава само цената:
- „Може да ни потрябва.“ Клас с един метод там, където стига функция, фабрика за един клас, таблица с едно правило. Всеки, който чете кода, плаща за това, а никой не го ползва.
- Името казва шаблона, а не работата.
AbstractDiscountStrategyFactoryказва как е направено,discountedказва какво прави. Ако не можеш да опишеш класа без думите на шаблона, вероятно не ти трябва. - Шаблон от друг език. Java шаблон, преписан клас по клас: абстрактен клас, фабрика и клас с един метод.
- Шаблонът преди болката. Добрият ред е обратният. Пишеш простото. Когато същият
switchсе появи на трето място или правилата станат пет, правиш refactor към шаблона. Тогава вече знаеш точно какво се сменя.
Учиш шаблоните, за да ги разпознаваш и назоваваш в чужд код и в прегледа. Не за да ги слагаш навсякъде.
Упражнение
Познай шаблона
Реши задачата в редактора си.
Три фрагмента от един магазин. Пусни ги и за всеки кажи: има ли шаблон, кой е (Strategy, Observer, Factory Method, Singleton или Adapter от урока „Шаблони за дизайн“ в модула „Модерен Python“) и с едно изречение: какъв проблем решава. Един от трите няма нужда от шаблон.
А.
class Order: def __init__(self): self.listeners = []
def on_shipped(self, listener): self.listeners.append(listener)
def ship(self, order_id): for listener in self.listeners: listener(order_id)order = Order()order.on_shipped(lambda order_id: print(f"Имейл: поръчка {order_id} е изпратена"))order.on_shipped(lambda order_id: print(f"Склад: махни {order_id} от опашката"))order.ship("1042")Имейл: поръчка 1042 е изпратенаСклад: махни 1042 от опашкатаБ.
class CourierApi: def send_parcel(self, city, grams): return f"Пратка за {city}, {grams} г"
class CourierShipping: def __init__(self): self.api = CourierApi()
def ship(self, city, kilograms): return self.api.send_parcel(city, round(kilograms * 1000))shipping = CourierShipping()print(shipping.ship("Пловдив", 1.25))Пратка за Пловдив, 1250 гВ.
def price_label(price, with_vat): if with_vat: return f"{price * 1.20:.2f} € с ДДС" return f"{price:.2f} € без ДДС"print(price_label(25, True))print(price_label(25, False))30.00 € с ДДС25.00 € без ДДСОтговорите
- А е Observer. Поръчката не знае кой я слуша. Имейлът и складът се абонират, а
shipги вика по реда на абонирането. Нов слушател не пипаOrder. - Б е Adapter.
CourierShippingпревежда твоетоship(килограми) към чуждотоsend_parcel(грамове), без да пипа нито едното. - В няма нужда от шаблон. Два варианта, на едно място, никой не ги сменя. Strategy тук би бил две функции и таблица вместо един
if.
Още задачи
- Кога В иска шаблон. Магазинът започва да продава в пет държави с различен ДДС, а клиентът избира държавата. Какво се променя? Напиши кой шаблон би помогнал и защо, без код.
- Шаблонът, който изчезва. Вземи петте класа от картата „Капанът“ и ги замени с една функция. Преброй редовете преди и след. Изходът трябва да остане същият.
- Твоят код. Намери в свой проект
switchили верига отif, която се повтаря на две места. С коя дума би я назовал в прегледа?
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Списъци: append, remove и sort · dict и set · Ламбди · Шаблони за дизайн
- Задачи по темата: Шаблони за дизайн