Decorator Pattern: Resolve the Mix and Match puzzle

Published on: February 14, 2025

The Problem: A Growing Mess in the Dealership

“IT team, we have another special request. This time, the customer wants Tire & Wheel Protection along with Key Replacement. Can you add that?”

Imagine you're working in a car dealership insurance application. Initially, it starts simple, just computing the basic cost of the insurance and handling a few aftermarket add-ons. But, as always, customers have special requests. Before you know it, you’re drowning in a mess of new classes for every possible coverage combination.

  • One customer wanted the Gap along with the theft protection, so you created a class to handle that requirement.
  • Then another customer wanted the Gap along with the Tire and Wheel protection, once again you made another class to cover that whish.
  • Then, another one wanted the Gap with Theft AND Tire and Wheel, you grew concerned, but you still added another class to handle that case.
  • Finally, yet again this week, you received a call from sales.: a big important customer wants key replacement and tire and wheel protection only and you haven't covered that scenario!
  • Within a few weeks you saw the problem and you know "class explosion" is not a good practice.

This approach results in a bloated codebase that constantly needs modification, violating the Open/Closed Principle (from SOLID). Every new coverage combination requires yet another subclass, making your system rigid and unmanageable.

Naive Approach: A Hardcoded Mess

Let’s see what happens when we try the naive approach:

public class BasicInsurance
{
    // Nice move! Making it virtual allows it to be overridden later.
    public virtual decimal Cost()
    {
        return 100;
    }
}

public class WithGap : BasicInsurance
{
    public override decimal Cost()
    {
        return base.Cost() + 50;
    }
}

public class WithTheftProtection : BasicInsurance
{
    public override decimal Cost()
    {
        return base.Cost() + 30;
    }
}

public class WithTireAndWheelProtection : BasicInsurance
{
    public override decimal Cost()
    {
        return base.Cost() + 20;
    }
}

public class WithKeyReplacementCoverage : BasicInsurance
{
    public override decimal Cost()
    {
        return base.Cost() + 10;
    }
}

public class WithGapAndTheftProtection : BasicInsurance
{
    public override decimal Cost()
    {
        return base.Cost() + 50 + 30;
    }
}

public class WithGapAndTireAndWheelProtection : BasicInsurance
{
    public override decimal Cost()
    {
        return base.Cost() + 50 + 20;
    }
}

public class WithGapTheftTireAndWheelKeyProtection : BasicInsurance
{
    public override decimal Cost()
    {
        return base.Cost() + 50 + 30 + 20 + 10;
    }
}

// ... at this point you see the problem right? The number of combinations is growing exponentially

Your Program.cs will look like this:

Console.WriteLine("Customer 1: Only wants basic car insurance.");
var customerOneInsurance = new BasicInsurance();
Console.WriteLine($"Customer 1 - Insurance Cost: {customerOneInsurance.Cost()}");

Console.WriteLine("Customer 2: Wants basic car insurance + gap coverage.");
var customerTwoInsurance = new WithGap();
Console.WriteLine($"Customer 2 - Insurance Cost: {customerTwoInsurance.Cost()}");

Console.WriteLine("Customer 3: Wants basic car insurance + theft protection.");
var customerThreeInsurance = new WithTheftProtection();
Console.WriteLine($"Customer 3 - Insurance Cost: {customerThreeInsurance.Cost()}");

Console.WriteLine("Customer 4: Wants basic car insurance + tire and wheel protection.");
var customerFourInsurance = new WithTireAndWheelProtection();
Console.WriteLine($"Customer 4 - Insurance Cost: {customerFourInsurance.Cost()}");

Console.WriteLine("Customer 5: Wants basic car insurance + key replacement coverage.");
var customerFiveInsurance = new WithKeyReplacementCoverage();
Console.WriteLine($"Customer 5 - Insurance Cost: {customerFiveInsurance.Cost()}");

Console.WriteLine("Customer 6: Wants basic car insurance + gap coverage + theft protection.");
var customerSixInsurance = new WithGapAndTheftProtection();
Console.WriteLine($"Customer 6 - Insurance Cost: {customerSixInsurance.Cost()}");

Console.WriteLine("Customer 7: Wants basic car insurance + gap coverage + tire and wheel protection.");
var customerSevenInsurance = new WithGapAndTireAndWheelProtection();
Console.WriteLine($"Customer 7 - Insurance Cost: {customerSevenInsurance.Cost()}");

Console.WriteLine("Customer 8: Wants basic car insurance + gap coverage + theft protection + tire and wheel protection + key replacement coverage.");
var customerEightInsurance = new WithGapTheftTireAndWheelKeyProtection();
Console.WriteLine($"Customer 8 - Insurance Cost: {customerEightInsurance.Cost()}");

