Adapter/Wrapper
A pattern that allows incompatible interfaces to work together by wrapping one interface to match another.
Contributors
When to Use
- When integrating third-party libraries with incompatible interfaces
- When standardizing interfaces across different implementations
- When you want to swap implementations without changing client code
When NOT to Use
- When you control both interfaces and can change them directly
- For simple one-to-one method calls without transformation
- When the performance overhead of wrapping is unacceptable
The Adapter pattern allows incompatible interfaces to work together by creating a wrapper that translates one interface into another. In Go, this is commonly achieved using interface implementation and composition.
Implementation
package storage
import (
"context"
"errors"
)
// Storage is our application's interface
type Storage interface {
Get(ctx context.Context, key string) ([]byte, error)
Set(ctx context.Context, key string, value []byte) error
}
// RedisClient represents a third-party Redis library
type RedisClient struct {
addr string
}
func (r *RedisClient) GET(key string) (string, error) {
// Redis-specific implementation
return "", nil
}
func (r *RedisClient) SET(key, value string) error {
return nil
}
// RedisAdapter adapts RedisClient to our Storage interface
type RedisAdapter struct {
client *RedisClient
}
func NewRedisAdapter(client *RedisClient) Storage {
return &RedisAdapter{client: client}
}
func (a *RedisAdapter) Get(ctx context.Context, key string) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
value, err := a.client.GET(key)
if err != nil {
return nil, err
}
return []byte(value), nil
}
func (a *RedisAdapter) Set(ctx context.Context, key string, value []byte) error {
if err := ctx.Err(); err != nil {
return err
}
return a.client.SET(key, string(value))
}
// MemoryCache is another third-party implementation
type MemoryCache struct {
data map[string][]byte
}
func (m *MemoryCache) Read(key string) ([]byte, bool) {
val, ok := m.data[key]
return val, ok
}
func (m *MemoryCache) Write(key string, value []byte) {
m.data[key] = value
}
// MemoryAdapter adapts MemoryCache to our Storage interface
type MemoryAdapter struct {
cache *MemoryCache
}
func NewMemoryAdapter(cache *MemoryCache) Storage {
return &MemoryAdapter{cache: cache}
}
func (a *MemoryAdapter) Get(ctx context.Context, key string) ([]byte, error) {
value, ok := a.cache.Read(key)
if !ok {
return nil, errors.New("key not found")
}
return value, nil
}
func (a *MemoryAdapter) Set(ctx context.Context, key string, value []byte) error {
a.cache.Write(key, value)
return nil
}
Usage
package main
import (
"context"
)
type Application struct {
storage storage.Storage
}
func NewApplication(storage storage.Storage) *Application {
return &Application{storage: storage}
}
func (app *Application) SaveUser(ctx context.Context, userID string, data []byte) error {
return app.storage.Set(ctx, userID, data)
}
func main() {
ctx := context.Background()
// Use Redis in production
redisClient := &storage.RedisClient{addr: "localhost:6379"}
prodApp := NewApplication(storage.NewRedisAdapter(redisClient))
prodApp.SaveUser(ctx, "user1", []byte("data"))
// Use memory cache in tests
memCache := &storage.MemoryCache{data: make(map[string][]byte)}
testApp := NewApplication(storage.NewMemoryAdapter(memCache))
testApp.SaveUser(ctx, "user1", []byte("data"))
}
Benefits
- Interface Compatibility: Makes incompatible interfaces work together
- Flexibility: Easy to swap implementations without changing client code
- Testability: Simplifies testing by allowing mock implementations