Skip to Content
Course content

109: The Decorator Pattern

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen in plenty of production codebases: a developer starts with a simple requirement, and then "feature creep" hits. Suddenly, the class hierarchy looks like a disaster zone. Let me show you a snippet of a notification system I encountered a while back. The goal was simple: send notifications via different channels. The developer used inheritance to handle the combinations.

public interface INotification {
    void Send(string message);
}

public class EmailNotification : INotification {
    public void Send(string message) => Console.WriteLine($"Sending Email: {message}");
}

public class SmsNotification : INotification {
    public void Send(string message) => Console.WriteLine($"Sending SMS: {message}");
}

// Here is where it gets ugly...
public class EmailAndSmsNotification : INotification {
    public void Send(string message) {
        new EmailNotification().Send(message);
        new SmsNotification().Send(message);
    }
}

public class EmailSmsAndSlackNotification : INotification {
    public void Send(string message) {
        new EmailNotification().Send(message);
        new SmsNotification().Send(message);
        new SlackNotification().Send(message);
    }
}

The Inheritance Explosion

Look at those last two classes. It's a nightmare. If you add a fourth notification channel—say, WhatsApp—you don't just add one class. You have to create every possible combination of that new channel with the existing ones. This is what I call "class explosion." It's rigid, it's repetitive, and it's a fragile way to build software.

The core problem is that the developer is trying to use inheritance to add behavior. Inheritance is for "is-a" relationships. A "Notification that sends Email and SMS" isn't really a different kind of notification; it's just a notification with extra steps. When you find yourself creating classes like EmailAndSmsAndSlackNotification, you've gone down the wrong path.

Wrapping Behavior instead of Extending Classes

This is where the Decorator Pattern saves you. Instead of creating a class for every combination, we create a "wrapper." The wrapper implements the same interface as the object it's wrapping, but it adds its own logic before or after delegating the call to the inner object.

Here is how I would rewrite this to be actually maintainable:

public interface INotification {
    void Send(string message);
}

// The base implementation
public class BasicNotification : INotification {
    public void Send(string message) => Console.WriteLine($"Base message: {message}");
}

// The Base Decorator
public abstract class NotificationDecorator : INotification {
    protected readonly INotification _inner;
    public NotificationDecorator(INotification inner) => _inner = inner;
    
    public virtual void Send(string message) => _inner.Send(message);
}

// Concrete Decorators
public class EmailDecorator : NotificationDecorator {
    public EmailDecorator(INotification inner) : base(inner) { }
    public override void Send(string message) {
        base.Send(message);
        Console.WriteLine($"Sending Email: {message}");
    }
}

public class SmsDecorator : NotificationDecorator {
    public SmsDecorator(INotification inner) : base(inner) { }
    public override void Send(string message) {
        base.Send(message);
        Console.WriteLine($"Sending SMS: {message}");
    }
}

Stacking your Decorators

Now, instead of having a hard-coded EmailAndSmsNotification class, you compose your object at runtime. You just wrap the objects like an onion.

// I want Email and SMS? Easy.
INotification notify = new BasicNotification();
notify = new EmailDecorator(notify);
notify = new SmsDecorator(notify);

notify.Send("Hello World!"); 
// Output: 
// Base message: Hello World!
// Sending Email: Hello World!
// Sending SMS: Hello World!

Notice how we didn't have to create a new class to handle the combination? If you suddenly need to add Slack, you just create one SlackDecorator and wrap it around whatever you're already using. I love this approach because it follows the Open/Closed Principle: the system is open for extension (new decorators) but closed for modification (you don't have to touch the existing logic to add a new combination).




📋 Practical Task

Build a Dynamic Coffee Order System

You are building a point-of-sale system for a coffee shop. The shop has a basic coffee, but customers can add various "condiments" (Milk, Sugar, Whipped Cream) that increase the cost and change the description of the drink.

Your Goal: Implement the Decorator Pattern to allow for any combination of condiments without creating a separate class for every possible drink combination (e.g., no CoffeeWithMilkAndSugar class).

  • Create an ICoffee interface with a GetCost() method (returning a double) and a GetDescription() method (returning a string).
  • Create a concrete SimpleCoffee class that implements ICoffee (Cost: 2.00, Description: "Simple Coffee").
  • Create an abstract CoffeeDecorator class that implements ICoffee and wraps an instance of ICoffee.
  • Create at least two concrete decorators: MilkDecorator (adds 0.50) and SugarDecorator (adds 0.20).
  • In your Main method, instantiate a coffee with both milk and sugar, and print the final description and cost to the console.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.