Adapter и Facade
Накратко: Adapter превежда чужд интерфейс към този, който кодът ти очаква, а Facade скрива няколко подсистеми (склад, плащане, фактура, имейл) зад един метод.
Проблем
Новият доставчик не пасва
Магазинът плаща през свой интерфейс: charge приема сумата в евро и казва дали плащането е минало. Магазинът не знае кой стои отзад:
class Shop: def __init__(self, gateway): self.gateway = gateway
def pay(self, order, amount): if self.gateway.charge(amount): print(order + ": платено") else: print(order + ": отказано")charge.Фирмата сменя доставчика на плащания с FastPay, който взима по-ниски такси. FastPay дава готова библиотека:
# Библиотеката на FastPay: чужд код, който не можеш да промениш.class FastPayClient: def make_payment(self, currency, amount_in_cents, options): print(f"FastPay: {amount_in_cents} {currency}") if amount_in_cents > 100000: return "DECLINED" return "APPROVED"Прави същото, но с друга форма. Методът се казва make_payment, иска валута, сума в центове и опции, и връща текст "APPROVED" или "DECLINED", а не да или не. Подаден направо на магазина, не пасва:
shop = Shop(FastPayClient())shop.pay("#1042", 19.99)AttributeError: 'FastPayClient' object has no attribute 'charge'Библиотеката е чужда: не можеш да ѝ добавиш метод. Да пренапишеш магазина за FastPay значи да пипнеш всяко място, което плаща, и да го пипнеш пак при следващия доставчик.
Идеята
Две обвивки, две различни цели
Двата шаблона в този урок обвиват чужд код, но решават различни задачи.
Adapter (адаптер) е клас с формата, която кодът ти чака, а вътре вика чуждия обект. Превежда имената, единиците и резултата. Като преходник за контакт в чужбина: щепселът на зарядното ти не пасва на контакта, преходникът пасва и на двете. Нищо не добавя, само сменя формата.
Facade (фасада) е една проста входна точка пред много класове, с които трябва да се работи в определен ред. Като рецепцията в хотел: не звъниш поотделно на чистачите, кухнята и счетоводството, казваш на рецепцията.
Разликата накратко:
- адаптерът сменя формата на един интерфейс, за да пасне на друг;
- фасадата опростява работата с много класове, като събира обичайната последователност зад един метод.
Виж го
Адаптерът превежда
FastPayAdapter има метод charge, точно като магазина чака. Вътре пази клиента на FastPay и превежда: евро в центове, "EUR" и празни опции, текстът в да или не:
class FastPayAdapter: def __init__(self, client): self.client = client
def charge(self, amount): cents = round(amount * 100) status = self.client.make_payment("EUR", cents, {}) return status == "APPROVED"Магазинът получава адаптера вместо клиента. В него не се пипа нищо:
shop = Shop(FastPayAdapter(FastPayClient()))shop.pay("#1042", 19.99)shop.pay("#1043", 1299)FastPay: 1999 EUR#1042: платеноFastPay: 129900 EUR#1043: отказаноТелевизорът за 1299 € е над лимита на FastPay (1000 €), затова е отказан. Магазинът научава отказа на своя език: False.
Сцената показва адаптера, после и фасадата от следващата карта:
payments = FastPayAdapter(FastPayClient())print(payments.charge(19.99))
checkout = CheckoutFacade(Stock(), payments, Invoices(), Mailer())checkout.place_order("Чайник", 24.90)Стъпка по стъпка, с думи
- Магазинът вика charge(сума в евро). FastPayClient има само make_payment(валута, центове, опции), и прякото извикване отскача.
- FastPayAdapter застава между тях: отвън има charge, както магазинът чака, а вътре пази клиента на FastPay.
- charge(19.99): адаптерът закръгля евро в центове, 1999, и вика make_payment с "EUR".
- FastPay отговаря "APPROVED", а адаптерът го превежда в True: езика на магазина.
- CheckoutFacade получава склада, плащанията (същия адаптер), фактурите и имейла.
- place_order: първо складът запазва чайника.
- После плащането, пак през адаптера: 24.90 € стават 2490 цента.
- Минало е, затова фасадата издава фактурата и праща имейла. Викащият написа един ред.
По-точно, за любопитните
- Python не проверява типове: магазинът вика
chargeна каквото му дадеш. Адаптерът трябва само да има методchargeс правилните параметри и резултат. Грешката при чужд обект идва чак при плащането (AttributeError), не при старта, затова тестът е важен. - Ако искаш формата да е записана,
typing.Protocolописва „обект с методcharge(amount) -> bool“, а проверката на типове (mypy) я сверява преди пускане.
Виж го
Фасадата пред четири класа
Плащането е само една стъпка от поръчката. Преди него складът запазва стоката, а след него идват фактурата и имейлът. Ако плащането е отказано, складът освобождава стоката. Всяка стъпка е малък клас:
class Stock: def reserve(self, item): print("Склад: запазвам " + item) return True
def release(self, item): print("Склад: освобождавам " + item)
class Invoices: def issue(self, item, amount): print(f"Фактура: {item}, {amount:.2f} €")
class Mailer: def send(self, text): print("Имейл: " + text)Без фасада всяко място, което приема поръчка (сайтът, мобилното приложение, поръчките по телефон), пише същото, в същия ред:
if stock.reserve(item): if payments.charge(amount): invoices.issue(item, amount) mailer.send("потвърждение за " + item) else: stock.release(item)Ако някое място забрави stock.release, стоката остава запазена за поръчка, която не е платена. CheckoutFacade пише реда веднъж:
class CheckoutFacade: def __init__(self, stock, payments, invoices, mailer): self.stock = stock self.payments = payments self.invoices = invoices self.mailer = mailer
def place_order(self, item, amount): if not self.stock.reserve(item): return False if not self.payments.charge(amount): self.stock.release(item) return False self.invoices.issue(item, amount) self.mailer.send("потвърждение за " + item) return TrueПлащанията във фасадата са обект с charge, подадени отвън (dependency injection от урока „Singleton и Dependency Injection“). Тук това е адаптерът от предишната карта. Двата шаблона работят заедно:
payments = FastPayAdapter(FastPayClient())checkout = CheckoutFacade(Stock(), payments, Invoices(), Mailer())checkout.place_order("Чайник", 24.90)Склад: запазвам ЧайникFastPay: 2490 EURФактура: Чайник, 24.90 €Имейл: потвърждение за ЧайникФасадата не е стена. Класовете зад нея се ползват и направо, когато трябва нещо извън обичайния път. Счетоводството издава фактурата отново:
invoices = Invoices()invoices.issue("Чайник", 24.90)Фактура: Чайник, 24.90 €Пробвай
Предвиди изхода
Класовете са от картите „Адаптерът превежда“ и „Фасадата пред четири класа“. Какво печата програмата?
payments = FastPayAdapter(FastPayClient())checkout = CheckoutFacade(Stock(), payments, Invoices(), Mailer())checkout.place_order("Телевизор", 1299)Пробвай
Намери грешката
shop = Shop(FastPayAdapter(FastPayClient()))shop.pay("#1042", 19.99)FastPay: 1998 EUR#1042: платеноИзбери реда с грешката.
int не закръгля, а отрязва дробната част. А 19.99 във float не е точно 19.99: числата с плаваща запетая се пазят в двоичен вид, и 19.99 * 100 дава 1998.9999999999998. Отрязано, това е 1998. Поправката е round(amount * 100): най-близкото цяло, 1999. Цент изглежда малко, но е грешка при всяка такава сума, и сметките на магазина и на FastPay не излизат.По-точно, за любопитните
Пробвай сам:
print(19.99 * 100)1998.9999999999998round в Python закръгля половинките към четно: round(2.5) е 2. За пари, където половинката отива нагоре, има Decimal, който пази сумата точно:
print(round(2.5), round(3.5))
from decimal import Decimal, ROUND_HALF_UP
print((Decimal("0.125") * 100).quantize(Decimal("1"), rounding=ROUND_HALF_UP))2 413Идеята
Кога не ти трябва
Адаптер не ти трябва, когато и двете страни са твой код. Тогава смени едната да пасне на другата, вместо да слагаш преводач между тях. Адаптер е за код, който не можеш да пипнеш: чужда библиотека, стара система, услуга на друг екип.
Фасада не ти трябва, когато последователността се ползва на едно място. Една функция там стига. Фасадата има и капан: лесно става клас, през който минава всичко, с метод за всяка нужда на всеки екран. Пази в нея само обичайните пътища, а за останалото викай класовете направо.
И двата шаблона са тънки. Адаптер, който започва да решава бизнес правила (отстъпки, лимити), вече не е адаптер. Фасада, която сама смята суми, вместо да вика класовете, които го правят, също.
Упражнение
Втори доставчик
Реши задачата в редактора си.
PayNow. Фирмата пуска и втори доставчик, PayNow. Библиотеката му иска центовете първи и валутата втора, и връща число: 0 значи успех, всичко друго е отказ.
class PayNowApi: def send(self, cents, currency): print(f"PayNow: {currency} {cents}") return 0Напиши PayNowAdapter, така че магазинът да плаща през PayNow без промяна:
shop = Shop(PayNowAdapter(PayNowApi()))shop.pay("#1042", 19.99)PayNow: EUR 1999#1042: платеноОще задачи
- Отказ на поръчка. Добави във фасадата cancel_order(item): складът освобождава стоката, а клиентът получава имейл „отказана поръчка за …“.
- Тестов доставчик. Напиши
FakeGatewayс методcharge, който отказва всяка сума над 50 €. Подай го на фасадата и провери, че при отказ складът освобождава стоката. - Фактурата пада. Нека издаването на фактура хвърля изключение. Парите вече са взети. Какво трябва да направи фасадата? Реши и го напиши: например да продължи с имейла и да запише грешката, за да се издаде фактурата по-късно.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: dict и set · Шаблони за дизайн · Strategy · Singleton и Dependency Injection
- Продължава в: Магазинът: кой шаблон къде · Hexagonal и clean architecture · Интеграция със стари и чужди системи
- Задачи по темата: Шаблони за дизайн