Issues arising from the decorator pattern and how to avoid them
The decorator pattern is a well-known and widely used programming design pattern. At first glance, the pattern seems to fit multiple software development principles and best practices, however, after years of using the pattern, we have noticed some serious issues. This post describes some of the pitfalls associated with the decorator pattern, which ultimately made us avoid the pattern altogether.
The decorator pattern is a well-known structural design pattern that allows for extending existing classes with additional functionality without touching the original implementation.
Essentially, the pattern takes a component that we want to extend, say with retry logic, and creates a new component wrapping the original one. All calls to the new component are forwarded to the wrapped component, with some additional logic done either before or after. The two components should implement the same interface allowing the decorator to be injected and used in place of the original component.
The Gist below shows a simplified usage of the decorator pattern. Here, looking up a username is decorated with a policy retrying 10 times. The
UsernameLookupRetryDecorator can now be used instead of the
UsernameLookup as they implement the same interface.
Typical reasons for applying the decorator pattern are adhering to the famous Single Responsibility Principle and the Open-Closed Principle.
The common perception of these principles dictates that a single class should do just one small thing. The decorator pattern enables this as part of the logic, such as retrying or logging, can be placed in a separate class decorating the original one and thereby separating the concerns.
The open-closed principles extension over modification which is exactly what the decorator pattern does — if you realize that your existing component needs additional logic, the decorator pattern allows you to add this without modifying the original class in any way that encourages.
Despite the aforementioned benefits of the decorator pattern, we mostly stopped using the pattern altogether. As it turns out, the benefits of the decorator pattern are also what create serious pitfalls.
The decorator is added without modifying the original component or even the places where the component is used. This also means that it is not immediately clear when later looking at the existing code that it is in fact being decorated with additional logic behind the scenes. The logic contained in decorators is essentially hidden. This causes serious pitfalls.
In the past years, we have seen this create problems in various ways. The worst cases have caused production issues due to critical logic from a decorator being overlooked and left out when a component was refactored. The opposite thing has also happened multiple times — people adding logic directly to a component, which was already present in a decorator.
Someone could stumble upon the UsernameLookup component from the Gist above, realize that it has no retry logic, and add this directly to the class as seen below. Once to production, the component would retry 100 times instead of the expected 10. 10 times in the component itself, multiplied by the 10 retries that were already there in the hidden decorator logic.
The less serious downside of the pattern is that code is simply less readable and harder to comprehend when closely related logic gets spread to different classes. This is especially the case when these classes are then composed together behind the scenes, as they are with the decorator pattern.
Most of the time, the uses of the decorator pattern can simply be removed. Instead of putting logging, retry, or whatever other logic you need into a decorator, simply place it directly in the original component. The Single Responsibility Principle does not have to be interpreted at such a granular level and modifying a component instead of extending it rarely has any negative implications in practice. At the same time, the number of files and classes required is significantly reduced which helps keep the code concise and easy to understand.
If you really want to separate a piece of the logic into its own class, either for separation of concerns or to avoid duplicating the logic if multiple components need it — consider injecting this class into the original component instead of it. This way, it is clear and visible when looking at the original component what is going on.
The decorator pattern can of course provide enough value in certain cases to outweigh the risks and downsides. The important point here is simply to consider if it is actually worth it. If you choose to use the pattern, consider adding tests that cover the intended behavior end-to-end. Unit tests that cover individual classes in isolation will not be enough to discover missing or duplicate logic like described in this article.
I hope you enjoyed reading this post and that it made you rethink the implications that arise from using the popular decorator pattern. This post is part of a series on simplifying the way software is written. Check out the initial post, describing the overall thoughts and how we removed 80% of our code:
Want to Connect?Join my email list for more simplified software insights!