Next up in my Design Patterns series is the
Decorator pattern, which allows you to give your objects new responsibilities at runtime without making any code changes to their underlying classes.
Favor Composition over Inheritance
This is an oft mentioned phrase when learning Object Oriented Programming, and it surfaces again when studying the utility of the decorator pattern.
When sub-classing, behaviors inherited from the parent are statically set at compile time. This leads to valuable compile time checks and can reduce the frequency of runtime errors, but it also introduces rigidity to your class hierarchy.
Dynamically extending an object’s behavior through composition can be done at runtime, and removes the rigidity introduced by inheritance. In addition, it allows you as the developer to write new code to extend the target object with new behaviors instead of needing to alter the original class code. Writing new code eliminates the potential for introducing bugs in the parent class, or inadvertently causing side-effects in code elsewhere in the application that relies on the original class definition.
This hits at a core principle of good class design called the “Open/Closed Principle”: classes should be open for extension, but closed for modification.
The Decorator Pattern
The Decorator pattern allows us to “decorate” (extend) objects with added functionality at runtime. It gives developers the power to compose objects that are purpose driven for the current demands of their users.
Decorators can be thought of as wrappers around the object they are decorating: they are the same type as the object being decorated (SuperType), and therefore the decorated object can be passed and used in a program in place of the original wrapped object. See class diagram below:
The diagram above shows how the decorator pattern might be implemented in a traditional OO language like Java.
Decoratorthemselves are both either abstract classes or interfaces (doesn’t matter which).
Decoratorextends (or implements)
Component, which gives concrete implementations of
Decoratorthe ability to be used in place of concrete implementations of
- Each concrete implementation of
Decoratorhas a field of type
Component(the SuperType shared between the decorator and the decorator target) where the instance of the object
ConcreteComponentto be decorated is set.
- Methods overridden in a
Decoratorwill delegate their implementation to the object they are wrapping, then will extend or modify that implementation with new behavior.
- Since the concrete
Decoratorsthemselves share the type
Component, decorators can be wrapped in other decorators! Each one adds a new layer or “wrapper” to the inner concrete object.
Decoratoris useful for extending behaviors exposed as abstract methods by the shared supertype (
Componentabove), but cannot be used to add brand new behaviors to objects that do not already exist as abstract methods in the shared supertype. In other words, a decorator cannot add a brand new method to a decorated object, because the contract doesn’t exist in the shared abstract supertype.
Pizza Shop Example
Let’s say we have a pizza shop: pizzas can have various pizza toppings like Pepperoni or Cheese, and can be either Thin Crust or Deep Dish (I am from Chicago after all!). Check out the following repository with some test code that I wrote to get an idea of how the decorator pattern can help design a system that flexibly meets any pizza / topping combination the customer throws at you!
Downsides of Decorator
There are several limitations of the
Decorator pattern that I’d like to highlight:
- If you have code that relies on the type of the concrete implementation that you are wrapping (i.e. DeepDish or ThinCrust from the pizza example above) then decorator will obscure that type information from you.
- Decorating your objects manually is a pain. Lots of parameters to remember to pass and many constructors if everything is a new object. Combining
Builderpatterns alleviates some of this overhead, and makes building purpose driven objects much simpler and less prone to error.
- As seen from my example repo, using
Decoratoroften leads to numerous small classes that are each responsible for one behavior modification. This makes the code somewhat more difficult to read and understand.
Doing a deep dive in to the Decorator pattern helped me get a better understanding of its pros and cons. The Open/Closed principle (keeping closed parts isolated from new extensions) is great to strive for while developing systems, and
Decorator gets us closer to this goal.
I also gave a presentation at work that covered the Decorator pattern. Check out the slides here.
My next design pattern post will be about the Factory pattern, coming soon!