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

Adapter и Facade

Накратко: Adapter превежда чужд интерфейс към този, който кодът ти очаква, а Facade скрива няколко подсистеми (склад, плащане, фактура, имейл) зад един метод.

Проблем

Новият доставчик не пасва

Магазинът плаща през свой интерфейс: charge приема сумата в евро и казва дали плащането е минало. Магазинът не знае кой стои отзад:

class Shop {
constructor(gateway) {
this.gateway = gateway;
}
pay(order, amount) {
if (this.gateway.charge(amount)) {
console.log(order + ": платено");
} else {
console.log(order + ": отказано");
}
}
}
В JavaScript интерфейс няма: магазинът приема всеки обект, който има метод charge.

Фирмата сменя доставчика на плащания с FastPay, който взима по-ниски такси. FastPay дава готова библиотека:

// Библиотеката на FastPay: чужд код, който не можеш да промениш.
class FastPayClient {
makePayment(currency, amountInCents, options) {
console.log(`FastPay: ${amountInCents} ${currency}`);
if (amountInCents > 100000) {
return "DECLINED";
}
return "APPROVED";
}
}

Прави същото, но с друга форма. Методът се казва makePayment, иска валута, сума в центове и опции, и връща текст "APPROVED" или "DECLINED", а не да или не. Подаден направо на магазина, не пасва:

const shop = new Shop(new FastPayClient());
shop.pay("#1042", 19.99);
Грешка
TypeError: this.gateway.charge is not a function

Библиотеката е чужда: не можеш да ѝ добавиш метод. Да пренапишеш магазина за FastPay значи да пипнеш всяко място, което плаща, и да го пипнеш пак при следващия доставчик.

Идеята

Две обвивки, две различни цели

Двата шаблона в този урок обвиват чужд код, но решават различни задачи.

Adapter (адаптер) е клас с формата, която кодът ти чака, а вътре вика чуждия обект. Превежда имената, единиците и резултата. Като преходник за контакт в чужбина: щепселът на зарядното ти не пасва на контакта, преходникът пасва и на двете. Нищо не добавя, само сменя формата.

Facade (фасада) е една проста входна точка пред много класове, с които трябва да се работи в определен ред. Като рецепцията в хотел: не звъниш поотделно на чистачите, кухнята и счетоводството, казваш на рецепцията.

Разликата накратко:

  • адаптерът сменя формата на един интерфейс, за да пасне на друг;
  • фасадата опростява работата с много класове, като събира обичайната последователност зад един метод.

Виж го

Адаптерът превежда

FastPayAdapter има метод charge, точно като магазина чака. Вътре пази клиента на FastPay и превежда: евро в центове, "EUR" и празни опции, текстът в да или не:

class FastPayAdapter {
constructor(client) {
this.client = client;
}
charge(amount) {
const cents = Math.round(amount * 100);
const status = this.client.makePayment("EUR", cents, {});
return status === "APPROVED";
}
}

Магазинът получава адаптера вместо клиента. В него не се пипа нищо:

const shop = new Shop(new FastPayAdapter(new FastPayClient()));
shop.pay("#1042", 19.99);
shop.pay("#1043", 1299);
Изход
FastPay: 1999 EUR
#1042: платено
FastPay: 129900 EUR
#1043: отказано

Телевизорът за 1299 € е над лимита на FastPay (1000 €), затова е отказан. Магазинът научава отказа на своя език: false.

Сцената показва адаптера, после и фасадата от следващата карта:

const payments = new FastPayAdapter(new FastPayClient());
console.log(payments.charge(19.99));
const checkout = new CheckoutFacade(new Stock(), payments, new Invoices(), new Mailer());
checkout.placeOrder("Чайник", 24.90);
Стъпка по стъпка, с думи
  1. Магазинът вика charge(сума в евро). FastPayClient има само makePayment(валута, центове, опции), и прякото извикване отскача.
  2. FastPayAdapter застава между тях: отвън има charge, както магазинът чака, а вътре пази клиента на FastPay.
  3. charge(19.99): адаптерът закръгля евро в центове, 1999, и вика makePayment с "EUR".
  4. FastPay отговаря "APPROVED", а адаптерът го превежда в true: езика на магазина.
  5. CheckoutFacade получава склада, плащанията (същия адаптер), фактурите и имейла.
  6. placeOrder: първо складът запазва чайника.
  7. После плащането, пак през адаптера: 24.90 € стават 2490 цента.
  8. Минало е, затова фасадата издава фактурата и праща имейла. Викащият написа един ред.
По-точно, за любопитните
  • JavaScript не проверява типове: магазинът вика charge на каквото му дадеш. Адаптерът трябва само да има метод charge с правилните параметри и резултат. Грешката при чужд обект идва чак при плащането (TypeError), не при старта, затова тестът е важен.
  • В TypeScript формата се записва като interface PaymentGateway { charge(amount: number): boolean } и компилаторът я проверява.

Виж го

Фасадата пред четири класа

Плащането е само една стъпка от поръчката. Преди него складът запазва стоката, а след него идват фактурата и имейлът. Ако плащането е отказано, складът освобождава стоката. Всяка стъпка е малък клас:

