← Tilbage til patterns

Adapter Pattern

Konverterer en klasses interface til et andet interface som klienter forventer, hvilket tillader klasser med inkompatible interfaces at arbejde sammen.

StructuralMellem

Om Patterned

Adapter Pattern, også kendt som Wrapper Pattern, er et strukturelt design pattern introduceret af Gang of Four i 1994, der løser problemet med interface incompatibility mellem klasser. Patterned fungerer ved at skabe en mellemliggende adapter klasse der oversætter kald fra klientens forventede interface til det interface som den adapterede klasse faktisk tilbyder. Dette er fundamentalt når du arbejder med tredjepartsbiblioteker, legacy code eller eksterne APIs hvor du ikke kan eller vil ændre den eksisterende kode, men har brug for at integrere den i dit system. Adapter Pattern fremmer loose coupling ved at isolere klienten fra den konkrete implementation og gør det muligt at skifte implementations uden at påvirke klient kode. I moderne PHP udvikling bruges adapters extensively til at wrappe eksterne libraries, database drivers, cache providers og API clients bag ensartede interfaces.

Key Points

  • Konverterer mellem inkompatible interfaces
  • Også kendt som Wrapper Pattern
  • To varianter: Class Adapter (arv) og Object Adapter (komposition)
  • Object Adapter er preferred i PHP (komposition over arv)
  • Gør legacy code kompatibel med nyt system
  • Isolerer klient fra tredjepartsbiblioteker
  • Fremmer loose coupling
  • Single Responsibility - adapter fokuserer kun på interface translation
  • Open/Closed Principle - tilføj adapters uden at modificere eksisterende kode
  • Kan tilføje ekstra funktionalitet under oversættelsen
  • Bruges ofte ved migration mellem libraries
  • PSR interfaces i PHP er designet til adapter pattern

Kode Eksempel

<?php

declare(strict_types=1);

namespace App\Cache;

/**
 * Target Interface - Det interface vores applikation forventer
 */
interface CacheInterface
{
    public function get(string $key): mixed;
    public function set(string $key, mixed $value, int $ttl = 3600): bool;
    public function delete(string $key): bool;
    public function clear(): bool;
    public function has(string $key): bool;
}

/**
 * Adaptee - Redis Library (third-party)
 * Dette er biblioteket vi vil adaptere
 */
final class RedisClient
{
    private array $data = []; // Simulation
    
    public function getValue(string $key): mixed
    {
        return $this->data[$key] ?? null;
    }
    
    public function setValue(string $key, mixed $value, int $seconds): void
    {
        $this->data[$key] = $value;
        echo "Redis: Set {$key} with TTL {$seconds}s\n";
    }
    
    public function removeKey(string $key): void
    {
        unset($this->data[$key]);
        echo "Redis: Removed {$key}\n";
    }
    
    public function flushAll(): void
    {
        $this->data = [];
        echo "Redis: Flushed all keys\n";
    }
    
    public function keyExists(string $key): bool
    {
        return isset($this->data[$key]);
    }
}

/**
 * Adaptee - Memcached Library (third-party)
 */
final class MemcachedClient
{
    private array $storage = []; // Simulation
    
    public function fetch(string $key): mixed
    {
        return $this->storage[$key] ?? false;
    }
    
    public function store(string $key, mixed $value, int $expiration): bool
    {
        $this->storage[$key] = $value;
        echo "Memcached: Stored {$key} with expiration {$expiration}s\n";
        return true;
    }
    
    public function remove(string $key): bool
    {
        unset($this->storage[$key]);
        echo "Memcached: Removed {$key}\n";
        return true;
    }
    
    public function flush(): bool
    {
        $this->storage = [];
        echo "Memcached: Flushed all data\n";
        return true;
    }
}

/**
 * Adapter - Redis to CacheInterface
 */
final readonly class RedisCacheAdapter implements CacheInterface
{
    public function __construct(
        private RedisClient $redis
    ) {}
    
    public function get(string $key): mixed
    {
        return $this->redis->getValue($key);
    }
    
    public function set(string $key, mixed $value, int $ttl = 3600): bool
    {
        $this->redis->setValue($key, $value, $ttl);
        return true;
    }
    
    public function delete(string $key): bool
    {
        $this->redis->removeKey($key);
        return true;
    }
    
    public function clear(): bool
    {
        $this->redis->flushAll();
        return true;
    }
    
    public function has(string $key): bool
    {
        return $this->redis->keyExists($key);
    }
}

/**
 * Adapter - Memcached to CacheInterface
 */
final readonly class MemcachedCacheAdapter implements CacheInterface
{
    public function __construct(
        private MemcachedClient $memcached
    ) {}
    
    public function get(string $key): mixed
    {
        $value = $this->memcached->fetch($key);
        return $value !== false ? $value : null;
    }
    
    public function set(string $key, mixed $value, int $ttl = 3600): bool
    {
        return $this->memcached->store($key, $value, $ttl);
    }
    
    public function delete(string $key): bool
    {
        return $this->memcached->remove($key);
    }
    
    public function clear(): bool
    {
        return $this->memcached->flush();
    }
    
    public function has(string $key): bool
    {
        return $this->memcached->fetch($key) !== false;
    }
}

