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-контракты.

🎓 Источники

См. также