Skip to content
intermediate Phase 10 · More Design Patterns

Decorator Pattern

Decorator pattern for adding behavior dynamically, with Magento pricing and totals decorator examples

45m
0 problems
Topic Progress 0%

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?

Question 1 options

2. A Decorator must:

Question 2 options

3. In Magento, price modifiers like discounts and taxes are examples of:

Question 3 options

Flashcards

Question

What does the Decorator pattern do?

Answer

Adds behavior to objects dynamically while maintaining the same interface

Question

Decorator vs Inheritance?

Answer

Decorator = runtime composition (flexible); Inheritance = compile-time (rigid)

Question

What must a Decorator implement?

Answer

The same interface as the component it decorates

Question

Magento example of Decorators?

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