Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке патерн Adapter?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Патерн Adapter** перетворює інтерфейс одного класу на той, що очікує клієнт, щоб несумісні класи працювали разом без зміни коду. ```javascript class OldGateway { processPayment(amt) { /*...*/ } } class Adapter { constructor(gw) { this.gw = gw; } pay(amt) { this.gw.processPayment(amt); } } ``` **Ключове:** композиція з перекладом інтерфейсу, а не переписування.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Патерн Adapter** (адаптер) перетворює інтерфейс одного класу на той, що очікує клієнт, щоб несумісні класи могли працювати разом без зміни коду. ## Теорія ### TL;DR - Аналогія: перехідник для розетки. Вилка не змінюється, перехідник вирішує невідповідність форми і напруги. - Патерн огортає існуючий клас і делегує виклики з перекладом, замість того щоб переписувати старий код. - Головний тригер: інтерфейс не підходить і змінити жодну зі сторін не можна. - Правило вибору: якщо не можна змінити старий клас і не можна змінити нову систему, то Adapter. - Adapter стоїть на стику двох систем, які ніколи не були розраховані на спільну роботу. ### Швидкий приклад ```javascript // Старий принтер знає тільки printRaw(data) class OldPrinter { printRaw(data) { console.log(`Raw: ${data}`); } } // Adapter огортає старий принтер, надає метод printFormatted(text) class PrinterAdapter { constructor(oldPrinter) { this.old = oldPrinter; } printFormatted(text) { const formatted = text.toUpperCase(); // переклад this.old.printRaw(formatted); // делегування } } const adapter = new PrinterAdapter(new OldPrinter()); adapter.printFormatted('hello'); // Виведе: Raw: HELLO ``` Тут відбуваються дві речі: переклад (перетворення у верхній регістр) і делегування (виклик старого методу). Жоден клас не змінюється. ### Ключова різниця від переписування Можна було б просто перейменувати `printRaw` на `printFormatted`. Але що якщо це стороння бібліотека? Або 50 місць у коді досі викликають `printRaw`? Adapter стоїть між двома системами. Старий код працює далі. Новий код отримує потрібний інтерфейс. Обидві сторони залишаються незайманими. ### Коли використовувати - Старий API не відповідає тому, що очікує новий код, і змінити жодну зі сторін не виходить. - Стороння бібліотека зі своїм інтерфейсом: огортаємо її, не форкаємо. - Кілька сервісів з різними сигнатурами: по одному адаптеру на сервіс, єдиний інтерфейс для решти коду. - Тестові моки замість реальних залежностей: підміняємо через спільний інтерфейс, логіка не змінюється. ### Як це працює всередині Клієнт викликає метод адаптера. Адаптер перетворює параметри на те, що чекає adaptee, викликає його, потім перекладає результат назад. У JavaScript структурна типізація робить це природно: якщо адаптер має потрібну форму, клієнтський код приймає його. V8 компілює обгортку як тонкий шар без відчутних накладних витрат для типових випадків. У TypeScript явно додавай `implements IPayment` або відповідний інтерфейс. Без цього компілятор не спіймає невідповідність інтерфейсів під час збірки. ### Типові помилки **1. Неповний інтерфейс** ```javascript // Неправильно: є тільки pay(), немає refund() class PartialAdapter { pay() { /* ... */ } } adapter.refund(); // TypeError: adapter.refund is not a function ``` Реалізуй увесь цільовий інтерфейс. Якщо метод не має відповідника в старому класі, кидай явну помилку замість того щоб залишати `undefined`. **2. Наслідування замість композиції** ```javascript // Неправильно: extends прив'язує до ієрархії OldGateway class BadAdapter extends OldGateway { pay() { super.processPayment(); } } ``` Використовуй композицію: `new Adapter(new OldGateway())`. Наслідування створює тісний зв'язок: якщо `OldGateway` змінить свою ієрархію, адаптер зламається. **3. Відсутність перекладу помилок** ```javascript // Неправильно: стара помилка просочується до клієнта pay(amount) { this.old.process(amount); } // Правильно pay(amount) { try { this.old.process(amount); } catch (e) { throw new PaymentError(e.message); // перекладаємо у відомий тип } } ``` Старий код кидає `OldAuthError`. Новий код чекає `UnauthorizedError`. Адаптер обробляє це відображення. Інакше обробка помилок на стороні клієнта ламається. **4. Покладання лише на структурну типізацію в TypeScript** JavaScript гнучкий щодо форм об'єктів, але TypeScript зі strict mode хоче явного `implements IPayment`. Без цього помилки типів не виявляються під час збірки в складних кодових базах. ### Реальне використання - `express-jwt` адаптує бібліотеку `jsonwebtoken` до сигнатури Express middleware. - `react-query` огортає Axios і Fetch за єдиним query function. - Міграційні утиліти AWS SDK з версії 2 до 3 побудовані на адаптерах. - Redux-saga адаптує між thunk-стилем і ефектами saga. ### Питання на співбесіді **Q:** Яка різниця між Adapter і Decorator? **A:** Decorator додає поведінку, зберігаючи той самий інтерфейс. Adapter змінює інтерфейс під те, що очікує клієнт. Структура схожа, мета різна. **Q:** Яка різниця між Adapter і Bridge? **A:** Adapter вирішує проблему вже існуючих класів, які не були розраховані на спільну роботу. Bridge розділяє абстракцію від реалізації на етапі проектування, до виникнення проблеми. **Q:** Чи можна зробити двосторонній адаптер? **A:** Так. Кожен напрямок має власний переклад. ORM-и роблять саме це: SQL у бік бази даних, об'єкти у бік застосунку. **Q:** Коли Adapter може додавати затримку? **A:** У глибоких ланцюгах викликів або при дорогому перекладі, наприклад конвертації великих структур даних. У таких випадках варто профілювати глибину делегування. **Q:** (Senior) Як Adapter вписується в API Gateway у мікросервісах? **A:** Gateway сам по собі є системним адаптером. Він перекладає зовнішні протоколи, REST або gRPC, у формат, який очікують внутрішні сервіси, і обробляє версіонування без змін у самих сервісах. ## Приклади ### Базовий: обгортка для застарілого принтера ```javascript class OldPrinter { printRaw(data) { console.log(`Raw: ${data}`); } } class PrinterAdapter { constructor(printer) { this.printer = printer; } printFormatted(text) { this.printer.printRaw(text.toUpperCase()); } } const adapter = new PrinterAdapter(new OldPrinter()); adapter.printFormatted('hello'); // Raw: HELLO ``` Старий клас не змінюється. Нові виклики використовують `printFormatted`. Адаптер бере на себе переклад між двома інтерфейсами. ### Середній рівень: адаптація legacy-автентифікації до Express middleware Реальний сценарій: команда отримує бібліотеку авторизації з callback-інтерфейсом. Нові маршрути Express чекають `req.user`, встановленого в middleware. ```javascript class OldAuthLib { authenticate(userId, callback) { // імітація асинхронного запиту до БД callback(null, { id: userId, name: 'User' }); } } class AuthMiddlewareAdapter { constructor(auth) { this.auth = auth; } middleware(req, res, next) { this.auth.authenticate(req.body.userId, (err, user) => { if (err) return next(err); req.user = user; // відповідає конвенції Express next(); }); } } const authAdapter = new AuthMiddlewareAdapter(new OldAuthLib()); app.post('/login', (req, res, next) => authAdapter.middleware(req, res, next), (req, res) => res.json(req.user) // { id: 123, name: 'User' } ); ``` Обробник маршруту не знає, що під капотом стара система. Я сам бачив, як цей підхід рятував міграцію, коли сторонній SDK змінив свій API між версіями: оновити довелося тільки адаптер. ### Просунутий: платіжний шлюз з повним перекладом помилок Приклад показує, як виглядає готовий до продакшену адаптер: повний інтерфейс і переклад помилок. ```javascript class OldPaymentGateway { processPayment(amount) { console.log(`Processing $${amount}`); } reverseTransaction(id) { console.log(`Reversing ${id}`); } } class PaymentError extends Error {} class PaymentAdapter { constructor(legacyGateway) { this.gateway = legacyGateway; } pay(amount, currency) { try { const converted = currency === 'EUR' ? amount * 1.1 : amount; this.gateway.processPayment(converted); } catch (e) { throw new PaymentError(`Payment failed: ${e.message}`); } } refund(transactionId) { try { this.gateway.reverseTransaction(transactionId); } catch (e) { throw new PaymentError(`Refund failed: ${e.message}`); } } } const adapter = new PaymentAdapter(new OldPaymentGateway()); adapter.pay(100, 'EUR'); // Processing $110 adapter.refund('tx-001'); // Reversing tx-001 ``` Обидва методи реалізовані, часткового інтерфейсу немає. Помилки перекладаються у відомий тип. Адаптер покриває повний контракт цільового інтерфейсу.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.