Console.WriteLine("Customer 9: Wants basic car insurance + key replacement + tire and wheel protection.");
Console.WriteLine("Error: This combination is not currently supported. Call IT to add yet another subclass...");

Check out this code here

Why Is This a Bad Idea?

  1. Code Duplication: Every new combination of coverages and protections requires a new subclass.
  2. Lack of Flexibility: What if a customer wants a combination that you haven't covered? You need another subclass.
  3. Violation of the Open/Closed Principle: You constantly modify existing code instead of extending it.

Introducing the Decorator Pattern: The Superpower of Flexibility

The Decorator Pattern helps by dynamically adding responsibilities to an object at runtime without modifying its structure. Instead of subclassing, we wrap objects with other objects that extend functionality.

Clearly, subclassing every possible combination is not sustainable. Instead of building a rigid hierarchy, we need a flexible way to compose insurance coverages dynamically. Enter the Decorator Pattern.

Applying the Decorator Pattern: A Clean, Flexible Solution

Let’s refactor the naive code using the Decorator Pattern step by step:

Step 1: Create a Common Interface

public interface IInsuranceCostProvider
{
    decimal Cost();
}

Step 2: Implement a Base Cost Computing Class

public class BasicInsurance : IInsuranceCostProvider
{
    public decimal Cost()
    {
        return 100;
    }
}

Step 3: Create an Abstract Decorator

// Base decorator class that wraps an insurance cost provider
public abstract class InsuranceCostAbstractDecorator : IInsuranceCostProvider
{
    protected readonly IInsuranceCostProvider InsuranceCostProvider;

    protected InsuranceCostAbstractDecorator(IInsuranceCostProvider insuranceCostProvider)
    {
        InsuranceCostProvider = insuranceCostProvider;
    }

    public abstract decimal Cost();
}

Step 4: Implement Concrete Decorators

public class GapCoverageDecorator : InsuranceCostAbstractDecorator
{
    public GapCoverageDecorator(IInsuranceCostProvider insuranceCost) : base(insuranceCost) { }

    public override decimal Cost()
    {
        return InsuranceCostProvider.Cost() + 50;
    }
}

public class TheftProtectionDecorator : InsuranceCostAbstractDecorator
{
    public TheftProtectionDecorator(IInsuranceCostProvider insuranceCost) : base(insuranceCost) { }

    public override decimal Cost()
    {
        return InsuranceCostProvider.Cost() + 30;
    }
}

public class TireAndWheelProtectionDecorator : InsuranceCostAbstractDecorator
{
    public TireAndWheelProtectionDecorator(IInsuranceCostProvider insuranceCost) : base(insuranceCost) { }

    public override decimal Cost()
    {
        return InsuranceCostProvider.Cost() + 20;
    }
}

public class KeyReplacementCoverageDecorator : InsuranceCostAbstractDecorator
{
    public KeyReplacementCoverageDecorator(IInsuranceCostProvider insuranceCost) : base(insuranceCost) { }

    public override decimal Cost()
    {
        return InsuranceCostProvider.Cost() + 10;
    }
}

Step 5: Use the Decorators in Action

