Design patterns in software development have been around for a little while now. They were made famous in 1994 from the book Design Patters: Elements of Reusable Software and consequently made the authors so famous that they are often now just referred to as the Gang of Four (GoF). The Gang of Four consisted of Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides. If you haven’t read this book, don’t feel bad — it is a very informative book that has changed how we write software, but it can be a very dry read. (If you want an easier introduction to Design Patterns, I recommend Head First Design Patterns).
If I remember correctly, there are 23 design patterns defined in the original text and the authors go very deep into each one. With so many design patterns and such in-depth discussion about the correct way to use each one, it can be intimidating to even try to understand them and many developers will just put off even learning them.
I have a love/hate stance on design patterns. This is because they are great if used in the right scenario, but completely useless and counterproductive if you try to make them fit your problem when the problem you have needs a different solution. Blindly pushing patterns into the code will increase complexity, introduce bugs, and create a maintainability nightmare. Instead, we should understand the underlying principles and ideas to apply them or build upon them as needed.
What I want to do is introduce one of the patterns that I use the most that I feel a lot of software projects could benefit from if used correctly.
The Decorator Pattern
Before we get into exactly what the decorator pattern is and when you could benefit from using it, we need to understand another design principle those pretentious software architects are always boasting about. I’m talking about the Open/Closed Principle (made famous from “first five principles” named by Robert C. Martin in the early 2000s that stands for five basic principles of object-oriented programming and design.)
One thing you will start to notice is that many design patterns and software best practices/principles go hand in hand. The Open/Closed Principle and the Decorator Pattern are one of these pairs. The Open/Closed Principle states that:
Software entities should be open for extension, but closed for modification.
Hmmm… Sounds a little contradictory doesn’t it? At least that’s what I thought when I first read it. How the heck can something be open and closed at the same time? Let’s dig a little deeper.
When we say “open for extension” in the context of object-oriented design we are talking about the ability to add responsibilities or behaviors to an object to “extend” that object’s functionality. When we say “closed for modification” that means we don’t want to modify an existing class definition or construct that has been tested and proven to work (this helps prevent new bugs from popping up).
So in comes the decorator pattern to help save the day. The decoration pattern allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class.
This is achieved by designing a new decorator class that wraps the original class. This wrapping could be achieved by the following sequence of steps:
- Subclass the original “Component” class into a “Decorator” class (see UML diagram);
- In the Decorator class, add a Component pointer as a field;
- Pass a Component to the Decorator constructor to initialize the Component pointer;
- In the Decorator class, redirect all “Component” methods to the “Component” pointer; and
- In the ConcreteDecorator class, override any Component method(s) whose behavior needs to be modified.
Abstractly you can think of it like this diagram:
Clear as mud??
I think a concrete example will help illustrate the idea a little better.
Say you are working as a developer for a coffee shop and they need a system that will handle the determining what the cost and description is for coffee plus any of the added condiments (mocha, extra shot espresso, etc) get added to their coffee. This quickly become a maintainability nightmare if you need classes that represent each of these combinations.
It also is not easy to use interfaces to define the contracts because you would still need to code the implementations in each of these classes whenever prices or descriptions change. The easiest way to get the most flexibility with your code and dynamically build these objects as people mix and match combinations is to use the decorator pattern. This makes it simple when a new condiment or drink is added because you just wrap you existing object with the new CondimentDecorator class and the internal behavior can be recursively called by accessing the parent behavior.
- Say a customer orders a DarkRoast (this would be a DarkRoast class who is derived from Coffee, nothing special yet). Remember that since DarkRoast inherits from Coffee it has a cost() method that computes the cost of the drink.
- The customer wants Mocha, so we create a Mocha object and wrap it around the DarkRoast by passing DarkRoast into Mocha’s constructor. The Mocha object is a decorator. Its type morrors the object it is decorating, in the case, a Coffee. This means that Mocha has a cost() method too, and through polymorphism we can treat any Coffee wrapped in Mocha as a Coffee, too (because Mocha is a subtype of Coffee)
- The customer also wants WhipCream, so we create a WhipCream decorator and wrap Mocha with it the same way Mocha wrapped DarkRoast. Whip is also a decorator, so it also mirrors DarkRoast’s type (Coffee) and includes a cost() method. So, a DarkRoast wrapped in Mocha and Whip is still a Coffee and we can do anything with it we can do with a DarkRoast, including call its cost() method.
- Now it’s time to compute the cost for the customer. We do this by calling cost() on the outermost decorator, WhipCream, and WhipCream is going to delegate computing the cost to the object itit decorates. Once it gets a cost, it will add-on the cost of the WhipCream. The system execution goes as follows:
- First, we call cost() on the out most decorator, WhipCream.
- WhipCream calls cost() on Mocha. (in Java by calling super.Cost(). Other languages have similar constructs) .
- Mocha calls cost() on DarkRoast.
- DarkRoast returns its cost (99 cents).
- Mocha adds its cost, 20 cents, to the result from DarkRoast, and returns the new total, $1.19.
- WhipCream adds its cost, 10 cents, to the result from Mocha, and return the final result, $1.29
(Photo credit: Head First Design Patters)
Using this pattern over subtyping and inheritance can two major advantages to your design:
- More flexibility than static inheritance. The Decorator pattern provides a more flexible way to add responsibilities to objects than can be had with static (multiple) inheritance. With decorators, responsibilities can be added and removed at run-time simply by attaching and detaching them. In contrast, inheritance requires creating a new class for each additional responsibility (e.g., BorderedScrollableTextView, BorderedTextView). This gives rise to many classes and increases the complexity of a system. Furthermore, providing different Decorator classes for a specific Component class lets you mix and match responsibilities.Decorators also make it easy to add a property twice. For example, to give a TextView a double border, simply attach two BorderDecorators. Inheriting from a Border class twice is error-prone at best.
- Avoids feature-laden classes high up in the hierarchy. Decorator offers a pay-as-you-go approach to adding responsibilities. Instead of trying to support all foreseeable features in a complex, customizable class, you can define a simple class and add functionality incrementally with Decorator objects. Functionality can be composed from simple pieces. As a result, an application needn’t pay for features it doesn’t use. It’s also easy to define new kinds of Decorators independently from the classes of objects they extend, even for unforeseen extensions. Extending a complex class tends to expose details unrelated to the responsibilities you’re adding.
Patterns can be a complex subject and knowing when to use them is sometimes more complicated than understanding how to use them. I hope this article shed a little light on the subject. If you have any suggestions to make this clearer, let me know in the comments!
If you liked this please like, share, subscribe to my blog at jasonroell.com. Thanks!
I love to code and build new innovating solutions to people's problems!