Adapter и Facade
Накратко: Adapter превежда чужд интерфейс към този, който кодът ти очаква, а Facade скрива няколко подсистеми (склад, плащане, фактура, имейл) зад един метод.
Проблем
Новият доставчик не пасва
Магазинът плаща през свой интерфейс: Charge приема сумата в евро и казва дали плащането е минало. Магазинът не знае кой стои отзад:
interface IPaymentGateway{ bool Charge(decimal amount);}
class Shop{ private readonly IPaymentGateway gateway;
public Shop(IPaymentGateway gateway) { this.gateway = gateway; }
public void Pay(string order, decimal amount) { if (gateway.Charge(amount)) { Console.WriteLine(order + ": платено"); } else { Console.WriteLine(order + ": отказано"); } }}Фирмата сменя доставчика на плащания с FastPay, който взима по-ниски такси. FastPay дава готова библиотека:
// Библиотеката на FastPay: чужд код, който не можеш да промениш.class FastPayClient{ public string MakePayment(string currency, long amountInCents, Dictionary<string, string> options) { Console.WriteLine($"FastPay: {amountInCents} {currency}"); if (amountInCents > 100000) { return "DECLINED"; } return "APPROVED"; }}Прави същото, но с друга форма. Методът се казва MakePayment, иска валута, сума в центове и опции, и връща текст "APPROVED" или "DECLINED", а не да или не. Подаден направо на магазина, не пасва:
var shop = new Shop(new FastPayClient());error CS1503: Argument 1: cannot convert from 'FastPayClient' to 'IPaymentGateway'Библиотеката е чужда: не можеш да ѝ добавиш метод. Да пренапишеш магазина за FastPay значи да пипнеш всяко място, което плаща, и да го пипнеш пак при следващия доставчик.
Идеята
Две обвивки, две различни цели
Двата шаблона в този урок обвиват чужд код, но решават различни задачи.
Adapter (адаптер) е клас с формата, която кодът ти чака, а вътре вика чуждия обект. Превежда имената, единиците и резултата. Като преходник за контакт в чужбина: щепселът на зарядното ти не пасва на контакта, преходникът пасва и на двете. Нищо не добавя, само сменя формата.
Facade (фасада) е една проста входна точка пред много класове, с които трябва да се работи в определен ред. Като рецепцията в хотел: не звъниш поотделно на чистачите, кухнята и счетоводството, казваш на рецепцията.
Разликата накратко:
- адаптерът сменя формата на един интерфейс, за да пасне на друг;
- фасадата опростява работата с много класове, като събира обичайната последователност зад един метод.
Виж го
Адаптерът превежда
FastPayAdapter имплементира IPaymentGateway. Вътре пази клиента на FastPay и превежда: евро в центове, "EUR" и празни опции, текстът в да или не:
class FastPayAdapter : IPaymentGateway{ private readonly FastPayClient client;
public FastPayAdapter(FastPayClient client) { this.client = client; }
public bool Charge(decimal amount) { long cents = (long)Math.Round(amount * 100, MidpointRounding.AwayFromZero); string status = client.MakePayment("EUR", cents, new Dictionary<string, string>()); return status == "APPROVED"; }}Магазинът получава адаптера вместо клиента. В него не се пипа нищо:
var shop = new Shop(new FastPayAdapter(new FastPayClient()));shop.Pay("#1042", 19.99m);shop.Pay("#1043", 1299m);FastPay: 1999 EUR#1042: платеноFastPay: 129900 EUR#1043: отказаноТелевизорът за 1299 € е над лимита на FastPay (1000 €), затова е отказан. Магазинът научава отказа на своя език: false.
Сцената показва адаптера, после и фасадата от следващата карта:
var payments = new FastPayAdapter(new FastPayClient());Console.WriteLine(payments.Charge(19.99m));
var checkout = new CheckoutFacade(new Stock(), payments, new Invoices(), new Mailer());checkout.PlaceOrder("Чайник", 24.90m);Стъпка по стъпка, с думи
- Магазинът вика Charge(сума в евро). FastPayClient има само MakePayment(валута, центове, опции), и прякото извикване отскача.
- FastPayAdapter застава между тях: отвън е IPaymentGateway с Charge, а вътре пази клиента на FastPay.
- Charge(19.99): адаптерът закръгля евро в центове, 1999, и вика MakePayment с "EUR".
- FastPay отговаря "APPROVED", а адаптерът го превежда в True: езика на магазина.
- CheckoutFacade получава склада, плащанията (същия адаптер), фактурите и имейла.
- PlaceOrder: първо складът запазва чайника.
- После плащането, пак през адаптера: 24.90 € стават 2490 цента.
- Минало е, затова фасадата издава фактурата и праща имейла. Викащият написа един ред.
По-точно, за любопитните
- Адаптери има и в .NET.
StreamReaderправи от поток байтове (Stream) четец на текст, аReadOnlyCollection<T>показва списък като колекция само за четене. - Адаптерът пази чуждия обект в поле (композиция). Може и да го наследи, но тогава магазинът вижда и всички методи на FastPay, а един адаптер не може да обвие вече създаден клиент.
Виж го
Фасадата пред четири класа
Плащането е само една стъпка от поръчката. Преди него складът запазва стоката, а след него идват фактурата и имейлът. Ако плащането е отказано, складът освобождава стоката. Всяка стъпка е малък клас:
class Stock{ public bool Reserve(string item) { Console.WriteLine("Склад: запазвам " + item); return true; }
public void Release(string item) { Console.WriteLine("Склад: освобождавам " + item); }}
class Invoices{ public void Issue(string item, decimal amount) { Console.WriteLine($"Фактура: {item}, {amount:F2} €"); }}
class Mailer{ public void Send(string text) { Console.WriteLine("Имейл: " + 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{ private readonly Stock stock; private readonly IPaymentGateway payments; private readonly Invoices invoices; private readonly Mailer mailer;
public CheckoutFacade(Stock stock, IPaymentGateway payments, Invoices invoices, Mailer mailer) { this.stock = stock; this.payments = payments; this.invoices = invoices; this.mailer = mailer; }
public bool PlaceOrder(string item, decimal amount) { if (!stock.Reserve(item)) { return false; } if (!payments.Charge(amount)) { stock.Release(item); return false; } invoices.Issue(item, amount); mailer.Send("потвърждение за " + item); return true; }}Плащанията във фасадата са IPaymentGateway, подадени отвън (dependency injection от урока „Singleton и Dependency Injection“). Тук това е адаптерът от предишната карта. Двата шаблона работят заедно:
var payments = new FastPayAdapter(new FastPayClient());var checkout = new CheckoutFacade(new Stock(), payments, new Invoices(), new Mailer());checkout.PlaceOrder("Чайник", 24.90m);Склад: запазвам ЧайникFastPay: 2490 EURФактура: Чайник, 24.90 €Имейл: потвърждение за ЧайникФасадата не е стена. Класовете зад нея се ползват и направо, когато трябва нещо извън обичайния път. Счетоводството издава фактурата отново:
var invoices = new Invoices();invoices.Issue("Чайник", 24.90m);Фактура: Чайник, 24.90 €Пробвай
Предвиди изхода
Класовете са от картите „Адаптерът превежда“ и „Фасадата пред четири класа“. Какво печата програмата?
var payments = new FastPayAdapter(new FastPayClient());var checkout = new CheckoutFacade(new Stock(), payments, new Invoices(), new Mailer());checkout.PlaceOrder("Телевизор", 1299m);Пробвай
Намери грешката
var shop = new Shop(new FastPayAdapter(new FastPayClient()));shop.Pay("#1044", 0.375m * 13.99m);FastPay: 524 EUR#1044: платеноИзбери реда с грешката.
(long) не закръгля, а отрязва дробната част. decimal пази 5.24625 точно, но 524.625 цента се отрязват до 524. Поправката е (long)Math.Round(amount * 100, MidpointRounding.AwayFromZero): половинката отива нагоре, 525. Без втория аргумент Math.Round закръгля половинките към четно (524.5 става 524), както банките, а магазинът трябва да избере правилото си изрично.По-точно, за любопитните
Пробвай сам:
Console.WriteLine(0.375m * 13.99m * 100);524.62500decimal, а не double: с double 19.99 * 100 дава 1998.9999999999998, точно като в Java. decimal пази десетичните цифри точно, но частите от цента пак трябва да се закръглят, и правилото (MidpointRounding) се пише изрично.Идеята
Кога не ти трябва
Адаптер не ти трябва, когато и двете страни са твой код. Тогава смени едната да пасне на другата, вместо да слагаш преводач между тях. Адаптер е за код, който не можеш да пипнеш: чужда библиотека, стара система, услуга на друг екип.
Фасада не ти трябва, когато последователността се ползва на едно място. Една функция там стига. Фасадата има и капан: лесно става клас, през който минава всичко, с метод за всяка нужда на всеки екран. Пази в нея само обичайните пътища, а за останалото викай класовете направо.
И двата шаблона са тънки. Адаптер, който започва да решава бизнес правила (отстъпки, лимити), вече не е адаптер. Фасада, която сама смята суми, вместо да вика класовете, които го правят, също.
Упражнение
Втори доставчик
Реши задачата в редактора си.
PayNow. Фирмата пуска и втори доставчик, PayNow. Библиотеката му иска центовете първи и валутата втора, и връща число: 0 значи успех, всичко друго е отказ.
class PayNowApi{ public int Send(int cents, string currency) { Console.WriteLine($"PayNow: {currency} {cents}"); return 0; }}Напиши PayNowAdapter, така че магазинът да плаща през PayNow без промяна:
var shop = new Shop(new PayNowAdapter(new PayNowApi()));shop.Pay("#1042", 19.99m);PayNow: EUR 1999#1042: платеноОще задачи
- Отказ на поръчка. Добави във фасадата CancelOrder(item): складът освобождава стоката, а клиентът получава имейл „отказана поръчка за …“.
- Тестов доставчик. Напиши
FakeGateway : IPaymentGateway, който отказва всяка сума над 50 €. Подай го на фасадата и провери, че при отказ складът освобождава стоката. - Фактурата пада. Нека издаването на фактура хвърля изключение. Парите вече са взети. Какво трябва да направи фасадата? Реши и го напиши: например да продължи с имейла и да запише грешката, за да се издаде фактурата по-късно.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Шаблони за дизайн · Списъци, речници и LINQ · Strategy · Singleton и Dependency Injection
- Продължава в: Магазинът: кой шаблон къде · Hexagonal и clean architecture · Интеграция със стари и чужди системи
- Задачи по темата: Шаблони за дизайн