Блог инженера

History is written by its contributors

Adapter Pattern в Go: совместимость интерфейсов

2025-06-15 время чтения 8 мин Patterns Ilya Brin

Adapter Pattern решает проблему несовместимости интерфейсов. У вас есть код, который ожидает один интерфейс, и библиотека, которая предоставляет другой. Адаптер — это прослойка, которая делает их совместимыми.

Проблема: несовместимые интерфейсы

Ваш код работает с интерфейсом Logger:

type Logger interface {
    Log(level, message string)
}

Но сторонняя библиотека предоставляет другой интерфейс:

type ZapLogger struct{}

func (z *ZapLogger) Info(msg string) {}
func (z *ZapLogger) Error(msg string) {}
func (z *ZapLogger) Debug(msg string) {}

Интерфейсы несовместимы. Нужен адаптер.

Решение: адаптер

type ZapLoggerAdapter struct {
    logger *ZapLogger
}

func (a *ZapLoggerAdapter) Log(level, message string) {
    switch level {
    case "info":
        a.logger.Info(message)
    case "error":
        a.logger.Error(message)
    case "debug":
        a.logger.Debug(message)
    }
}

Теперь ZapLogger совместим с интерфейсом Logger.

Реальный пример 1: миграция с Logrus на Zap

У вас legacy-код с Logrus:

type LogrusLogger struct {
    logger *logrus.Logger
}

func (l *LogrusLogger) Info(msg string) {
    l.logger.Info(msg)
}

Новый код использует стандартный интерфейс:

type AppLogger interface {
    Log(ctx context.Context, level Level, msg string)
}

Адаптер для Logrus:

type LogrusAdapter struct {
    logger *logrus.Logger
}

func (a *LogrusAdapter) Log(ctx context.Context, level Level, msg string) {
    entry := a.logger.WithContext(ctx)
    
    switch level {
    case LevelInfo:
        entry.Info(msg)
    case LevelError:
        entry.Error(msg)
    case LevelDebug:
        entry.Debug(msg)
    }
}

Адаптер для Zap:

type ZapAdapter struct {
    logger *zap.Logger
}

func (a *ZapAdapter) Log(ctx context.Context, level Level, msg string) {
    switch level {
    case LevelInfo:
        a.logger.Info(msg)
    case LevelError:
        a.logger.Error(msg)
    case LevelDebug:
        a.logger.Debug(msg)
    }
}

Теперь можно переключаться между логгерами без изменения кода:

var logger AppLogger

if useZap {
    logger = &ZapAdapter{logger: zap.NewProduction()}
} else {
    logger = &LogrusAdapter{logger: logrus.New()}
}

logger.Log(ctx, LevelInfo, "Application started")

Реальный пример 2: интеграция платёжных систем

Ваше приложение работает с интерфейсом Payment:

type Payment interface {
    Charge(amount int, currency string) (string, error)
    Refund(transactionID string) error
}

Stripe API:

type StripeClient struct{}

func (s *StripeClient) CreateCharge(params ChargeParams) (*Charge, error) {
    // Stripe-специфичная логика
}

func (s *StripeClient) CreateRefund(chargeID string) (*Refund, error) {
    // Stripe-специфичная логика
}

Адаптер для Stripe:

type StripeAdapter struct {
    client *StripeClient
}

func (a *StripeAdapter) Charge(amount int, currency string) (string, error) {
    charge, err := a.client.CreateCharge(ChargeParams{
        Amount:   amount,
        Currency: currency,
    })
    if err != nil {
        return "", err
    }
    return charge.ID, nil
}

func (a *StripeAdapter) Refund(transactionID string) error {
    _, err := a.client.CreateRefund(transactionID)
    return err
}

PayPal API другой:

type PayPalClient struct{}

func (p *PayPalClient) ProcessPayment(req PaymentRequest) (*PaymentResponse, error) {
    // PayPal-специфичная логика
}

func (p *PayPalClient) RefundPayment(paymentID string) error {
    // PayPal-специфичная логика
}

Адаптер для PayPal:

type PayPalAdapter struct {
    client *PayPalClient
}

func (a *PayPalAdapter) Charge(amount int, currency string) (string, error) {
    resp, err := a.client.ProcessPayment(PaymentRequest{
        Amount:   amount,
        Currency: currency,
    })
    if err != nil {
        return "", err
    }
    return resp.TransactionID, nil
}

func (a *PayPalAdapter) Refund(transactionID string) error {
    return a.client.RefundPayment(transactionID)
}

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

func ProcessOrder(payment Payment, amount int) error {
    transactionID, err := payment.Charge(amount, "USD")
    if err != nil {
        return err
    }
    
    // Если что-то пошло не так
    if needRefund {
        return payment.Refund(transactionID)
    }
    
    return nil
}

