Adapter: подружить несовместимых

Adapter: подружить несовместимых

Внешний SDK почти никогда не говорит на том же языке, что и твой код. Adapter - это переходник, который снимает эту боль без переписывания всего вокруг.

Проблема

Ты подключил SDK для отправки SMS. У него метод Push(phone, msg) (int, error). А твой код ожидает интерфейс Send(to, text) error. Имена не совпадают, сигнатуры разные, возвращаемые типы другие.

Менять свой код под каждый SDK - плохая идея. Менять SDK - невозможно. Нужен переходник.

Решение: Adapter

Adapter - обёртка, которая транслирует один интерфейс в другой. Как переходник с европейской розетки на американскую: внутри ток тот же, но вилка подходит.

Adapter как переходник между интерфейсами

Адаптер над внешним SMS API

// Наш интерфейс - так мы хотим работать с SMS
type SmsSender interface {
    Send(to, text string) error
}

// Внешний клиент (из чужого SDK) - не наш интерфейс
type ThirdPartySmsClient struct{}
func (c *ThirdPartySmsClient) Push(phone string, msg string) (int, error) {
    // отправка через внешний API
    return 200, nil
}

// Адаптер - переводит наш интерфейс в вызовы SDK
type SmsAdapter struct {
    client *ThirdPartySmsClient
}

func (a *SmsAdapter) Send(to, text string) error {
    code, err := a.client.Push(to, text)
    if err != nil {
        return fmt.Errorf("sms send failed: %w", err)
    }
    if code != 200 {
        return fmt.Errorf("sms api returned code %d", code)
    }
    return nil
}

Теперь бизнес-логика зависит от SmsSender, а не от конкретного SDK - классическая иллюстрация DIP:

type NotificationService struct {
    sms SmsSender  // интерфейс, а не конкретный клиент
}

func (s *NotificationService) NotifyUser(phone, msg string) error {
    return s.sms.Send(phone, msg)
}

Сменили SDK? Написали новый адаптер - остальной код не трогаем.

// Наш интерфейс - так мы хотим работать с SMS
interface SmsSender
{
    public function send(string $to, string $text): void;
}

// Внешний клиент (из чужого SDK) - не наш интерфейс
final class ThirdPartySmsClient
{
    public function push(string $phone, string $msg): int
    {
        return 200;
    }
}

// Адаптер - переводит наш интерфейс в вызовы SDK
final class SmsAdapter implements SmsSender
{
    public function __construct(
        private readonly ThirdPartySmsClient $client,
    ) {}

    public function send(string $to, string $text): void
    {
        $code = $this->client->push($to, $text);
        if ($code !== 200) {
            throw new RuntimeException("sms api returned code $code");
        }
    }
}

Бизнес-логика зависит от интерфейса, а не от SDK:

final class NotificationService
{
    public function __construct(
        private readonly SmsSender $sms,
    ) {}

    public function notifyUser(string $phone, string $msg): void
    {
        $this->sms->send($phone, $msg);
    }
}

Сменили SDK? Написали новый адаптер - NotificationService не трогаем.

// Внешний клиент (из чужого SDK) - не наш интерфейс
class ThirdPartySmsClient {
  push(phone, msg) {
    // отправка через внешний API
    return 200;
  }
}

// Адаптер - переводит наш контракт send(to, text) в вызовы SDK
class SmsAdapter {
  #client;
  constructor(client) {
    this.#client = client;
  }

  send(to, text) {
    const code = this.#client.push(to, text);
    if (code !== 200) {
      throw new Error(`sms api returned code ${code}`);
    }
  }
}

Бизнес-логика зависит от объекта с методом send, а не от SDK:

