Adapter Pattern — Адаптер
Конвертирует один интерфейс в другой. Стыкует две несовместимые абстракции, которые мы не хотим/не можем переписывать.
Проблема
Есть две существующие абстракции с разными контрактами. Одна — наша, другая — чужая (библиотека, legacy-код, другой отдел). Менять их нельзя. Нужно подружить.
Где используется
- Интеграция сторонних библиотек (axios → fetch, Moment.js → date-fns)
- Нормализация форматов API-ответов (snake_case → camelCase)
- Legacy-код: адаптация старого модуля под новый интерфейс
- Тестирование: адаптер для замены реальных сервисов на тестовые заглушки
- Унификация интерфейса для нескольких провайдеров (SMS, email, push)
Решение
Прослойка-переводчик: снаружи отдаёт один контракт, внутри обращается ко второму.
Два варианта связи с adaptee:
- Композиция: адаптер сам создаёт инстанс того, что оборачивает
- Агрегация: инстанс приходит снаружи в конструктор (предпочтительнее)
Реализации
Через extends (наследование)
class ArrayToQueueAdapter extends Array {
enqueue(x) { this.push(x); }
dequeue { return this.shift(); }
get count { return this.length; }
}
Через агрегацию (предпочтительнее)
class QueueAdapter {
#arr;
constructor(arr) { this.#arr = arr; } // агрегация
enqueue(x) { this.#arr.push(x); }
dequeue { return this.#arr.shift(); }
}
const q = new QueueAdapter();
q.enqueue(1); q.enqueue(2);
q.dequeue; // 1
Адаптер логгера (классический)
class OldLogger {
writeLog(level, text) { console.log(`[${level}] ${text}`); }
}
// Новый интерфейс: { log, warn, error }
class LoggerAdapter {
constructor(oldLogger) { this.logger = oldLogger; }
log(msg) { this.logger.writeLog('INFO', msg); }
warn(msg) { this.logger.writeLog('WARN', msg); }
error(msg) { this.logger.writeLog('ERROR', msg); }
}
function processData(data, logger) {
logger.log('start');
}
processData(data, new LoggerAdapter(new OldLogger));
Адаптер на функции (wrapper)
const promisify = (fn) => (...args) =>
new Promise((resolve, reject) => {
fn(...args, (err, res) => err ? reject(err) : resolve(res));
});
Адаптер API-ответа
function adaptUserFromAPI(raw) {
return {
id: raw.user_id,
name: raw.full_name,
email: raw.email_address,
avatar: raw.profile_picture_url,
createdAt: new Date(raw.created_at * 1000)
};
}
async function getUsers(page) {
const raw = await fetch(`/api/v1/users?page=${page}`).then(r => r.json());
return {
items: raw.data.users.map(adaptUserFromAPI),
total: raw.data.total_count,
page: raw.data.current_page
};
}
Унификация провайдеров SMS
class TwilioAdapter {
constructor(client) { this.client = client; }
send(phone, message) {
return this.client.sendSMS({ to: phone, from: '+71234567890', body: message });
}
}
class AWSAdapter {
constructor(client) { this.client = client; }
send(phone, message) {
return this.client.send({ PhoneNumber: phone, Message: message });
}
}
// Клиент работает с единым интерфейсом
const sms = process.env.SMS_PROVIDER === 'aws'
? new AWSAdapter(new AWSSMS)
: new TwilioAdapter(new TwilioSMS);
Где используется в JS-экосистеме
- Node.js:
util.promisify,util.callbackify— адаптеры контрактов асинхронности - Stream → AsyncIterator:
for await (const x of readable)— встроенный адаптер - 3rd-party API → внутренний контракт — частая задача
- DOM Event → внутренний event при миграции
Подводные камни
- Adapter ≠ Decorator: adapter меняет контракт, decorator расширяет тот же.
- Adapter ≠ Facade: adapter оборачивает одну абстракцию, facade — много.
- В JS adapter, wrapper, boxing — почти синонимы. Различает цель, не код.
extends Arrayработает, но даёт сужение области определения (теория типов), а не расширение.- Слишком сложный адаптер = нарушает SRP. Адаптер — только переводчик интерфейса.
- Двунаправленный адаптер (A→B и B→A) быстро превращается в запутанный код — лучше два отдельных адаптера.
Главные тезисы автора
- «Адаптер скрывает один контракт, наружу отдаёт другой» — главная формулировка.
- «Wrapper, боксинг — синонимы» в JS-практике.
- В классическом GoF adapter — это класс. В JS — может быть функцией.
- Адаптер часто использует другие паттерны внутри: revealing constructor, открытый конструктор для инжекции поведения.
- extends в ООП = расширение, в теории типов = сужение — важно понимать.
- Промисификация/коллбэкификация — классические адаптеры, стыкующие async-контракты.
🎓 Источники
- 🎓 Adapter — a pattern for achieving compatibility · 2019-03-19
- 🎓 Паттерн Adapter — пример кода из курса Patterns · 2024-07-17
- 🎓 GoF Patterns Adapter for JavaScript and TypeScript · 2024-11-11
- Композиция vs агрегация в адаптере
- ArrayToQueue через extends и через приватное поле
- extends как сужение области определения
- promisify/callbackify как адаптеры контрактов
- 🎓 Асинхронные адаптеры promisify, callbackify, asyncify · 2018-12-18
- 🎓 Адаптер, Декоратор, Прокси, Фасад — В чём разница · 2026-02-09
- refactoring.guru — Adapter