/**
 * Adapter - Simple File Cache
 */
final class FileCacheAdapter implements CacheInterface
{
    public function __construct(
        private readonly string $cacheDir = '/tmp/cache'
    ) {
        if (!is_dir($this->cacheDir)) {
            mkdir($this->cacheDir, 0755, true);
        }
    }
    
    public function get(string $key): mixed
    {
        $file = $this->getFilePath($key);
        
        if (!file_exists($file)) {
            return null;
        }
        
        $data = unserialize(file_get_contents($file));
        
        if ($data['expires'] < time()) {
            $this->delete($key);
            return null;
        }
        
        return $data['value'];
    }
    
    public function set(string $key, mixed $value, int $ttl = 3600): bool
    {
        $file = $this->getFilePath($key);
        $data = [
            'value' => $value,
            'expires' => time() + $ttl,
        ];
        
        file_put_contents($file, serialize($data));
        echo "FileCache: Saved {$key} with TTL {$ttl}s\n";
        
        return true;
    }
    
    public function delete(string $key): bool
    {
        $file = $this->getFilePath($key);
        
        if (file_exists($file)) {
            unlink($file);
            echo "FileCache: Deleted {$key}\n";
        }
        
        return true;
    }
    
    public function clear(): bool
    {
        $files = glob($this->cacheDir . '/*');
        
        foreach ($files as $file) {
            unlink($file);
        }
        
        echo "FileCache: Cleared all cache\n";
        return true;
    }
    
    public function has(string $key): bool
    {
        return $this->get($key) !== null;
    }
    
    private function getFilePath(string $key): string
    {
        return $this->cacheDir . '/' . md5($key) . '.cache';
    }
}

/**
 * Client Code - Arbejder kun med CacheInterface
 */
final readonly class UserRepository
{
    public function __construct(
        private CacheInterface $cache
    ) {}
    
    public function findById(int $id): ?array
    {
        $key = "user:{$id}";
        
        // Try cache first
        if ($this->cache->has($key)) {
            echo "Cache hit for {$key}\n";
            return $this->cache->get($key);
        }
        
        // Simulate database fetch
        echo "Cache miss for {$key}, fetching from database\n";
        $user = ['id' => $id, 'name' => 'John Doe', 'email' => 'john@example.com'];
        
        // Store in cache
        $this->cache->set($key, $user, 300);
        
        return $user;
    }
}

// Brug af Adapter Pattern
echo "=== Adapter Pattern Demo ===\n\n";

// Kan skifte mellem forskellige cache implementations
echo "--- Using Redis ---\n";
$redisCache = new RedisCacheAdapter(new RedisClient());
$repo1 = new UserRepository($redisCache);
$repo1->findById(1);
echo "\n";

echo "--- Using Memcached ---\n";
$memcachedCache = new MemcachedCacheAdapter(new MemcachedClient());
$repo2 = new UserRepository($memcachedCache);
$repo2->findById(2);
echo "\n";

echo "--- Using File Cache ---\n";
$fileCache = new FileCacheAdapter();
$repo3 = new UserRepository($fileCache);
$repo3->findById(3);

Fordele

  • +Single Responsibility - adapter fokuserer kun på interface conversion
  • +Open/Closed Principle - tilføj nye adapters uden at ændre eksisterende kode
  • +Loose coupling - isolerer klient fra tredjepartsbiblioteker
  • +Fremmer code reuse - samme adapter kan bruges multiple steder
  • +Gør det let at skifte implementations
  • +Beskytter mod breaking changes i eksterne libraries

Ulemper

  • Øget kompleksitet med ekstra abstraction lag
  • Kan føre til performance overhead
  • Flere klasser skal vedligeholdes
  • Adapter kan ikke altid mappet alle features 1:1

Hvornår bruges det?

  • Cache providers - adapt Redis, Memcached, APCu bag fælles interface
  • Payment gateways - uniforme interface til Stripe, PayPal, MobilePay
  • Database drivers - adapt forskellige PDO drivers, MongoDB, etc.
  • Logging - adapt forskellige log destinations (file, syslog, cloud)
  • Email services - uniforme interface til SMTP, SendGrid, Mailgun
  • Legacy code integration - gør gamle systemer kompatible med ny arkitektur

Best Practices

  • Prefer Object Adapter (komposition) over Class Adapter (arv)
  • Brug readonly properties til adaptee i PHP 8+
  • Definer clear target interface baseret på use cases
  • Dokumenter hvilke features fra adaptee der ikke supporteres
  • Test adapters thoroughly da de er critical integration points
  • Overvej PSR interfaces (PSR-3 Logger, PSR-6 Cache, PSR-16 SimpleCache)
  • Hold adapter simple - kompleks logic hører til i andre lag
  • Brug dependency injection til at inject adapters

Quick Info

Kategori
Structural
Sværhedsgrad
Mellem
Relaterede Patterns
  • Decorator Pattern - tilføjer functionality vs ændrer interface
  • Facade Pattern - forenkler komplekst interface vs konverterer interface
  • Proxy Pattern - samme interface vs konverterer til andet interface