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

Защо шаблони

Накратко: Шаблонът за дизайн е назовано, проверено решение на често срещан проблем в кода; класическите 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 шаблона в три семейства. Модулът минава през най-ползваните:

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 amount
print(f"Сума: {discounted(120):.2f} €")
print(f"Сума: {discounted(80):.2f} €")
Изход
Сума: 108.00 €
Сума: 80.00 €

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

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

В Python това е още по-ясно: абстрактен клас с ABC за правило от един ред е Java, написана на Python. Функцията вече е стойност, която можеш да подадеш и да пазиш.

Пробвай

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

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

Идеята

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

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

  • „Може да ни потрябва.“ Клас с един метод там, където стига функция, фабрика за един клас, таблица с едно правило. Всеки, който чете кода, плаща за това, а никой не го ползва.
  • Името казва шаблона, а не работата. 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, която се повтаря на две места. С коя дума би я назовал в прегледа?

Тест

Провери се

1. Какво е шаблон за дизайн?
2. Защо името на шаблона помага в прегледа на кода?
3. Кое е знак, че шаблонът е излишен?
4. Кой е добрият ред?
5. Adapter, Decorator и Composite са:

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