What is the Decorator design pattern?
The Decorator pattern allows you to add new functionalities or features to an existing object at runtime without modifying its original code and without having to create a multitude of subclasses.
The Goal of the Decorator Pattern
- Open/Closed Principle: Your code should be open for extension (adding new features) but closed for modification (you shouldn’t have to change existing classes every time you want to add a new feature).
- Avoid Class Explosion: Instead of creating a new class for every possible combination of features, you build features as modular “decorators” that can be stacked.
Let’s illustrate this with a common example: a NotificationService.
Imagine you have a NotificationService that sends messages to customers.
Initial Requirement: Send only an email.
public class EmailNotification
{
public void Send(string message)
{
Console.WriteLine($"Sending Email: {message}");
}
}
Now, imagine the requirements change. You also need to send SMS and WhatsApp messages. Furthermore, customers might choose combinations like (Email + SMS), (SMS + WhatsApp), or all three.
If you were to use inheritance to handle this, you’d quickly fall into the “class explosion” problem:
EmailAndSMSNotificationEmailAndWhatsAppNotificationAllInOneNotification- … and so on, for every permutation.
graph TD
A[NotificationService] --> B[EmailNotification];
A --> C[SMSNotification];
A --> D[WhatsAppNotification];
B --> E[EmailAndSMSNotification];
B --> F[EmailAndWhatsAppNotification];
C --> E;
C --> G[SMSAndWhatsAppNotification];
D --> F;
D --> G;
E --> H[AllInOneNotification];
F --> H;
G --> H;
note for H "Class Explosion!"
The code for sending an SMS, for example, would be duplicated across multiple classes. If the SMS API changes, you’d have to modify several classes.
The core issue here is that inheritance is static. You have to know all possible combinations at compile time and create classes for them. It’s not easy to dynamically add a feature for a specific customer based on a variable from the database without a lot of if statements.
Naima’s Note: In the context of AI-driven applications, features and integrations are constantly evolving. You might have an AI model that needs to notify users via different channels based on their preferences, the urgency of the alert, or even the AI’s confidence score. Using a static inheritance hierarchy for such dynamic requirements would quickly become unmanageable. The Decorator pattern provides the runtime flexibility needed to adapt to these changing needs without constant code refactoring.
The Solution: The Decorator Pattern
Instead of creating classes for every combination, we’ll build “blocks” that can be stacked on top of each other.
-
INotificationInterface: Define the core contract.public interface INotification { void Send(string message); } -
EmailNotification(Concrete Component): This is our base notification sender.public class EmailNotification : INotification { public void Send(string message) { Console.WriteLine($"Sending Email: {message}"); } } -
NotificationDecorator(Base Decorator/Wrapper): This abstract class implementsINotificationand holds a reference to anotherINotificationobject. This is the “wrapper” that allows us to chain decorators.public abstract class NotificationDecorator : INotification { protected INotification _notification; public NotificationDecorator(INotification notification) { _notification = notification; } public virtual void Send(string message) { _notification.Send(message); } } -
Concrete Decorators (Additional Features): These classes extend
NotificationDecoratorand add specific functionalities.public class SMSDecorator : NotificationDecorator { public SMSDecorator(INotification notification) : base(notification) { } public override void Send(string message) { base.Send(message); // Call the wrapped notification's Send method Console.WriteLine($"Sending SMS: {message}"); // Add SMS functionality } } public class WhatsAppDecorator : NotificationDecorator { public WhatsAppDecorator(INotification notification) : base(notification) { } public override void Send(string message) { base.Send(message); // Call the wrapped notification's Send method Console.WriteLine($"Sending WhatsApp: {message}"); // Add WhatsApp functionality } }
Now, you can compose notifications dynamically at runtime:
// Send only Email
INotification emailOnly = new EmailNotification();
emailOnly.Send("Hello!");
// Send Email + SMS
INotification emailAndSms = new SMSDecorator(new EmailNotification());
emailAndSms.Send("Hello!");
// Send Email + SMS + WhatsApp
INotification allChannels = new WhatsAppDecorator(new SMSDecorator(new EmailNotification()));
allChannels.Send("Hello!");
graph TD
A[INotification] --> B[EmailNotification];
A --> C[NotificationDecorator];
C --> D[SMSDecorator];
C --> E[WhatsAppDecorator];
D --> C;
E --> C;
B -- "Decorated by" --> D;
D -- "Decorated by" --> E;
Notice how this allows you to add functionality at runtime. You’re building up the desired behavior by wrapping objects, adhering to the Open/Closed Principle.
Naima’s Final Word: The Decorator pattern is a powerful tool for achieving flexibility and extensibility without resorting to complex inheritance hierarchies. It’s about composing behavior rather than inheriting it. In a world where software requirements are constantly shifting, and AI capabilities are being integrated in novel ways, patterns like Decorator empower developers to build adaptable systems that can evolve gracefully. This pragmatic approach to design is a cornerstone of the 10xdev.blog philosophy.