BackendBit

К списку паттернов

Adapter позволяет конвертировать интерфейс класса в интерфейс, который ожидает клиент. Другими словами, паттерн решает проблему несовместимости интерфейсов.

UML диаграмма классов паттерна АдаптерПоказывает интерфейс Messenger, класс VK из сторонней библиотеки и класс-адаптер VKAdapter, который реализует Messenger и оборачивает VK.<<interface>>Messenger+sendMessage(UserId, String)VKAdapter+sendMessage(UserId, String)VK+sendMessage(String, String)Конвертирует вызовв формат VKКласс из стороннейбиблиотекиОборачивает

Пример

Предположим, что у нас есть приложение, которое должно присылать пользователям сообщения через различные сторонние мессенджеры.

Функционал мессенджера представлен интерфейсом:

package messenger;

public interface Messenger {
    void sendMessage(UserId recipientId, String text);
}
java

Где UserId – это value-объект для идентификатора пользователя:

package messenger;

public record UserId(String value) {
    public UserId {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException("UserId must not be blank");
        }
    }
}
java

Предположим, что мы хотим добавить в наше приложение интеграцию с VK. Мы поискали готовые решения на GitHub и нашли хорошую библиотеку, которая предоставляет готовую обёртку над API VK. В том числе там есть возможность отправить сообщение.

package vk.sdk;

import java.net.http.HttpClient;

public final class VK {
    private final HttpClient httpClient;

    public VK(HttpClient httpClient) {
        this.httpClient = httpClient;
    }

    public void sendMessage(String chatId, String text) {
        // Отправка сообщения через VK API
        // httpClient.send(...)
    }

    // Остальные методы библиотеки опущены для краткости
}
java

Мы видим, что у библиотечного класса VK есть метод sendMessage, который выглядит как то, что нам нужно.

Однако есть проблема – класс VK по очевидным причинам не соответствует интерфейсу Messenger из нашего приложения.

Здесь нам поможет класс-адаптер:

package messenger;

import vk.sdk.VK;

public final class VKAdapter implements Messenger {
    private final VK vk;

    public VKAdapter(VK vk) {
        this.vk = vk;
    }

    @Override
    public void sendMessage(UserId recipientId, String text) {
        vk.sendMessage(recipientId.value(), text);
    }
}
java

Задача класса-адаптера – сделать так, чтобы класс VK соответствовал нашему интерфейсу. Он выполняет эту задачу следующим образом:

  1. Сам имплементирует интерфейс Messenger.
  2. Оборачивает класс VK из сторонней библиотеки и конвертирует запрос на отправку сообщения в формат, «понятный» методу этого класса.

Поскольку код нашего приложения зависит от интерфейса Messenger, мы можем подставить объект класса VKAdapter и всё будет работать.

Реальные примеры

InputStreamReader

InputStreamReader – пример адаптера в стандартной библиотеке Java. Он адаптирует байтовый интерфейс InputStream к символьному интерфейсу Reader.

Несовместимость очевидна: InputStream читает сырые байты, а Reader – символы. InputStreamReader выступает мостом между ними, декодируя байты в символы с помощью указанной кодировки.

// InputStream – байтовый интерфейс (адаптируемый класс)
try (InputStream byteStream = new FileInputStream("data.txt");
     // InputStreamReader – адаптер: конвертирует байтовый интерфейс в символьный
     Reader reader = new InputStreamReader(byteStream, StandardCharsets.UTF_8);
     // BufferedReader работает с интерфейсом Reader
     BufferedReader buffered = new BufferedReader(reader)) {
    String line = buffered.readLine();
}
java

Клиентский код (BufferedReader) ожидает Reader и ничего не знает о том, что данные изначально поступают в виде байтов. Адаптер берёт на себя преобразование.

По тому же принципу работает OutputStreamWriter – обратный адаптер, который конвертирует символьный интерфейс Writer в байтовый OutputStream.

Arrays.asList()

Arrays.asList() адаптирует массив фиксированного размера к интерфейсу List.

String[] array = {"один", "два", "три"};

// Arrays.asList() – адаптер: конвертирует массив в интерфейс List
List<String> list = Arrays.asList(array);

System.out.println(list.get(0));          // один
System.out.println(list.size());          // 3
System.out.println(list.contains("два")); // true
java

http.HandlerFunc

В Go функции – это first-class citizen, поэтому адаптером может быть не структура, а именованный функциональный тип.

Интерфейс http.Handler требует метод ServeHTTP(ResponseWriter, *Request). Тип http.HandlerFunc адаптирует обычную функцию к этому интерфейсу.

package main

import (
    "log"
    "net/http"
)

// Обычная функция – не реализует интерфейс http.Handler
func healthCheck(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("ok"))
}

func main() {
    // http.HandlerFunc – адаптер: конвертирует функцию в интерфейс http.Handler
    var handler http.Handler = http.HandlerFunc(healthCheck)

    http.Handle("/health", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}
go

Вот как HandlerFunc определён в стандартной библиотеке:

// Определение из стандартной библиотеки Go
type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}
go

HandlerFunc – это именованный тип функции с методом ServeHTTP, который просто вызывает саму функцию. Это элегантный и идиоматичный для Go способ адаптировать сигнатуру функции к интерфейсу.

slog-zerolog

Библиотека slog-zerolog – ещё один пример адаптера в Go. Она адаптирует логгер zerolog к интерфейсу slog.Handler из стандартной библиотеки.

Несовместимость здесь в том, что zerolog имеет свой собственный API для записи логов, а пакет log/slog ожидает реализацию интерфейса Handler. Адаптер берёт на себя преобразование вызовов slog в вызовы zerolog.

Этот пример подробно разобран в статье о принципе инверсии зависимостей, где адаптер позволяет высокоуровневому коду не зависеть от конкретной библиотеки логгирования.