class Program
{
    static void Main()
    {
        Console.WriteLine("Customer 1: Only wants basic car insurance.");
        var customerOneInsurance = new BasicInsurance();
        Console.WriteLine($"Customer 1 - Insurance Cost: {customerOneInsurance.Cost()}");

        Console.WriteLine("Customer 2: Wants basic car insurance + gap coverage.");
        IInsuranceCostProvider customerTwoInsurance = new BasicInsurance();
        customerTwoInsurance = new GapCoverageDecorator(customerTwoInsurance);
        Console.WriteLine($"Customer 2 - Insurance Cost: {customerTwoInsurance.Cost()}");

        Console.WriteLine("Customer 3: Wants basic car insurance + theft protection.");
        IInsuranceCostProvider customerThreeInsurance = new BasicInsurance();
        customerThreeInsurance = new TheftProtectionDecorator(customerThreeInsurance);
        Console.WriteLine($"Customer 3 - Insurance Cost: {customerThreeInsurance.Cost()}");

        Console.WriteLine("Customer 4: Wants basic car insurance + tire and wheel protection.");
        IInsuranceCostProvider customerFourInsurance = new BasicInsurance();
        customerFourInsurance = new TireAndWheelProtectionDecorator(customerFourInsurance);
        Console.WriteLine($"Customer 4 - Insurance Cost: {customerFourInsurance.Cost()}");

        Console.WriteLine("Customer 5: Wants basic car insurance + key replacement coverage.");
        IInsuranceCostProvider customerFiveInsurance = new BasicInsurance();
        customerFiveInsurance = new KeyReplacementCoverageDecorator(customerFiveInsurance);
        Console.WriteLine($"Customer 5 - Insurance Cost: {customerFiveInsurance.Cost()}");

        Console.WriteLine("Customer 6: Wants basic car insurance + gap coverage + theft protection.");
        IInsuranceCostProvider customerSixInsurance = new BasicInsurance();
        customerSixInsurance = new GapCoverageDecorator(customerSixInsurance);
        customerSixInsurance = new TheftProtectionDecorator(customerSixInsurance);
        Console.WriteLine($"Customer 6 - Insurance Cost: {customerSixInsurance.Cost()}");

        Console.WriteLine("Customer 7: Wants basic car insurance + gap coverage + tire and wheel protection.");
        IInsuranceCostProvider customerSevenInsurance = new BasicInsurance();
        customerSevenInsurance = new GapCoverageDecorator(customerSevenInsurance);
        customerSevenInsurance = new TireAndWheelProtectionDecorator(customerSevenInsurance);
        Console.WriteLine($"Customer 7 - Insurance Cost: {customerSevenInsurance.Cost()}");

        Console.WriteLine("Customer 8: Wants basic car insurance + gap coverage + theft protection + tire and wheel protection + key replacement coverage.");
        IInsuranceCostProvider customerEightInsurance = new BasicInsurance();
        customerEightInsurance = new GapCoverageDecorator(customerEightInsurance);
        customerEightInsurance = new TheftProtectionDecorator(customerEightInsurance);
        customerEightInsurance = new TireAndWheelProtectionDecorator(customerEightInsurance);
        customerEightInsurance = new KeyReplacementCoverageDecorator(customerEightInsurance);
        Console.WriteLine($"Customer 8 - Insurance Cost: {customerEightInsurance.Cost()}");

        Console.WriteLine("Customer 9: Wants basic car insurance + key replacement + tire and wheel protection.");
        IInsuranceCostProvider customerNineInsurance = new BasicInsurance();
        customerNineInsurance = new KeyReplacementCoverageDecorator(customerNineInsurance);
        customerNineInsurance = new TireAndWheelProtectionDecorator(customerNineInsurance);
        Console.WriteLine($"Customer 9 - Insurance Cost: {customerNineInsurance.Cost()}");

        Console.WriteLine($"Customer 10: Wants basic car insurance + theft protection + key replacement.");
        IInsuranceCostProvider customerTenInsurance = new BasicInsurance();
        customerTenInsurance = new TheftProtectionDecorator(customerTenInsurance);
        customerTenInsurance = new KeyReplacementCoverageDecorator(customerTenInsurance);
        Console.WriteLine($"Customer 10 - Insurance Cost: {customerTenInsurance.Cost()}");
    }
}

Check out the refactored code here

Why Is This Better?

  • Extensible: You can add new protections without modifying existing code.
  • Composability: Coverages can be combined in any order, avoiding subclass explosion.
  • Follows the Open/Closed Principle: You extend behavior without modifying core classes.

Real-World Scenarios for the Decorator Pattern

  1. Logging Mechanism: You can wrap a simple logger within others dynamically to add log features (timestamp, encryption, etc).
  2. Data Compression & Encryption: Wrap data streams dynamically to compress or encrypt data.
  3. GUI Components: Apply styles dynamically to UI elements (like adding scrollbars to a window).

Code Smells That Signal You Need a Decorator

  • 🚨 Too Many Subclasses: If every new feature requires a subclass, it’s time to refactor.
  • 🚨 Repeated Code Blocks: If different classes share the same logic, decorators can help.
  • 🚨 Rigid Feature Expansion: If adding features requires modifying core classes, decorators provide a better approach.
  • 🚨 Adds-On: If part of the requirements mention the use of "Add-Ons" or "Options", this pattern might be just at the corner.

Final Thoughts

The Decorator Pattern is a powerful way to extend functionality dynamically without modifying existing code. By wrapping objects instead of subclassing them, you create a more maintainable and flexible design. Every time you think, “I just need one more subclass” stop. The Decorator Pattern offers a scalable way to extend behavior dynamically without breaking existing code. Embrace the flexibility, avoid subclass chaos, and write cleaner, more maintainable software. Sometimes, all you need is a good decorator!


Happy coding! ⚡


Design PatternsDecorator PatternC#OOPSoftware Engineering