Decorator Pattern
Tilføjer dynamisk ny funktionalitet til objekter ved at wrappe dem i decorator objekter, uden at ændre deres interface.
Om Patterned
Decorator Pattern er et strukturelt design pattern introduceret af Gang of Four i 1994, der giver en fleksibel alternativ til subclassing for at udvide funktionalitet. Patterned fungerer ved at wrappe objekter i decorator klasser der implementerer samme interface som det originale objekt, hvilket tillader stacking af multiple decorators for at kombinere behaviors. Dette er fundamentalt når du har brug for at tilføje funktionalitet til objekter dynamisk runtime uden at ændre deres kildekode eller skabe eksplosive arvshierarkier. Decorator Pattern bruges extensively i PHP frameworks såsom Symfony's HttpKernel middleware stack og PSR-7 HTTP message decorators. Patterned fremmer Single Responsibility Principle ved at opdele funktionalitet i små, genbrugelige komponenter der kan kombineres på forskellige måder efter behov.
Key Points
- ✓Tilføjer funktionalitet dynamisk til objekter
- ✓Alternative til subclassing for at udvide behavior
- ✓Decorators implementerer samme interface som objekter de wrapper
- ✓Multiple decorators kan stables for at kombinere behaviors
- ✓Fremmer Single Responsibility Principle
- ✓Open/Closed Principle - udvid uden at modificere
- ✓Decorators er transparente for klienten
- ✓Hver decorator tilføjer én specifik funktionalitet
- ✓Kan føre til mange små klasser
- ✓Rækkefølgen af decorators kan være vigtig
- ✓Bruges ofte i middleware patterns
- ✓PHP 8 attributes kan bruges som metadata decorators
Kode Eksempel
<?php
declare(strict_types=1);
namespace App\Coffee;
/**
* Component Interface
*/
interface CoffeeInterface
{
public function getDescription(): string;
public function getCost(): float;
public function getCalories(): int;
}
/**
* Concrete Component - Base Coffee
*/
final class Espresso implements CoffeeInterface
{
public function getDescription(): string
{
return 'Espresso';
}
public function getCost(): float
{
return 25.00;
}
public function getCalories(): int
{
return 5;
}
}
/**
* Concrete Component - Filter Coffee
*/
final class FilterCoffee implements CoffeeInterface
{
public function getDescription(): string
{
return 'Filter Coffee';
}
public function getCost(): float
{
return 20.00;
}
public function getCalories(): int
{
return 2;
}
}
/**
* Abstract Decorator
*/
abstract class CoffeeDecorator implements CoffeeInterface
{
public function __construct(
protected readonly CoffeeInterface $coffee
) {}
public function getDescription(): string
{
return $this->coffee->getDescription();
}
public function getCost(): float
{
return $this->coffee->getCost();
}
public function getCalories(): int
{
return $this->coffee->getCalories();
}
}
/**
* Concrete Decorator - Milk
*/
final class MilkDecorator extends CoffeeDecorator
{
public function getDescription(): string
{
return $this->coffee->getDescription() . ', Milk';
}
public function getCost(): float
{
return $this->coffee->getCost() + 5.00;
}
public function getCalories(): int
{
return $this->coffee->getCalories() + 50;
}
}
/**
* Concrete Decorator - Whipped Cream
*/
final class WhippedCreamDecorator extends CoffeeDecorator
{
public function getDescription(): string
{
return $this->coffee->getDescription() . ', Whipped Cream';
}
public function getCost(): float
{
return $this->coffee->getCost() + 8.00;
}
public function getCalories(): int
{
return $this->coffee->getCalories() + 100;
}
}
/**
* Concrete Decorator - Caramel Syrup
*/
final class CaramelDecorator extends CoffeeDecorator
{
public function getDescription(): string
{
return $this->coffee->getDescription() . ', Caramel Syrup';
}
public function getCost(): float
{
return $this->coffee->getCost() + 6.00;
}
public function getCalories(): int
{
return $this->coffee->getCalories() + 80;
}
}
/**
* Concrete Decorator - Extra Shot
*/
final class ExtraShotDecorator extends CoffeeDecorator
{
public function getDescription(): string
{
return $this->coffee->getDescription() . ', Extra Shot';
}
public function getCost(): float
{
return $this->coffee->getCost() + 10.00;
}
public function getCalories(): int
{
return $this->coffee->getCalories() + 5;
}
}
/**
* Helper function til at printe ordre
*/
function printOrder(CoffeeInterface $coffee): void
{
echo "Order: {$coffee->getDescription()}\n";
echo "Cost: DKK {$coffee->getCost()}\n";
echo "Calories: {$coffee->getCalories()} kcal\n";
echo str_repeat('-', 50) . "\n";
}
// Brug af Decorator Pattern
echo "Coffee Shop Orders\n";
echo str_repeat('=', 50) . "\n\n";
// Simple espresso
$order1 = new Espresso();
printOrder($order1);
// Espresso med mælk
$order2 = new MilkDecorator(new Espresso());
printOrder($order2);
// Latte (espresso + milk + extra shot)
$order3 = new ExtraShotDecorator(
new MilkDecorator(
new Espresso()
)
);
printOrder($order3);
// Caramel Macchiato (espresso + milk + caramel + whipped cream)
$order4 = new WhippedCreamDecorator(
new CaramelDecorator(
new MilkDecorator(
new Espresso()
)
)
);
printOrder($order4);
// Filter coffee med alt
$order5 = new WhippedCreamDecorator(
new CaramelDecorator(
new ExtraShotDecorator(
new MilkDecorator(
new FilterCoffee()
)
)
)
);
printOrder($order5);
Fordele
- +Fleksibel alternative til subclassing
- +Tilføj/fjern funktionalitet dynamisk runtime
- +Single Responsibility - hver decorator har én opgave
- +Open/Closed Principle - udvid uden at modificere eksisterende kode
- +Kombiner behaviors ved at stable decorators
- +Fremmer code reuse - decorators kan bruges på forskellige objekter
Ulemper
- −Kan resultere i mange små klasser
- −Decorators stack kan blive svær at debugge
- −Rækkefølgen af decorators kan være kritisk
- −Instantiering kan blive verbose ved mange decorators
Hvornår bruges det?
- •HTTP middleware - tilføj authentication, logging, caching lag til requests
- •Logging - tilføj timestamps, context, formatting til log messages
- •Data transformation - tilføj encryption, compression, serialization lag
- •UI components - tilføj borders, scrollbars, shadows til GUI elements
- •Stream processing - tilføj buffering, filtering, encoding til data streams
- •Cache layers - wrap services med caching functionality
Best Practices
- ✓Brug readonly properties til at gemme wrapped component
- ✓Definer clear interface som alle decorators implementerer
- ✓Overvej abstract base decorator class for common functionality
- ✓Dokumenter om rækkefølgen af decorators betyder noget
- ✓Brug beskrivende navne på decorators (f.eks. CachingDecorator)
- ✓Overvej Builder Pattern til at simplificere decorator stacking
- ✓Test decorators både individuelt og i kombinationer
- ✓Vær opmærksom på performance overhead ved mange lag
Quick Info
- • Adapter Pattern - ændrer interface vs tilføjer funktionalitet
- • Proxy Pattern - kontrollerer adgang vs udvider behavior
- • Chain of Responsibility - lignende chaining men med different purpose