Adapter: подружить несовместимых
Adapter: подружить несовместимых
Внешний SDK почти никогда не говорит на том же языке, что и твой код. Adapter - это переходник, который снимает эту боль без переписывания всего вокруг.
Проблема
Ты подключил SDK для отправки SMS. У него метод Push(phone, msg) (int, error). А твой код ожидает интерфейс Send(to, text) error. Имена не совпадают, сигнатуры разные, возвращаемые типы другие.
Менять свой код под каждый SDK - плохая идея. Менять SDK - невозможно. Нужен переходник.
Решение: 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 - через поле структуры. Это и есть рекомендуемый подход.
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 + Factory - фабрика создаёт нужный адаптер по конфигу
Adapter + Decorator - адаптер меняет интерфейс, декоратор добавляет поведение
Adapter + DI - адаптер инжектится как реализация интерфейса
Adapter - это практическое воплощение DIP: домен объявляет свой интерфейс, адаптер живёт на периферии и переводит его в вызовы внешнего SDK.
Мини-задание
- Оберни 1 внешний сервис (HTTP-клиент, SDK) адаптером со своим интерфейсом
- Напиши мок для своего интерфейса и используй его в тесте вместо реального SDK
- Посмотри как
http.HandlerFuncадаптирует функцию в интерфейс - напиши аналогичный адаптер для своего кейса - Попробуй выстроить цепочку адаптеров через
io.Reader(файл → gzip → bufio)