As I mentioned in the first article of this series, Design patterns are solutions to problems that someone once had and solved by applying a model that has been documented and that you can adapt entirely or according to the need for your solution.
The original idea of Design Patterns came up with Christopher Alexander when he proposed the creation of pattern catalogs for architecture. Christopher himself defined the patterns in this way:
“A pattern describes a problem that occurs numerous times in a given context, and it also describes the solution to that problem, so that this solution can be used systematically in different situations”.
The main purpose of design patterns is to help developers to structure their applications in more flexible ways, easier to understand and maintain.
In this article, I would like to share some thoughts about the Decorator, a Structural Pattern.
Structural patterns focus on how classes and objects are composed to form larger structures, maintaining its flexibility and efficiency. The flexibility of object composition comes from the ability to change the composition at runtime.
The decorator pattern (also known as Wrapper) is used to extend or alter the functionality of objects at runtime by wrapping them in an object of a decorator class. This provides a flexible alternative to using inheritance to modify behavior.
Decorators offer a flexible way to add responsibility to individual objects. Unlike inheritance, decorated objects are not limited by their parent classes. Putting in other terms, a client has control over how and when to decorate the component.
We can use the decorator to:
- Append responsibilities to individual objects dynamically and transparently, without affecting other objects.
- For traits or responsibilities that can be withdrawn.
- When subclassing is impractical.
In general, this pattern is presented with a few particular elements. The first one would be an abstract representation of the component, that in swift can be a
protocol. This is followed by a concrete implementation of this component, an abstract definition of a decorator for this component and one or more concrete implementations for this component’s decorator.
A few of the characteristics of the Decorator Pattern are:
- The decorators have the same super-type or conformance as the object they decorate.
- More than one decorator can be used to comprise an object.
- Once the decorator has the same super-type or conformance as its decorated object (decoratee), it is possible to pass a decorated object in place of the original object.
- The decorator adds its own behavior before or after delegating the decoratee its work.
- The objects can be decorated at any moment, which makes it possible to decorate them dynamically at run-time, with as many decorators as needed.
Practical Example in Swift
There are many use cases for a decorator, but to keep things simple for this article, let’s make a salary calculator.
In order to calculate the salary, we’ll input an hourly base rate, that the calculator needs to convert to a weekly value. With our weekly value, it will first discount the taxes and then discount the healthcare.
So we begin with an abstraction
SalaryCalculator, with the component’s definition.
Now we create a class that’s going to conform to the protocol, and implement the method.
After that we can create our
TaxDiscountSalaryDecorator, that, in our case, will apply the first discount we want.
As you can see, it takes as parameter an object (our decoratee) conforming to
SalaryCalculator and also implements
SalaryCalculator. This means that when the method is called, we can apply the discount, and forward the message.
HealthCareDiscountSalaryDecorator. It follows the same principles as
TaxDiscountSalaryDecorator, but this time, it applies a different discount.
Now we have all elements we need to accomplish our goal. So let’s see how our calculator works, with the discounts.
In order to use our calculator with the discounts, we need to compose it with our decorators, like so:
This way, we apply the discounts we need without modifying our original
ExampleSalaryCalculator class. We’re extending it’s behavior without modifying it.
- The use of a pattern provides the project with a universal style, which can make the code more comprehensible.
- The client’s code does not need to be modified in order to add new functionalities.
- The expansion of functionalities happen dynamically, which offers more flexibility to the codebase.
- It allows the components to be as simple as possible, delegating the addition of new functionalities to the decorators.
- Bigger number of classes utilized in the project, when compared to a version containing attributes and methods in a base class and implementation of specific functionalities in subclasses.
- It may become hard to read and understand the project, in case of overuse.
- It could decrease the project’s efficiency in case many decorators are used for an object with multiple public methods (that would require the decorator to offer the same public interface as the objects they decorate).
As we could observe, the Decorator Pattern makes usage of conformance to be able to encompass the decorated objects. This way, when a decorator is composed with it’s component, a new functionality can be added, but not inherited.
This small difference offers a huge amount of flexibly to create objects as needed without modifications to the existing components. As in all decisions, there are no silver bullets. The advantages of this pattern can be tremendous. So all in all, teams could take advantage of this pattern to solve a specific problem, observe the decision’s consequences, it’s impact in performance, and the evolution in the codebase as a whole.
And, of course, there are some downsides to that, as we could notice. Therefore, it bears to the developers the evaluation of the project’s needs and the tradeoffs that decorators would bring.
If you’re looking for more examples of how we can use decorators, I highly recommended these videos: Decoupling analytics from MVVM components and Testing code that uses DispatchQueue.main.async. They explain some really good use cases, using Swift.
Design Patterns - Elements of Reusable Object-Oriented Software - Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides.