Адаптер

Адаптер — это структурный паттерн проектирования, который позволяет объектам с несовместимыми интерфейсами работать вместе, не изменяя их код.
Это первый структурный паттерн в серии — до этого Фабричный метод, Абстрактная фабрика, Строитель, Прототип и Одиночка отвечали на вопрос «как создать объект». Структурные паттерны отвечают на другой вопрос — «как из существующих объектов и классов собрать более крупную структуру».
Проблема
В статье про Одиночку 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); // кВт → л.с.
}
}Адаптер решает сразу две проблемы несовместимости: разное имя метода (readPowerKw → measure) и разные единицы измерения (кВт → л.с.). Клиентский код при этом не меняется вообще — он работает с интерфейсом 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, которые нельзя изменить напрямую. Он не добавляет новой функциональности и не решает архитектурных проблем — он лишь переводит один интерфейс в другой, оставляя обе стороны нетронутыми.