class Stock {
reserve(item) {
console.log("Склад: запазвам " + item);
return true;
}
release(item) {
console.log("Склад: освобождавам " + item);
}
}
class Invoices {
issue(item, amount) {
console.log(`Фактура: ${item}, ${amount.toFixed(2)} €`);
}
}
class Mailer {
send(text) {
console.log("Имейл: " + 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 {
constructor(stock, payments, invoices, mailer) {
this.stock = stock;
this.payments = payments;
this.invoices = invoices;
this.mailer = mailer;
}
placeOrder(item, amount) {
if (!this.stock.reserve(item)) {
return false;
}
if (!this.payments.charge(amount)) {
this.stock.release(item);
return false;
}
this.invoices.issue(item, amount);
this.mailer.send("потвърждение за " + item);
return true;
}
}

Плащанията във фасадата са обект с charge, подадени отвън (dependency injection от урока „Singleton и Dependency Injection“). Тук това е адаптерът от предишната карта. Двата шаблона работят заедно:

const payments = new FastPayAdapter(new FastPayClient());
const checkout = new CheckoutFacade(new Stock(), payments, new Invoices(), new Mailer());
checkout.placeOrder("Чайник", 24.90);
Изход
Склад: запазвам Чайник
FastPay: 2490 EUR
Фактура: Чайник, 24.90 €
Имейл: потвърждение за Чайник

Фасадата не е стена. Класовете зад нея се ползват и направо, когато трябва нещо извън обичайния път. Счетоводството издава фактурата отново:

const invoices = new Invoices();
invoices.issue("Чайник", 24.90);
Изход
Фактура: Чайник, 24.90 €

Пробвай

Предвиди изхода

Класовете са от картите „Адаптерът превежда“ и „Фасадата пред четири класа“. Какво печата програмата?

const payments = new FastPayAdapter(new FastPayClient());
const checkout = new CheckoutFacade(new Stock(), payments, new Invoices(), new Mailer());
checkout.placeOrder("Телевизор", 1299);
Отговор

Пробвай

Намери грешката

Колега „опрости“ адаптера: центовете и без това са цяло число. Клиентът плаща поръчка #1042 за 19.99 €:
const shop = new Shop(new FastPayAdapter(new FastPayClient()));
shop.pay("#1042", 19.99);
Изход
FastPay: 1998 EUR
#1042: платено
FastPay взе 1998 цента, един по-малко. На кой ред в адаптера е грешката?

Избери реда с грешката.

По-точно, за любопитните

Пробвай сам:

console.log(19.99 * 100);
Изход
1998.9999999999998
В JavaScript няма десетичен тип за пари. Затова сумите се пазят като цели центове (1999), а в евро се превръщат само за показване: (1999 / 100).toFixed(2).

Идеята

Кога не ти трябва

Адаптер не ти трябва, когато и двете страни са твой код. Тогава смени едната да пасне на другата, вместо да слагаш преводач между тях. Адаптер е за код, който не можеш да пипнеш: чужда библиотека, стара система, услуга на друг екип.

Фасада не ти трябва, когато последователността се ползва на едно място. Една функция там стига. Фасадата има и капан: лесно става клас, през който минава всичко, с метод за всяка нужда на всеки екран. Пази в нея само обичайните пътища, а за останалото викай класовете направо.

И двата шаблона са тънки. Адаптер, който започва да решава бизнес правила (отстъпки, лимити), вече не е адаптер. Фасада, която сама смята суми, вместо да вика класовете, които го правят, също.

Упражнение

Втори доставчик

Реши задачата в редактора си.

PayNow. Фирмата пуска и втори доставчик, PayNow. Библиотеката му иска центовете първи и валутата втора, и връща число: 0 значи успех, всичко друго е отказ.

class PayNowApi {
send(cents, currency) {
console.log(`PayNow: ${currency} ${cents}`);
return 0;
}
}

Напиши PayNowAdapter, така че магазинът да плаща през PayNow без промяна:

const shop = new Shop(new PayNowAdapter(new PayNowApi()));
shop.pay("#1042", 19.99);
Изход
PayNow: EUR 1999
#1042: платено

Още задачи

  • Отказ на поръчка. Добави във фасадата cancelOrder(item): складът освобождава стоката, а клиентът получава имейл „отказана поръчка за …“.
  • Тестов доставчик. Напиши FakeGateway с метод charge, който отказва всяка сума над 50 €. Подай го на фасадата и провери, че при отказ складът освобождава стоката.
  • Фактурата пада. Нека издаването на фактура хвърля изключение. Парите вече са взети. Какво трябва да направи фасадата? Реши и го напиши: например да продължи с имейла и да запише грешката, за да се издаде фактурата по-късно.

Тест

Провери се

1. Библиотеката на доставчика има метод с друго име, други единици и друг резултат от тези, които кодът ти чака. Кой шаблон пасва?
2. Уебсайтът, мобилното приложение и телефонните поръчки повтарят едни и същи четири стъпки в същия ред. Кой шаблон пасва?
3. Има ли фасадата, складът и фактурите пак се ползват ли направо?
4. Адаптерът превръща 19.99 € в центове с отрязване на дробната част. Какво получава доставчикът?

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