The decorator pattern is a useful and versatile design pattern which can enable you to dynamically add functionality to existing objects as needed. In this article, we'll introduce the design pattern, walk through the basic concepts, and give a few different code examples in multiple programming languages.
Decorators can be applied to functions, classes, and other types of objects. In the sections below, we will refer to decorating functions, classes, and objects as decorating objects for brevity.
Benefits Over Subclassing
For class decorators, any behavior which can be achieved via decorators can also be achieved via subclassing. However, in many scenarios, decorators provide a much cleaner implementation than subclassing, especially in situations where behaviors need to be mixed and matched. The table below outlines a few situations and how the decorator vs subclassing approaches would satisfy them.
|Decorator Pattern||Inheritance Based Extensibility|
|Declare Behavior at Runtime||Package the core object with the necessary decorators.||Implement the behavior in a new subclass, then use a factory to create the desired subclass object.|
|Mix and Match Functionality||Package the core object with the required decorators.||Create one subclass for each combination of required functionalities.|
|Extend Functionality While Conforming to an Interface||Create decorators which wrap an object and have the same type / interface definition.||Subclass a root object and override methods as required.|
Note that the inheritance based extensibility options are more static in nature; thus, dynamicly defining behavior is much more difficult. In addition, if we want to mix and match behavior, we would need to create one new subclass for each combination of behaviors, or each permutation if order matters. If we had 10 decorator behaviors, we would need to create 10! = 3628800 to encompass all possible permutations of those behaviors (as order does matter in certain scenarios). In these types of scenarios, the decorator pattern is a much more viable alternative.
The abstract decorator class encapsulates the functionality for receiving the base instance in the constructor and making it accessible to its own subclasses. If you choose, it can also provide default implementations for forwarding method calls to the base instance with no further actions, thus allowing subclasses to only override what they need to (as a decorator might only need to decorate one or more methods of the class instead of every method). In the examples below, we'll decorate a class with a method performExpensiveCalc and call the method on a few different decorated instances to see how they behave:
Notice in the code above that changing the order of decorators changes the behavior. When the logging decorator wraps the cache decorator, every invocation gets logged, but when the cache decorator wraps the logging decorator, logging only happens the first time. If you find yourself working in a situation where multiple decorators are in play, ensure that they are created and wrapped in the correct order.
In the example below, we've taken the 2nd approach. The decorator functions themselves can be treated as factories. When those functions are invoked, they will return a function which represents the decorated version of the callback passed in.
- Preliminary action taken by the decorator before the decorated function is invoked.
- Invocation of the decorated function.
- Action taken by the decorator after the decorated function is invoked.
Each of the three parts above is optional, but at least one of them must exist. In certain cases, the second step may be skipped entirely, such as in our class decorator cache example above.
Implementing Your Own Decorator
Notice that in both examples above, the decorator had the same type definition as the object it was decorating. For the class decorators, all decorators implemented iCalcService. For the function decorators, all functions returned from the decorator had the same type definition: accepting to numbers as arguments and returning a single number. When implementing your own decorator, you will need to ensure that it also conforms to the same type definition as the object it is decorating. The consumer of the object you're decorating shoud have no knowledge of whether or not the object is being decorated, or how many decorators are applied. This will be the case as long as the decorator properly conforms to the type definition.
Decorators are recursively composable, which means that we can apply any number of decorators to an object. The chain of decorators would be implicity represented as a linked list via object references; the first decorator would point to the second, second to third, etc., until the original object is reached at the end of the chain. The order in which decorators are specified may have an impact depending upon what each decorator does, so care should be taken when applying them.