Adapter (Адаптер)
Adapter позволяет конвертировать интерфейс класса в интерфейс, который ожидает клиент. Другими словами, паттерн решает проблему несовместимости интерфейсов.
Пример
Предположим, что у нас есть приложение, которое должно присылать пользователям сообщения через различные сторонние мессенджеры.
Функционал мессенджера представлен интерфейсом:
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 соответствовал нашему интерфейсу. Он выполняет эту задачу следующим образом:
- Сам имплементирует интерфейс
Messenger. - Оборачивает класс
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("два")); // truejavahttp.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)
}goHandlerFunc – это именованный тип функции с методом ServeHTTP, который просто вызывает саму функцию. Это элегантный и идиоматичный для Go способ адаптировать сигнатуру функции к интерфейсу.
slog-zerolog
Библиотека slog-zerolog – ещё один пример адаптера в Go. Она адаптирует логгер zerolog к интерфейсу slog.Handler из стандартной библиотеки.
Несовместимость здесь в том, что zerolog имеет свой собственный API для записи логов, а пакет log/slog ожидает реализацию интерфейса Handler. Адаптер берёт на себя преобразование вызовов slog в вызовы zerolog.
Этот пример подробно разобран в статье о принципе инверсии зависимостей, где адаптер позволяет высокоуровневому коду не зависеть от конкретной библиотеки логгирования.