// Работает с любым провайдером
stripe := &StripeAdapter{client: stripeClient}
paypal := &PayPalAdapter{client: paypalClient}

ProcessOrder(stripe, 1000)
ProcessOrder(paypal, 1000)

Реальный пример 3: работа с разными базами данных

Ваш интерфейс репозитория:

type UserRepository interface {
    FindByID(ctx context.Context, id string) (*User, error)
    Save(ctx context.Context, user *User) error
}

PostgreSQL с sqlx:

type PostgresUserRepo struct {
    db *sqlx.DB
}

func (r *PostgresUserRepo) FindByID(ctx context.Context, id string) (*User, error) {
    var user User
    err := r.db.GetContext(ctx, &user, "SELECT * FROM users WHERE id = $1", id)
    return &user, err
}

func (r *PostgresUserRepo) Save(ctx context.Context, user *User) error {
    _, err := r.db.ExecContext(ctx, 
        "INSERT INTO users (id, name, email) VALUES ($1, $2, $3)",
        user.ID, user.Name, user.Email)
    return err
}

MongoDB с официальным драйвером:

type MongoUserRepo struct {
    collection *mongo.Collection
}

func (r *MongoUserRepo) FindByID(ctx context.Context, id string) (*User, error) {
    var user User
    err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&user)
    return &user, err
}

func (r *MongoUserRepo) Save(ctx context.Context, user *User) error {
    _, err := r.collection.InsertOne(ctx, user)
    return err
}

Оба репозитория реализуют один интерфейс — это и есть адаптер. Можно переключаться между БД:

var repo UserRepository

if useMongo {
    repo = &MongoUserRepo{collection: mongoCollection}
} else {
    repo = &PostgresUserRepo{db: postgresDB}
}

user, err := repo.FindByID(ctx, "123")

Реальный пример 4: кэширование с разными бэкендами

Интерфейс кэша:

type Cache interface {
    Get(key string) ([]byte, error)
    Set(key string, value []byte, ttl time.Duration) error
    Delete(key string) error
}

Redis адаптер:

type RedisCache struct {
    client *redis.Client
}

func (c *RedisCache) Get(key string) ([]byte, error) {
    return c.client.Get(context.Background(), key).Bytes()
}

func (c *RedisCache) Set(key string, value []byte, ttl time.Duration) error {
    return c.client.Set(context.Background(), key, value, ttl).Err()
}

func (c *RedisCache) Delete(key string) error {
    return c.client.Del(context.Background(), key).Err()
}

Memcached адаптер:

type MemcachedCache struct {
    client *memcache.Client
}

func (c *MemcachedCache) Get(key string) ([]byte, error) {
    item, err := c.client.Get(key)
    if err != nil {
        return nil, err
    }
    return item.Value, nil
}

func (c *MemcachedCache) Set(key string, value []byte, ttl time.Duration) error {
    return c.client.Set(&memcache.Item{
        Key:        key,
        Value:      value,
        Expiration: int32(ttl.Seconds()),
    })
}

func (c *MemcachedCache) Delete(key string) error {
    return c.client.Delete(key)
}

In-memory адаптер для тестов:

type InMemoryCache struct {
    data map[string][]byte
    mu   sync.RWMutex
}

func (c *InMemoryCache) Get(key string) ([]byte, error) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    value, ok := c.data[key]
    if !ok {
        return nil, errors.New("key not found")
    }
    return value, nil
}

func (c *InMemoryCache) Set(key string, value []byte, ttl time.Duration) error {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    c.data[key] = value
    return nil
}

func (c *InMemoryCache) Delete(key string) error {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    delete(c.data, key)
    return nil
}

Реальный пример 5: HTTP клиенты

Стандартный интерфейс:

type HTTPClient interface {
    Do(req *http.Request) (*http.Response, error)
}

Стандартный http.Client уже реализует этот интерфейс. Но что если нужен клиент с retry?

type RetryHTTPClient struct {
    client  *http.Client
    retries int
}

func (c *RetryHTTPClient) Do(req *http.Request) (*http.Response, error) {
    var resp *http.Response
    var err error
    
    for i := 0; i <= c.retries; i++ {
        resp, err = c.client.Do(req)
        if err == nil && resp.StatusCode < 500 {
            return resp, nil
        }
        
        if i < c.retries {
            time.Sleep(time.Second * time.Duration(i+1))
        }
    }
    
    return resp, err
}

Или клиент с метриками:

type MetricsHTTPClient struct {
    client  HTTPClient
    metrics *prometheus.CounterVec
}

