Originally posted here
The Decorator is a structural design pattern that lets us attach new behaviours to objects by placing these objects inside special wrappers. These wrappers add the desired behaviour without modifying the original code.
The Decorator is a handy tool when we have some object that we want to enhance with additional behaviours. Still, we are either unwilling or unable to change its internal workings. With this pattern, we can wrap the object in a specialized wrapper and implement the functionality in the wrapper.
You can find the example code of this post, on Github
Conceptualizing the Problem
Imagine that we have a notification library which lets us notify a user about deployment events.
The initial version of the library is based on a Notifier
class that has a few fields such as a list of email addresses, a constructor and a single Notify
method. The Notify
method accepts a message argument from the client and sends it to a list of emails, provided via the Notifier
's constructor. Some other application acts as a client and will configure the Notifier
once, and then use it each time something important happens during the deployment workflow.
The library worked perfectly for some time, but then disaster struck. During a nightly release, the build failed, and the emails were sent but there wasn't anyone to address the issue. After all, most people don't check their emails in the middle of the night.
Now the users require much more than just email notifications. Many of them would like to receive an SMS notification about critical issues. Others would like Microsoft Teams notifications, and the DevOps teams would love to get Slack notifications for some reason or another.
No problem there! We can extend the Notifier
class and add the additional notification methods into new subclasses. Now the client can instantiate the appropriate notification class and use it for further notification.
But then someone asked the million-dollar question. "Why can't you use several notifications at once? If the boat is sinking you'd probably want to be informed through every channel".
Keeping with our previous solution, we can address the problem by extending the Notifier
class with special subclasses that combine several notification methods in one class. Time for some simple math now. Each notification method can either exist or not exist in a subclass. Thus there are two states for the existence of a notification method. We currently have 3 different notification methods. So there are 2 to the power of 3 different combinations of subclasses or 8 classes. If we add another notification method we will have 2 to the power of 4 different subclasses, or 16 different classes altogether. It becomes apparent that this approach will bloat the code exponentially.
Extending a class is the first thing that comes to mind when we need to alter an object's behaviour. However, there are some issues with Inheritance.
- Inheritance is static. We can't alter the behaviour of an existing object at runtime. We can only replace the instance with another that's created by a different subclass.
- Subclasses can have just one parent class. In C#, inheritance doesn't let a class inherit behaviours from multiple classes simultaneously.
One of the ways to overcome these caveats is by using Aggregation or Composition instead of Inheritance. Both of these alternatives are almost identical. An instance has a reference to another instance and delegates some work, whereas, with inheritance, the instance itself executes that work, inheriting the behaviour from its parent.
With this new approach, we can easily substitute the linked object with another changing the behaviour of the container at runtime. Aggregation and Composition are key techniques behind many design patterns, including the Decorator.
A Decorator. also known as a Wrapper, can be linked with some target object. The wrapper can delegate all requests it receives to the target. However, the wrapper can alter the result by either processing the request before it is sent to the target or altering the response after the target returns a result.
Structuring the Decorator Pattern
The following diagram demonstrates how the Decorator pattern works.
- The application makes a request and the decorator class intercepts it.
- The decorator class can pre-process the request before passing it to the wrapped class.
- The wrapped class performs its functionality, as usual, unaware of the decorator class.
- The decorator class can post-process the response before passing it to the application.
- The decorator returns the result to the original caller.
In its base implementation, the Decorator pattern has four participants:
- Component: The Component declares the common interfaces for both wrappers and wrapped objects.
- Concrete Components: The Concrete Component is the class of objects that will be wrapped. It defines the original behaviour that can be altered by the decorators.
- Base Decorator: The Base Decorator references the wrapped object. The base decorator delegates all operations to the wrapped object.
- Concrete Decorators: The Concrete Decorator define additional behaviours that can be added to Concrete Components dynamically.
- Client: The Client can wrap components in multiple layers of decorators, as long as it works with the objects via a shared interface.
To demonstrate how the Decorator pattern works, we will open a fancy farm-to-fork restaurant.
The idea behind the restaurant is that make dishes from ingredients directly acquired from the producer. This means that some dishes may be marked as sold out since the farm can produce only so many ingredients.
To start, we will implement our Component participant, which is our abstract Dish
class:
namespace Decorator.Components
{
/// <summary>
/// The abstract Component class
/// </summary>
public abstract class Dish
{
public abstract void Display();
}
}
We also need a couple of ConcreteComponent participant classes representing the individual dishes our restaurant can serve. These classes only care about the ingredients of a dish and not the number of dishes available. This is the responsibility of the Decorator.
namespace Decorator.Components
{
/// <summary>
/// A ConcreteComponent class
/// </summary>
public class Salad : Dish
{
private readonly string _veggies;
private readonly string? _cheeses;
private readonly string? _dressing;
public Salad(string veggies, string? cheeses, string? dressing)
{
_veggies = veggies;
_cheeses = cheeses;
_dressing = dressing;
}
public override void Display()
{
Console.WriteLine("\nSalad:");
Console.WriteLine($" Veggies: {_veggies}");
Console.WriteLine($" Cheeses: {_cheeses}");
Console.WriteLine($" Dressing: {_dressing}");
}
}
}
namespace Decorator.Components
{
/// <summary>
/// A ConcreteComponent class
/// </summary>
public class Pasta : Dish
{
private readonly string _pasta;
private readonly string _sauce;
public Pasta(string pasta, string sauce)
{
_pasta = pasta;
_sauce = sauce;
}
public override void Display()
{
Console.WriteLine("\nPasta: ");
Console.WriteLine($" Pasta: {_pasta}");
Console.WriteLine($" Sauce: {_sauce}");
}
}
}
Now, we need to keep track of whether a dish is available. To do this we will first implement an AbstractDecorator
class which is our Decorator participant.
namespace Decorator.Decorators
{
/// <summary>
/// The Abstract Base Decorator
/// </summary>
public abstract class AbstractDecorator : Dish
{
protected Dish _dish;
protected AbstractDecorator(Dish dish)
{
_dish = dish;
}
public override void Display()
{
_dish.Display();
}
}
}
Finally, we need a ConcreteDecorator participant, to keep track of how many of the dishes have been ordered. This is the role of the AvailabilityDecorator
.
namespace Decorator.Decorators
{
public class AvailabilityDecorator : AbstractDecorator
{
public int AvailableItems { get; set; }
protected List<string> customers = new();
public AvailabilityDecorator(Dish dish, int available) : base(dish)
{
AvailableItems = available;
}
public void OrderItem(string name)
{
if (AvailableItems > 0)
{
customers.Add(name);
AvailableItems--;
}
else
Console.WriteLine($"\nNot enough ingredients for {name}'s dish");
}
public override void Display()
{
base.Display();
foreach(string customer in customers)
Console.WriteLine($"Ordered by {customer}");
}
}
}
The last step is to set up the Main
method. First, we will define a set of dishes, and then decorate them so they can keep track of their availability. Finally, we will order the dishes.
Salad caesarSalad = new("Crisp Romaine Lettuce", "Parmesan Cheese", "Homemade Caesar Dressing");
caesarSalad.Display();
Pasta fetuccine = new("Homemade Fetuccine", "Creamy Garlic Alfredo Sauce");
fetuccine.Display();
Console.WriteLine("\nChanging availability of the dishes");
AvailabilityDecorator caesarAvailability = new(caesarSalad, 3);
AvailabilityDecorator pastaAvailability = new(fetuccine, 4);
caesarAvailability.OrderItem("Marion");
caesarAvailability.OrderItem("Thomas");
caesarAvailability.OrderItem("Imogen");
caesarAvailability.OrderItem("Jude");
pastaAvailability.OrderItem("Marion");
pastaAvailability.OrderItem("Thomas");
pastaAvailability.OrderItem("Imogen");
pastaAvailability.OrderItem("Jude");
pastaAvailability.OrderItem("Jacinth");
caesarAvailability.Display();
pastaAvailability.Display();
Console.ReadLine();
The output of our application will be the following:
The Problem of Fat Decorators
Decorators are all about composability. This means that the reused class is not inherited but wrapped by decorator classes. This comes with a caveat. The decorator does not inherit the public
interface of the reused class, but needs to explicitly implement every method of the interface. Creating and maintaining all of these methods can add quite an overhead.
While there are some development tools like ReSharper that can help with handling all of those methods, the responsibility of maintenance still lies on the developer.
Pros and Cons of Decorator Pattern
✔ We can extend an object's behaviour without altering its source code | ❌ It's hard to remove a wrapper from a wrapper stack |
✔ We can extend an object's behaviour without creating a new subclass. | ❌ It's hard to implement decorators in such a way that will not depend on the order of decorators in a wrapper stack. |
✔ We can combine multiple decorators to add several behaviours to an object | ❌ Fat decorators might have to implement several methods that will not use |
✔ We can divide a monolithic class into multiple classes, each with its behaviour, thus satisfying the Single Responsibility Principle | ❌ The initial configuration of layers can look pretty ugly and might be hard to maintain |
Relations with Other Patterns
- The Adapter and Decorator work in a similar way. However, the Adapter changes the interface of an existing object, while the Decorator enhances an object without changing its interface. In addition, the Decorator supports recursive composition, which is impossible when using the Adapter. Finally the Adapter provides a different interface for the wrapped object, while the Decorator provides it with an enhanced interface.
- The Chain of Responsibility and Decorator have similar class structures. Both interfaces rely on recursive composition. However, there are some differences. The CoR handler can execute arbitrary operations without dependence on other handlers in the chain. On the other hand, a Decorator can extend the object's behaviour while keeping it consistent with the base interface. Moreover, decorators aren't allowed to break the flow of the request.
- The Composite and Decorator patterns have a similar structure since both rely on recursive composition. A Decorator is like a Composite but only has one child component. Also the Decorator adds additional responsibilities to the wrapped object, while Composite just sums up its children's results.
- The Decorator and Proxy have similar structures, but very different intents. Both patterns are built on the composition principle. However a Proxy usually manages the life cycle of its service object on its own, whereas the composition of the Decorator is always controlled by the client.
Final Thoughts
In this article, we have discussed what is the Decorator pattern, when to use it and what are the pros and cons of using this design pattern. We then examined what is a fat decorator and how the Decorator pattern relates to other classic design patterns.
It's worth noting that the Decorator pattern, along with the rest of the design patterns presented by the Gang of Four, is not a panacea or a be-all-end-all solution when designing an application. Once again it's up to the engineers to consider when to use a specific pattern. After all these patterns are useful when used as a precision tool, not a sledgehammer.
Top comments (0)