Decorator i Proxy to dwa wzorce strukturalne które działają podobnie – oba opakowują obiekt i implementują ten sam interfejs. Różni je intencja: Decorator dodaje nową funkcjonalność, Proxy kontroluje dostęp do obiektu. Subtelna różnica, ale fundamentalna dla prawidłowego zastosowania.
Decorator
Decorator „opakowuje” obiekt w kolejne warstwy funkcjonalności. Każda warstwa dodaje coś nowego, a klient widzi ten sam interfejs przez wszystkie warstwy.
<?php
declare(strict_types=1);
// Interfejs komponentu
interface TextFormatterInterface
{
public function format(string $text): string;
}
// Komponent bazowy - podstawowa implementacja
class PlainTextFormatter implements TextFormatterInterface
{
public function format(string $text): string
{
return $text;
}
}
// Bazowy dekorator - implementuje interfejs i opakowuje komponent
abstract class TextDecorator implements TextFormatterInterface
{
public function __construct(
protected TextFormatterInterface $formatter
) {}
public function format(string $text): string
{
return $this->formatter->format($text);
}
}
// Konkretne dekoratory
class UpperCaseDecorator extends TextDecorator
{
public function format(string $text): string
{
return strtoupper(parent::format($text));
}
}
class TrimDecorator extends TextDecorator
{
public function format(string $text): string
{
return trim(parent::format($text));
}
}
class HtmlEscapeDecorator extends TextDecorator
{
public function format(string $text): string
{
return htmlspecialchars(parent::format($text), ENT_QUOTES, 'UTF-8');
}
}
class WrapInTagDecorator extends TextDecorator
{
public function __construct(
TextFormatterInterface $formatter,
private string $tag,
private string $class = ''
) {
parent::__construct($formatter);
}
public function format(string $text): string
{
$formatted = parent::format($text);
$class = $this->class ? " class=\"{$this->class}\"" : '';
return "<{$this->tag}{$class}>{$formatted}{$this->tag}>";
}
}
<?php
// Składanie dekoratorów - kolejność ma znaczenie
$userInput = ' <script>alert("xss")</script> ';
// Najpierw trim, potem escape, potem wrap w paragraf
$formatter = new WrapInTagDecorator(
new HtmlEscapeDecorator(
new TrimDecorator(
new PlainTextFormatter()
)
),
tag: 'p',
class: 'user-content'
);
echo $formatter->format($userInput);
// <p class="user-content"><script>alert("xss")</script></p>
// Inna kombinacja - uppercase + wrap w nagłówek
$headingFormatter = new WrapInTagDecorator(
new UpperCaseDecorator(
new TrimDecorator(
new PlainTextFormatter()
)
),
tag: 'h1'
);
echo $headingFormatter->format(' witaj świecie ');
// <h1>WITAJ ŚWIECIE</h1>
Praktyczny przykład z loggingiem – Decorator dodaje logowanie do istniejącego serwisu bez jego modyfikacji:
<?php
declare(strict_types=1);
interface UserRepositoryInterface
{
public function findById(int $id): ?array;
public function save(array $user): int;
public function delete(int $id): bool;
}
class DatabaseUserRepository implements UserRepositoryInterface
{
public function findById(int $id): ?array { /* ... */ return ['id' => $id, 'name' => 'Jan']; }
public function save(array $user): int { /* ... */ return 1; }
public function delete(int $id): bool { /* ... */ return true; }
}
// Dekorator dodający logowanie - zero zmian w DatabaseUserRepository
class LoggingUserRepository implements UserRepositoryInterface
{
public function __construct(
private UserRepositoryInterface $repository,
private \Psr\Log\LoggerInterface $logger
) {}
public function findById(int $id): ?array
{
$this->logger->debug("UserRepository::findById({$id})");
$result = $this->repository->findById($id);
$this->logger->debug('Result: ' . ($result ? 'found' : 'not found'));
return $result;
}
public function save(array $user): int
{
$this->logger->info('Saving user', ['name' => $user['name'] ?? 'unknown']);
$id = $this->repository->save($user);
$this->logger->info("User saved with ID: {$id}");
return $id;
}
public function delete(int $id): bool
{
$this->logger->warning("Deleting user {$id}");
$result = $this->repository->delete($id);
$this->logger->info("User {$id} deleted: " . ($result ? 'ok' : 'failed'));
return $result;
}
}
Proxy
Proxy stawia pośrednika między klientem a prawdziwym obiektem. Pośrednik implementuje ten sam interfejs i może: opóźniać inicjalizację (lazy), kontrolować dostęp (protection), keszować wyniki (caching), logować wywołania (logging).
<?php
declare(strict_types=1);
interface ImageInterface
{
public function display(): void;
public function getSize(): int;
}
// Prawdziwy obiekt - ciężka inicjalizacja (ładowanie pliku z dysku)
class RealImage implements ImageInterface
{
private string $imageData;
public function __construct(private string $filename)
{
echo "Ładowanie pliku: {$filename}\n"; // symulacja ciężkiej operacji
$this->imageData = file_get_contents($filename) ?: '';
}
public function display(): void
{
echo "Wyświetlam: {$this->filename} (" . strlen($this->imageData) . " bajtów)\n";
}
public function getSize(): int
{
return strlen($this->imageData);
}
}
// Lazy Proxy - tworzy prawdziwy obiekt dopiero przy pierwszym użyciu
class LazyImageProxy implements ImageInterface
{
private ?RealImage $realImage = null;
public function __construct(
private string $filename
) {
// Konstruktor lekki - nic nie ładuje
echo "Proxy dla: {$filename} (plik nie załadowany)\n";
}
private function load(): RealImage
{
if ($this->realImage === null) {
$this->realImage = new RealImage($this->filename);
}
return $this->realImage;
}
public function display(): void
{
$this->load()->display(); // ładuje plik dopiero tutaj
}
public function getSize(): int
{
return $this->load()->getSize();
}
}
<?php
// Proxy kontrolujące dostęp - Protection Proxy
class SecureUserRepository implements UserRepositoryInterface
{
public function __construct(
private UserRepositoryInterface $repository,
private string $currentUserRole
) {}
public function findById(int $id): ?array
{
// Każdy może czytać
return $this->repository->findById($id);
}
public function save(array $user): int
{
// Zapis tylko dla adminów i edytorów
if (!in_array($this->currentUserRole, ['admin', 'editor'], true)) {
throw new \RuntimeException('Access denied: cannot save user');
}
return $this->repository->save($user);
}
public function delete(int $id): bool
{
// Usuwanie tylko dla adminów
if ($this->currentUserRole !== 'admin') {
throw new \RuntimeException('Access denied: cannot delete user');
}
return $this->repository->delete($id);
}
}
// Proxy keszujące - Caching Proxy
class CachingUserRepository implements UserRepositoryInterface
{
private array $cache = [];
public function __construct(
private UserRepositoryInterface $repository
) {}
public function findById(int $id): ?array
{
if (!isset($this->cache[$id])) {
$this->cache[$id] = $this->repository->findById($id);
}
return $this->cache[$id];
}
public function save(array $user): int
{
$id = $this->repository->save($user);
unset($this->cache[$id]); // unieważnij cache po zapisie
return $id;
}
public function delete(int $id): bool
{
$result = $this->repository->delete($id);
unset($this->cache[$id]);
return $result;
}
}
Decorator vs Proxy – kluczowa różnica
| Aspekt | Decorator | Proxy |
|---|---|---|
| Cel | Dodaje nową funkcjonalność | Kontroluje dostęp do istniejącej |
| Inicjalizacja obiektu | Dostaje gotowy obiekt z zewnątrz | Może sam tworzyć opakowany obiekt |
| Liczba warstw | Wiele – warstwy się składają | Zwykle jedna |
| Typowe użycie | Logging, formatowanie, walidacja | Lazy loading, cache, auth |
Podsumowanie
Decorator i Proxy to eleganckie wzorce które pozwalają dodawać zachowanie do obiektów bez modyfikacji ich kodu. Decorator skaluje się przez składanie warstw – możesz mieć dziesięć dekoratorów na jednym obiekcie. Proxy jest bardziej wyspecjalizowane – zazwyczaj jeden pośrednik z konkretnym zadaniem: opóźnienie inicjalizacji, cache albo kontrola uprawnień.