func (c *MetricsHTTPClient) Do(req *http.Request) (*http.Response, error) {
    start := time.Now()
    resp, err := c.client.Do(req)
    duration := time.Since(start)
    
    status := "success"
    if err != nil {
        status = "error"
    }
    
    c.metrics.WithLabelValues(req.Method, status).Inc()
    
    return resp, err
}

Композиция адаптеров:

client := &MetricsHTTPClient{
    client: &RetryHTTPClient{
        client:  http.DefaultClient,
        retries: 3,
    },
    metrics: prometheusMetrics,
}

Реальный пример 6: облачные хранилища

Интерфейс хранилища:

type Storage interface {
    Upload(ctx context.Context, key string, data []byte) error
    Download(ctx context.Context, key string) ([]byte, error)
    Delete(ctx context.Context, key string) error
}

AWS S3 адаптер:

type S3Storage struct {
    client *s3.Client
    bucket string
}

func (s *S3Storage) Upload(ctx context.Context, key string, data []byte) error {
    _, err := s.client.PutObject(ctx, &s3.PutObjectInput{
        Bucket: aws.String(s.bucket),
        Key:    aws.String(key),
        Body:   bytes.NewReader(data),
    })
    return err
}

func (s *S3Storage) Download(ctx context.Context, key string) ([]byte, error) {
    result, err := s.client.GetObject(ctx, &s3.GetObjectInput{
        Bucket: aws.String(s.bucket),
        Key:    aws.String(key),
    })
    if err != nil {
        return nil, err
    }
    defer result.Body.Close()
    
    return io.ReadAll(result.Body)
}

Google Cloud Storage адаптер:

type GCSStorage struct {
    client *storage.Client
    bucket string
}

func (g *GCSStorage) Upload(ctx context.Context, key string, data []byte) error {
    wc := g.client.Bucket(g.bucket).Object(key).NewWriter(ctx)
    defer wc.Close()
    
    _, err := wc.Write(data)
    return err
}

func (g *GCSStorage) Download(ctx context.Context, key string) ([]byte, error) {
    rc, err := g.client.Bucket(g.bucket).Object(key).NewReader(ctx)
    if err != nil {
        return nil, err
    }
    defer rc.Close()
    
    return io.ReadAll(rc)
}

Локальная файловая система для разработки:

type LocalStorage struct {
    basePath string
}

func (l *LocalStorage) Upload(ctx context.Context, key string, data []byte) error {
    path := filepath.Join(l.basePath, key)
    return os.WriteFile(path, data, 0644)
}

func (l *LocalStorage) Download(ctx context.Context, key string) ([]byte, error) {
    path := filepath.Join(l.basePath, key)
    return os.ReadFile(path)
}

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

  1. Интеграция сторонних библиотек — когда их интерфейс не совпадает с вашим
  2. Миграция между технологиями — постепенная замена одной библиотеки на другую
  3. Абстракция инфраструктуры — независимость от конкретных реализаций
  4. Мультитенантность — поддержка разных провайдеров для разных клиентов
  5. Тестирование — создание mock-адаптеров для тестов

Adapter vs Facade

Adapter делает один интерфейс совместимым с другим. Facade упрощает сложный интерфейс.

// Adapter: преобразование интерфейса
type LoggerAdapter struct {
    logger *ThirdPartyLogger
}

func (a *LoggerAdapter) Log(msg string) {
    a.logger.WriteLog(msg, time.Now())
}

// Facade: упрощение интерфейса
type DatabaseFacade struct {
    conn *sql.DB
    cache *redis.Client
    queue *kafka.Producer
}

func (f *DatabaseFacade) SaveUser(user *User) error {
    // Скрывает сложность работы с БД, кэшем и очередью
}

Производительность

Адаптер добавляет один уровень косвенности. Overhead минимален:

func BenchmarkDirect(b *testing.B) {
    logger := &ZapLogger{}
    for i := 0; i < b.N; i++ {
        logger.Info("test")
    }
}

func BenchmarkAdapter(b *testing.B) {
    adapter := &ZapLoggerAdapter{logger: &ZapLogger{}}
    for i := 0; i < b.N; i++ {
        adapter.Log("info", "test")
    }
}

Разница обычно в пределах 1-2 наносекунд.

Заключение

Adapter Pattern в Go:

  • Используйте для интеграции сторонних библиотек
  • Применяйте при миграции между технологиями
  • Создавайте адаптеры для тестирования
  • Абстрагируйте инфраструктуру через адаптеры
  • Комбинируйте адаптеры для расширения функциональности

Адаптер — это не усложнение. Это гибкость и независимость от конкретных реализаций.

В современных приложениях адаптеры везде: логгеры, базы данных, кэши, платёжные системы, облачные сервисы. Они делают код переносимым и тестируемым.

comments powered by Disqus