The Decorator Pattern Explained
Definition
The Decorator pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Problem: Extending Price Calculation
// Base price calculator
class SimplePriceCalculator implements PriceCalculatorInterface
{
public function calculate(ProductInterface $product): float
{
return $product->getPrice();
}
}
// Need: discount, tax, promotional pricing
// Bad: inheritance chain
class DiscountedPrice extends SimplePriceCalculator { /* ... */ }
class TaxedDiscountedPrice extends DiscountedPrice { /* ... */ }
class PromotionalTaxedDiscountedPrice extends TaxedDiscountedPrice { /* ... */ }
// 4+ classes for 3 behaviors = class explosion
Solution: Decorators
namespace Vendor\Catalog\Pricing;
interface PriceCalculatorInterface
{
public function calculate(ProductInterface $product): float;
}
// Base implementation
class BasePriceCalculator implements PriceCalculatorInterface
{
public function calculate(ProductInterface $product): float
{
return $product->getPrice();
}
}
// Abstract decorator
taxclass AbstractPriceDecorator implements PriceCalculatorInterface
{
public function __construct(
protected PriceCalculatorInterface $inner
) {}
public function calculate(ProductInterface $product): float
{
return $this->inner->calculate($product);
}
}
// Concrete decorators
class DiscountDecorator extends AbstractPriceDecorator
{
public function __construct(
PriceCalculatorInterface $inner,
private float $discountPercent
) {
parent::__construct($inner);
}
public function calculate(ProductInterface $product): float
{
$price = $this->inner->calculate($product);
return $price * (1 - $this->discountPercent / 100);
}
}
class TaxDecorator extends AbstractPriceDecorator
{
public function __construct(
PriceCalculatorInterface $inner,
private float $taxRate
) {
parent::__construct($inner);
}
public function calculate(ProductInterface $product): float
{
$price = $this->inner->calculate($product);
return $price * (1 + $this->taxRate);
}
}
// Compose decorators at runtime
$calculator = new BasePriceCalculator();
$calculator = new DiscountDecorator($calculator, 10); // 10% off
$calculator = new TaxDecorator($calculator, 0.20); // 20% tax
$finalPrice = $calculator->calculate($product); // Price * 0.9 * 1.2
Each decorator adds one behavior. They're stacked dynamically — no inheritance needed.
How Decorators Differ from Inheritance
Inheritance: Static, Compile-Time
// Static: behavior determined at class definition time
class DiscountedTaxedProduct extends Product
{
// Always has discount and tax
// Cannot remove either without new class
}
Decorator: Dynamic, Runtime
// Dynamic: behavior determined at runtime
$product = new Product();
$decorated = new DiscountDecorator($product, 10);
// Or without discount:
$decorated = new TaxDecorator($product, 0.20);
// Compose any combination at runtime
Key Differences
| Aspect | Inheritance | Decorator |
|---|---|---|
| Coupling | Tight (compile-time) | Loose (runtime) |
| Flexibility | Fixed at class definition | Any combination at runtime |
| Composition | Single parent | Multiple decorators stacked |
| Open/Closed | Requires new classes | New decorators extend without modify |
| Transparency | Caller must know subclass | Caller sees same interface |
When Inheritance is Better
- When behavior is truly fixed and won't change
- When there are only 2-3 variations
- When performance is critical (decorators add method call overhead)
Magento Pricing and Totals Decorators
Magento's Price Rendering
Magento uses the Decorator pattern extensively in its price rendering pipeline. The PriceRenderer interface is the base, and each modifier decorates the price:
// Magento\Pricing\Price\PriceInterface
interface PriceInterface
{
public function getValue();
public function getBasePrice();
}
// Magento\Pricing\Price\Modifier\DiscountInterface
interface DiscountInterface extends PriceInterface
{
public function getDiscountAmount();
}
// Price modifiers act as decorators
class TierPrice implements PriceInterface
{
public function __construct(
private PriceInterface $basePrice,
private array $tierPrices
) {}
public function getValue()
{
$baseValue = $this->basePrice->getValue();
$lowestTier = $this->getLowestTierPrice($baseValue);
return min($baseValue, $lowestTier);
}
}
Shopping Cart Totals
The cart totals calculation is a decorator chain:
// Each total modifier decorates the previous result
interface TotalInterface
{
public function getValue(): float;
public function getLabel(): string;
}
class SubTotal implements TotalInterface
{
public function getValue(): float
{
return $this->calculateItemsTotal();
}
}
class DiscountTotal implements TotalInterface
{
public function __construct(
private TotalInterface $inner
) {}
public function getValue(): float
{
return -$this->inner->getValue() * $this->getDiscountPercent();
}
}
class TaxTotal implements TotalInterface
{
public function __construct(
private TotalInterface $inner
) {}
public function getValue(): float
{
return $this->inner->getValue() * $this->getTaxRate();
}
}
// Final order: SubTotal → Discount → Tax → Grand Total
$totals = new SubTotal();
$totals = new DiscountTotal($totals);
$totals = new TaxTotal($totals);
Magento's Magento\Sales\Model\Order\Total configuration determines the order of these decorators.
Quiz
1. What is the key advantage of Decorator over inheritance?
2. A Decorator must:
3. In Magento, price modifiers like discounts and taxes are examples of:
Flashcards
Question
What does the Decorator pattern do?
Click to reveal answer
Answer
Adds behavior to objects dynamically while maintaining the same interface
Question
Decorator vs Inheritance?
Click to reveal answer
Answer
Decorator = runtime composition (flexible); Inheritance = compile-time (rigid)
Question
What must a Decorator implement?
Click to reveal answer
Answer
The same interface as the component it decorates
Question
Magento example of Decorators?
Click to reveal answer
Answer
Price modifiers: discounts, taxes, tier pricing wrap base price
Revision Notes
Key Takeaways
- 1. Decorator adds behavior dynamically without modifying the original class
- 2. Decorators implement the same interface as the component they wrap
- 3. Stacking decorators: each wraps the previous, adding one behavior
- 4. Magento uses decorators for pricing pipeline and cart totals
- 5. Decorators are more flexible than inheritance but add slight overhead
Interview Tips
- • Explain how stacking decorators works (each wraps the previous)
- • Give Magento example: BasePrice → DiscountDecorator → TaxDecorator
- • Distinguish Decorator from Adapter (Adapter converts interface, Decorator adds behavior)
Cheat Sheet
Decorator Pattern:
Interface → PriceInterface
Base → BasePriceCalculator
Decorator → DiscountDecorator, TaxDecorator
Stack: $calc = new TaxDecorator(new DiscountDecorator(new BasePriceCalculator()));
Key Rule: Decorator implements SAME interface as component
Magento: Price modifiers, cart totals, order totals
Decorator vs Adapter:
Decorator → Same interface, adds behavior
Adapter → Different interfaces, converts