Перейти к содержимому
Фёдор Башинский

Адаптер

5 min read
Обложка статьи «Адаптер»

Адаптер — это структурный паттерн проектирования, который позволяет объектам с несовместимыми интерфейсами работать вместе, не изменяя их код.

Это первый структурный паттерн в серии — до этого Фабричный метод, Абстрактная фабрика, Строитель, Прототип и Одиночка отвечали на вопрос «как создать объект». Структурные паттерны отвечают на другой вопрос — «как из существующих объектов и классов собрать более крупную структуру».

Проблема

В статье про Одиночку DynoService измерял мощность двигателя через DynoStandConnection — собственный класс с методом measure(string $model): int, возвращающим лошадиные силы.

Допустим, компания покупает диагностический стенд другого производителя, а к нему прилагается готовый SDK, который мы не можем менять — это чужой vendor-код:

// Класс из стороннего SDK — редактировать нельзя.
final class LegacyDynoDevice
{
    public function readPowerKw(string $carModel): float
    {
        // Возвращает мощность в киловаттах, а не в лошадиных силах,
        // и называется иначе, чем метод, который ждёт наш код.
        // ...
    }
}

У нового устройства другое имя метода, другая сигнатура и даже другая единица измерения — киловатты вместо лошадиных сил. DynoService рассчитывает работать с интерфейсом вроде measure(string $model): int, и просто подставить LegacyDynoDevice вместо DynoStandConnection не получится.

Переписывать DynoService под каждое новое устройство — плохая идея: клиентский код обрастёт условиями под конкретные вендоры и перестанет быть переиспользуемым.

Решение

Адаптер — это объект-переходник: он реализует интерфейс, которого ждёт клиентский код, а внутри хранит несовместимый объект и переводит вызовы в его собственный формат.

Сначала выделим интерфейс, которого ждёт клиент — раньше DynoService работал напрямую с DynoStandConnection, теперь он должен зависеть от абстракции:

interface DynoStand
{
    public function measure(string $model): int; // л.с.
}

Свой DynoStandConnection мы можем просто пометить как реализацию этого интерфейса — менять в нём ничего не нужно, он и так возвращает то, что нужно, под тем же именем метода. А вот для стороннего устройства пишем адаптер:

class LegacyDynoAdapter implements DynoStand
{
    public function __construct(
        private readonly LegacyDynoDevice $legacyDevice,
    ) {}
 
    public function measure(string $model): int
    {
        $kilowatts = $this->legacyDevice->readPowerKw($model);
 
        return (int) round($kilowatts * 1.35962); // кВт → л.с.
    }
}

Адаптер решает сразу две проблемы несовместимости: разное имя метода (readPowerKwmeasure) и разные единицы измерения (кВт → л.с.). Клиентский код при этом не меняется вообще — он работает с интерфейсом DynoStand и не знает, откуда взялась реализация:

class DynoService
{
    public function __construct(
        private readonly DynoStand $stand,
    ) {}
 
    public function measure(string $model): int
    {
        return $this->stand->measure($model);
    }
}
 
$ownStand = new DynoService(DynoStandConnection::getInstance());
$legacyStand = new DynoService(new LegacyDynoAdapter(new LegacyDynoDevice()));

Появится третий вендор со своим SDK — понадобится ещё один адаптер, но ни DynoStand, ни DynoService трогать не придётся.

Из чего состоит паттерн

  • Целевой интерфейс (DynoStand) — интерфейс, с которым работает клиентский код.
  • Адаптируемый класс (LegacyDynoDevice) — существующий класс с несовместимым интерфейсом, который нельзя или не нужно менять.
  • Адаптер (LegacyDynoAdapter) — реализует целевой интерфейс и хранит внутри себя адаптируемый объект, переводя вызовы в его формат.
  • Клиент (DynoService) — работает только через целевой интерфейс и не знает о существовании адаптируемого класса.

Объектный адаптер vs классовый адаптер

В классическом GoF-описании есть два варианта: классовый адаптер — через множественное наследование от целевого интерфейса и адаптируемого класса одновременно, и объектный адаптер — через композицию, как в примере выше.

В PHP классовый адаптер в чистом виде невозможен — класс не может унаследовать реализацию сразу от двух классов. Поэтому на практике в PHP почти всегда используется именно объектный адаптер: он не только единственный доступный вариант, но и более гибкий — один и тот же адаптер работает с любым наследником LegacyDynoDevice, а не только с этим конкретным классом.

Адаптер vs Декоратор

Оба паттерна оборачивают один объект другим, поэтому их легко перепутать — но у них разные задачи:

АдаптерДекоратор
Интерфейс обёрткиОтличается от интерфейса адаптируемого объектаСовпадает с интерфейсом оборачиваемого объекта
ЦельСделать несовместимое совместимымДобавить поведение, не меняя контракт
Когда применяетсяОдин раз, при интеграции конкретного несовместимого классаМожет применяться многократно, в любом сочетании

Если после оборачивания объект по-прежнему выглядит для клиента как LegacyDynoDevice с новыми возможностями — это Декоратор. Если объект начинает выглядеть как нечто совершенно другое, чего изначально не было в его интерфейсе, — это Адаптер.

Когда применять

  • Нужно использовать существующий класс (часто сторонний или устаревший), интерфейс которого не совпадает с тем, что ожидает остальной код.
  • Требуется постепенно мигрировать с одного API на другой, не переписывая весь клиентский код разом.
  • Несколько похожих по смыслу, но разных по интерфейсу источников данных нужно унифицировать под одну абстракцию.

Плюсы и минусы

Адаптер изолирует несовместимость в одном месте, соблюдает принцип открытости/закрытости — новый вендор добавляется новым адаптером, без изменения существующего кода — и позволяет клиентскому коду оставаться простым и не знать о деталях интеграции.

Минус — это дополнительный уровень косвенности: вместо прямого вызова появляется прослойка, а в системе с большим количеством вендоров адаптеров может стать больше, чем самого бизнес-кода. Если несовместимый интерфейс — это на самом деле собственный код проекта, иногда правильнее не писать адаптер, а просто исправить сам интерфейс.

Итог

Адаптер нужен ровно тогда, когда несовместимость навязана извне — сторонней библиотекой, легаси-кодом или внешним API, которые нельзя изменить напрямую. Он не добавляет новой функциональности и не решает архитектурных проблем — он лишь переводит один интерфейс в другой, оставляя обе стороны нетронутыми.