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.
Decorator Pattern
The decorator pattern is a design pattern which allows behavior and functionality to be added to functions and objects without modifying those functions and objects themselves. A decorator is a function, object, or class which wraps another function, object, or class (respectively) and provides additional functionality on top of the wrapped functionality. A few common use cases of the decorator pattern include:
- Caching: a function can be decorated to return a cached response after it has already been called instead of re-evaluating each time.
- Intercepting pending asynchronous calls: we can decorate a function to capture subequent function calls if one has already been made and is still pending completion, then return the same response once complete. For example, if we make an HTTP call, the decorator can ensure only one happens at once.
- Streaming data: input stream readers can be decorated with other readers to add enhanced streaming functionality, such as grouping the data by lines, special characters, etc.
- Decorating UI functionality: class decorators can be used to apply additional functionality to user interfaces, such as viewport scrollbars, text decoration, etc.
- App Config Based Enabling: in certain situations, a method or function decorator might be a good way to put a guard to only execute a function when some configuration is enabled or disabled.
- Execution Statistics and Logging: functions can be wrapped in other functions which record how long the function took to execute and other metadata.
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.
Class Decorators
A class decorator is a decorator which is applied to an entire class. The general approach to creating a class decorator is:
- Create an abstract decorator class which implements the same interface. This class will accept an instance of something which implements that same interface in the constructor. We'll refer to this as the base instance.
- Either mark the methods as abstract, or create default implementations to call the base instance's corresponding method with no further action taken.
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.
Function Decorators
Functions and methods can also be decorated themselves. In this section we will call an executable code a method if it is defined within a class and a function if it is not. A method decorator can be created in a few different ways:
- Create a class decorator and only decorate the desired method.
- Within a class, create a private generic method which will accept a callback for the main action, then do its own actions before and after. This is only recommended for common concerns, such as logging.
- In languages which support functions outside of classes, create a function decorator, then refer to that decorator within the class's method.
The first option is generally the simplest for method decorators. For function decorators, we also have a few options
- Use a series of functions as pipes to receive the return values from previous functions, transform them, and return the transformed values.
- Create functions which accept a callback as a parameter and returns a function which wraps the callback with additional functionality.
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.
Decorator Lifecycle
For a decorator function or a decorator class's method, there are three main parts of the function / method's invocation:
- 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.
Nested Decorators
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.