MidТеория3 min

Adapter

Паттерн Adapter в Go: адаптация интерфейсов, http.HandlerFunc и обёртки для сторонних библиотек

Adapter (Адаптер)

Проблема

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

В Go Adapter встречается повсеместно -- это один из самых часто используемых паттернов. Классический пример: http.HandlerFunc.

Диаграмма

    Клиент ожидает:           Есть:
    +----------+              +-----------+
    | Notifier |              | SlackAPI  |
    +----------+              +-----------+
    | Send()   |              | PostMsg() |
    +----------+              +-----------+

    Adapter:
    +------------------+
    | SlackAdapter     |
    +------------------+
    | api: *SlackAPI   |
    | Send() --> api.PostMsg()
    +------------------+

Реализация: Структурный адаптер

package notification

import "context"

// Notifier is the interface our application expects.
type Notifier interface {
    Send(ctx context.Context, to, subject, body string) error
}

// --- External Slack API (third-party, we can't change it) ---

type SlackAPI struct {
    token   string
    baseURL string
}

func NewSlackAPI(token string) *SlackAPI {
    return &SlackAPI{token: token, baseURL: "https://slack.com/api"}
}

func (s *SlackAPI) PostMessage(channel, text string) error {
    // External API call...
    return nil
}

// --- Adapter wraps SlackAPI to satisfy Notifier ---

type slackAdapter struct {
    api     *SlackAPI
    channel string
}

// NewSlackNotifier adapts SlackAPI to the Notifier interface.
func NewSlackNotifier(api *SlackAPI, channel string) Notifier {
    return &slackAdapter{api: api, channel: channel}
}

func (a *slackAdapter) Send(_ context.Context, _, subject, body string) error {
    text := subject + "\n" + body
    return a.api.PostMessage(a.channel, text)
}

Реализация: Функциональный адаптер

Самый элегантный пример в Go -- http.HandlerFunc. Это тип-функция, реализующий интерфейс.

package http

// Handler is the interface.
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

// HandlerFunc is a function adapter for Handler.
// Any function with the right signature becomes a Handler.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

Используем:

// Any function can be an http.Handler through the adapter
http.Handle("/health", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
}))

Свой функциональный адаптер

package notification

import "context"

// NotifyFunc is a function adapter for Notifier.
type NotifyFunc func(ctx context.Context, to, subject, body string) error

// Send implements Notifier by calling the function.
func (f NotifyFunc) Send(ctx context.Context, to, subject, body string) error {
    return f(ctx, to, subject, body)
}

// Usage: any matching function becomes a Notifier
var logNotifier Notifier = NotifyFunc(func(ctx context.Context, to, subject, body string) error {
    slog.InfoContext(ctx, "notification",
        "to", to,
        "subject", subject,
    )
    return nil
})

Реальный пример: адаптация нескольких сервисов

package notification

import "context"

// --- Email service adapter ---

type SMTPClient struct {
    host string
    port int
}

func (c *SMTPClient) SendEmail(from, to, subject, html string) error {
    // SMTP logic...
    return nil
}

type emailAdapter struct {
    client *SMTPClient
    from   string
}

func NewEmailNotifier(client *SMTPClient, from string) Notifier {
    return &emailAdapter{client: client, from: from}
}

func (a *emailAdapter) Send(_ context.Context, to, subject, body string) error {
    return a.client.SendEmail(a.from, to, subject, body)
}

// --- Telegram bot adapter ---

type TelegramBot struct {
    token string
}

func (b *TelegramBot) SendMessage(chatID int64, text string, parseMode string) error {
    // Telegram API call...
    return nil
}

type telegramAdapter struct {
    bot    *TelegramBot
    chatID int64
}

func NewTelegramNotifier(bot *TelegramBot, chatID int64) Notifier {
    return &telegramAdapter{bot: bot, chatID: chatID}
}

func (a *telegramAdapter) Send(_ context.Context, _, subject, body string) error {
    text := "*" + subject + "*\n" + body
    return a.bot.SendMessage(a.chatID, text, "Markdown")
}

// --- Multi-notifier uses all adapters uniformly ---

type multiNotifier struct {
    notifiers []Notifier
}

// NewMultiNotifier sends to all notification channels.
func NewMultiNotifier(notifiers ...Notifier) Notifier {
    return &multiNotifier{notifiers: notifiers}
}

func (m *multiNotifier) Send(ctx context.Context, to, subject, body string) error {
    for _, n := range m.notifiers {
        if err := n.Send(ctx, to, subject, body); err != nil {
            return err
        }
    }
    return nil
}

Использование:

func main() {
    notifier := notification.NewMultiNotifier(
        notification.NewEmailNotifier(smtpClient, "[email protected]"),
        notification.NewSlackNotifier(slackAPI, "#alerts"),
        notification.NewTelegramNotifier(tgBot, chatID),
    )

    // One interface for all notification channels
    notifier.Send(ctx, "[email protected]", "Alert", "Server is down!")
}

Примеры из стандартной библиотеки

// http.HandlerFunc -- function adapter
http.HandlerFunc(myFunc)

// sort.Interface -- adapts sort algorithm to custom types
sort.Sort(sort.Reverse(mySlice))

// io.ReadCloser from io.Reader + io.Closer
io.NopCloser(reader) // adapts Reader to ReadCloser

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

Используйте, когда:

  • Интеграция со сторонними библиотеками
  • Нужно привести разные API к единому интерфейсу
  • Функция должна удовлетворять интерфейсу (функциональный адаптер)
  • Миграция с одной библиотеки на другую

Не используйте, когда:

  • Можно изменить исходный интерфейс
  • Адаптация слишком сложная (возможно, нужен другой паттерн)
  • Один-к-одному маппинг без трансформации (просто используйте интерфейс)

Сравнение с похожими паттернами

Паттерн Отличие
Decorator Добавляет поведение, сохраняя интерфейс; Adapter меняет интерфейс
Facade Упрощает сложный API; Adapter приводит один интерфейс к другому
Proxy Тот же интерфейс + контроль доступа; Adapter меняет интерфейс

Проверь себя

Какой тип в стандартной библиотеке Go является классическим примером функционального адаптера?

Как в Go создать функциональный адаптер?

В чём разница между Adapter и Decorator?