class NotificationService {
  #sms;
  constructor(sms) { this.#sms = sms; }

  notifyUser(phone, msg) {
    this.#sms.send(phone, msg);
  }
}

const service = new NotificationService(new SmsAdapter(new ThirdPartySmsClient()));
service.notifyUser('+79001112233', 'Welcome!');

Сменили SDK? Написали новый адаптер - NotificationService не трогаем. В JS из-за duck-typing адаптер часто пишут как лёгкую обёртку-объект без класса: const adapter = { send: (to, text) => client.push(to, text) }. Класс полезен, когда у адаптера есть собственное состояние (счётчики, кеш).

Object Adapter vs Class Adapter

В классических языках есть два варианта:

Object Adapter (композиция)Class Adapter (наследование)
Адаптер содержит объект adapteeАдаптер наследует от adaptee
adapter.client.Push(...)super.Push(...)
Гибче: можно менять adapteeЖёстче: привязан к конкретному классу
Работает в Go ✅Не работает в Go ❌ (нет наследования)

В Go всегда Object Adapter - через поле структуры. Это и есть рекомендуемый подход.

Каждый раз, когда ты оборачиваешь внешнюю зависимость своим [интерфейсом](../go/09-interfaces.md) - ты пишешь Adapter. Это один из самых частых паттернов в Go backend-разработке.

Adapter в Go stdlib

os.File           реализует io.Reader, io.Writer, io.Closer
 - адаптирует системные вызовы ОС под стандартные интерфейсы

bytes.NewReader   принимает []byte и возвращает io.Reader
 - адаптирует слайс байт под интерфейс чтения

http.HandlerFunc  адаптирует функцию func(w, r) под интерфейс http.Handler
 - не нужно создавать структуру с методом ServeHTTP

http.HandlerFunc - элегантный пример: это адаптер, который превращает простую функцию в полноценный http.Handler. В PHP той же идее соответствует invokable class через __invoke - он удовлетворяет любому контракту с одним методом (например, PSR-15 RequestHandlerInterface):

// Без адаптера - нужна структура
type myHandler struct{}
func (h myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello"))
}

// С адаптером HandlerFunc - достаточно функции
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello"))
}))
<?php
declare(strict_types=1);

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

// Без адаптера - нужен отдельный класс
final class HelloHandler implements RequestHandlerInterface
{
    public function __construct(
        private readonly ResponseFactory $responses,
    ) {}

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        return $this->responses->text('hello');
    }
}

// С адаптером - обычная функция-замыкание превращается в Handler
final class CallableHandler implements RequestHandlerInterface
{
    /** @param callable(ServerRequestInterface): ResponseInterface $fn */
    public function __construct(
        private $fn,
    ) {}

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        return ($this->fn)($request);
    }
}

$router->add('/', new CallableHandler(
    fn (ServerRequestInterface $r): ResponseInterface => $responses->text('hello'),
));
// Express-стиль: маршрут принимает callback (req, res) - это и есть адаптер
// функции под интерфейс «обработчик»
app.get('/hello', (req, res) => {
  res.send('hello');
});

// Если нужен класс-обработчик, оборачиваем его в адаптер-замыкание
class HelloHandler {
  handle(req, res) {
    res.send('hello');
  }
}

const handler = new HelloHandler();
app.get('/hello', (req, res) => handler.handle(req, res));

Composability: адаптеры складываются

В Go адаптеры легко комбинируются благодаря интерфейсам:

// Оборачиваем io.Reader несколькими адаптерами
file, _ := os.Open("data.gz")       // io.Reader (файл)
gzReader, _ := gzip.NewReader(file)  // io.Reader (распаковка)
scanner := bufio.NewScanner(gzReader) // построчное чтение

// Каждый слой адаптирует предыдущий - и всё через io.Reader

Это возможно потому что каждый адаптер принимает и возвращает один и тот же интерфейс (io.Reader). Такая цепочка - основа Go I/O.

Когда НЕ использовать

Интерфейсы совпадают - если внешний SDK уже реализует нужный интерфейс, адаптер лишний.

Одноразовый вызов - если ты дёргаешь SDK в одном месте и не собираешься его менять, прямой вызов проще.

Adapter ≠ всё переименовать - адаптер не просто меняет имена методов. Он транслирует логику: преобразует типы, обрабатывает ошибки, маппит коды ответов.

Adapter подгоняет ОДИН интерфейс под другой. Facade упрощает доступ к НЕСКОЛЬКИМ подсистемам. Если ты оборачиваешь один SDK - это Adapter. Если координируешь 3 сервиса - это Facade.

Связь с другими паттернами

Adapter + Factory - фабрика создаёт нужный адаптер по конфигу
Adapter + Decorator - адаптер меняет интерфейс, декоратор добавляет поведение
Adapter + DI - адаптер инжектится как реализация интерфейса

Adapter - это практическое воплощение DIP: домен объявляет свой интерфейс, адаптер живёт на периферии и переводит его в вызовы внешнего SDK.

Мини-задание

  • Оберни 1 внешний сервис (HTTP-клиент, SDK) адаптером со своим интерфейсом
  • Напиши мок для своего интерфейса и используй его в тесте вместо реального SDK
  • Посмотри как http.HandlerFunc адаптирует функцию в интерфейс - напиши аналогичный адаптер для своего кейса
  • Попробуй выстроить цепочку адаптеров через io.Reader (файл → gzip → bufio)

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.