Adapter i Facade to dwa wzorce które rozwiązują problem złożoności interfejsów – ale z różnych stron. Adapter sprawia że niekompatybilny interfejs staje się kompatybilny. Facade upraszcza złożony interfejs do prostego. Oba pojawiają się często przy integracji z zewnętrznymi bibliotekami i API.
Adapter
Adapter konwertuje interfejs jednej klasy na interfejs którego oczekuje klient. To „przejściówka” która sprawia że dwie niekompatybilne klasy mogą ze sobą współpracować.
<?php
declare(strict_types=1);
// Target - interfejs którego oczekuje nasz kod
interface LoggerInterface
{
public function log(string $level, string $message, array $context = []): void;
public function info(string $message, array $context = []): void;
public function error(string $message, array $context = []): void;
public function debug(string $message, array $context = []): void;
}
// Adaptee - zewnętrzna biblioteka z innym interfejsem (której nie możemy zmienić)
class LegacyLogger
{
public function write(int $severity, string $msg): void
{
$levels = [1 => 'DEBUG', 2 => 'INFO', 3 => 'ERROR'];
$label = $levels[$severity] ?? 'UNKNOWN';
echo "[{$label}] {$msg}\n";
}
public function writeWithTimestamp(int $severity, string $msg, string $timestamp): void
{
echo "[{$timestamp}] [{$severity}] {$msg}\n";
}
}
// Adapter - opakowuje LegacyLogger i udostępnia LoggerInterface
class LegacyLoggerAdapter implements LoggerInterface
{
private const LEVELS = [
'debug' => 1,
'info' => 2,
'error' => 3,
];
public function __construct(
private LegacyLogger $legacyLogger
) {}
public function log(string $level, string $message, array $context = []): void
{
$severity = self::LEVELS[strtolower($level)] ?? 2;
$msg = $this->interpolate($message, $context);
$this->legacyLogger->write($severity, $msg);
}
public function info(string $message, array $context = []): void
{
$this->log('info', $message, $context);
}
public function error(string $message, array $context = []): void
{
$this->log('error', $message, $context);
}
public function debug(string $message, array $context = []): void
{
$this->log('debug', $message, $context);
}
private function interpolate(string $message, array $context): string
{
foreach ($context as $key => $value) {
$message = str_replace("{{$key}}", (string) $value, $message);
}
return $message;
}
}
// Klient - używa LoggerInterface, nie wie że pod spodem jest LegacyLogger
class OrderService
{
public function __construct(
private LoggerInterface $logger
) {}
public function placeOrder(int $orderId): void
{
$this->logger->info('Placing order {id}', ['id' => $orderId]);
try {
// logika zamówienia
$this->logger->debug('Order processed', ['order_id' => $orderId]);
} catch (\Exception $e) {
$this->logger->error('Order failed: {message}', ['message' => $e->getMessage()]);
}
}
}
// Podmiana loggera przez adapter - zero zmian w OrderService
$adapter = new LegacyLoggerAdapter(new LegacyLogger());
$service = new OrderService($adapter);
$service->placeOrder(42);
Adapter z dziedziczeniem zamiast kompozycji (Class Adapter) – mniej elastyczny, ale prostszy gdy adaptee nie ma interfejsu:
<?php
// Adapter przez dziedziczenie (Class Adapter)
// Działa gdy możemy dziedziczyć po Adaptee
class LegacyLoggerClassAdapter extends LegacyLogger implements LoggerInterface
{
public function log(string $level, string $message, array $context = []): void
{
$severity = match(strtolower($level)) {
'debug' => 1,
'error' => 3,
default => 2,
};
$this->write($severity, $message); // wywołanie metody rodzica
}
public function info(string $message, array $context = []): void { $this->log('info', $message); }
public function error(string $message, array $context = []): void { $this->log('error', $message); }
public function debug(string $message, array $context = []): void { $this->log('debug', $message); }
}
Facade
Facade zapewnia uproszczony interfejs do złożonego podsystemu. Nie ukrywa podsystemu – klient nadal może go używać bezpośrednio – ale oferuje wygodniejszy punkt wejścia dla typowych przypadków użycia.
<?php
declare(strict_types=1);
// Złożony podsystem - wiele klas z których każda robi jedną rzecz
class ProductLoader
{
public function load(int $id): array
{
echo "Ładowanie produktu {$id}\n";
return ['id' => $id, 'name' => 'Produkt', 'price' => 99.99, 'stock' => 5];
}
}
class StockChecker
{
public function isAvailable(int $productId, int $qty): bool
{
echo "Sprawdzanie stanu magazynowego dla produktu {$productId}\n";
return true;
}
}
class PriceCalculator
{
public function calculate(array $product, int $qty, ?string $coupon): float
{
echo "Obliczanie ceny\n";
$price = $product['price'] * $qty;
if ($coupon === 'DISCOUNT10') {
$price *= 0.9;
}
return $price;
}
}
class CartManager
{
private array $items = [];
public function addItem(array $product, int $qty, float $price): void
{
echo "Dodawanie do koszyka\n";
$this->items[] = ['product' => $product, 'qty' => $qty, 'price' => $price];
}
public function getItems(): array { return $this->items; }
}
class OrderCreator
{
public function create(array $cartItems, string $customerEmail): int
{
echo "Tworzenie zamówienia dla {$customerEmail}\n";
return rand(1000, 9999);
}
}
class PaymentProcessor
{
public function charge(int $orderId, float $amount, string $method): bool
{
echo "Przetwarzanie płatności {$amount} PLN dla zamówienia {$orderId}\n";
return true;
}
}
class EmailSender
{
public function sendConfirmation(string $email, int $orderId): void
{
echo "Wysyłanie potwierdzenia do {$email} dla zamówienia {$orderId}\n";
}
}
<?php
// BEZ Facade - klient musi orkiestrować cały podsystem
$loader = new ProductLoader();
$checker = new StockChecker();
$calculator = new PriceCalculator();
$cart = new CartManager();
$creator = new OrderCreator();
$payment = new PaymentProcessor();
$email = new EmailSender();
$product = $loader->load(42);
if (!$checker->isAvailable(42, 2)) {
throw new \RuntimeException('Out of stock');
}
$price = $calculator->calculate($product, 2, 'DISCOUNT10');
$cart->addItem($product, 2, $price);
$orderId = $creator->create($cart->getItems(), 'jan@example.com');
$payment->charge($orderId, $price, 'card');
$email->sendConfirmation('jan@example.com', $orderId);
// To samo w każdym miejscu gdzie składasz zamówienie - duplikacja logiki
<?php
// Facade - jeden spójny interfejs dla całego procesu
class OrderFacade
{
public function __construct(
private ProductLoader $productLoader,
private StockChecker $stockChecker,
private PriceCalculator $priceCalculator,
private CartManager $cartManager,
private OrderCreator $orderCreator,
private PaymentProcessor $paymentProcessor,
private EmailSender $emailSender
) {}
// Prosta metoda dla typowego przypadku użycia
public function placeOrder(
int $productId,
int $qty,
string $customerEmail,
string $paymentMethod = 'card',
?string $coupon = null
): int {
$product = $this->productLoader->load($productId);
if (!$this->stockChecker->isAvailable($productId, $qty)) {
throw new \RuntimeException("Product {$productId} out of stock");
}
$price = $this->priceCalculator->calculate($product, $qty, $coupon);
$this->cartManager->addItem($product, $qty, $price);
$orderId = $this->orderCreator->create(
$this->cartManager->getItems(),
$customerEmail
);
$success = $this->paymentProcessor->charge($orderId, $price, $paymentMethod);
if (!$success) {
throw new \RuntimeException('Payment failed');
}
$this->emailSender->sendConfirmation($customerEmail, $orderId);
return $orderId;
}
}
// Klient - jeden prosty call zamiast orkiestracji 7 klas
$facade = new OrderFacade(/* wstrzyknięte zależności */);
$orderId = $facade->placeOrder(42, 2, 'jan@example.com', 'card', 'DISCOUNT10');
echo "Zamówienie #{$orderId} złożone\n";
Adapter vs Facade – różnica
| Aspekt | Adapter | Facade |
|---|---|---|
| Cel | Konwersja interfejsu – „zrób to kompatybilne” | Uproszczenie interfejsu – „zrób to łatwiejsze” |
| Liczba klas | Zwykle jedna klasa adaptowana | Wiele klas podsystemu |
| Dostęp do podsystemu | Ukrywa adaptee za interfejsem | Nie ukrywa – klient może nadal używać klas bezpośrednio |
| Typowe użycie | Integracja z zewnętrzną biblioteką | Uproszczenie złożonego API |
Podsumowanie
Adapter i Facade to wzorce które ratują czytelność kodu przy integracji z zewnętrznym światem. Adapter pojawia się naturalnie gdy zmieniasz bibliotekę (np. z własnego loggera na Monolog) i nie chcesz zmieniać całego kodu – piszesz adapter. Facade pojawia się gdy masz złożony podsystem i chcesz dać klientom prostszy punkt wejścia bez ukrywania możliwości bezpośredniego dostępu.
