Learn the Decorator Design Pattern
This is the 12th post in a series on design patterns.
Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors.
Imagine that you are developing a notification library that allows other services to notify their users of any notifications. In the first version of the Notifier class, email notifications were the only method of notifications, but now users ask for SMS notifications for critical notifications, and later some users even request Telegram notifications.
You extended the Notifier class and added new notification methods to new subclasses. Now, the client was supposed to instantiate the desired notification class and use it for all future notifications.
Users are now reporting that they need notifications by a combination of communication means like Email+SMS or SMS+Telegram or Email+SMS+Telegram, etc. For this reason, you created a newer subclass with all combinations of notification methods. The code will quickly become bloated.
Decorator Design Pattern
When you need to alter the behavior of an object, the first thing that comes to mind is extending a class. Inheritance has several caveats, as explained below:
- An existing object cannot be changed at runtime. You can only replace the whole object with another one that’s created from a different subclass.
- There can only be one parent class for subclasses in Java.
The composition can be used instead of inheritance to overcome these caveats. In composition, an object references another and delegates some work to it, while in inheritance, the object itself is able to complete the task, inheriting the behavior from its superclass.
You can easily replace the linked helper object with another using composition, thus changing the behavior of the container at runtime.
UML Class Diagram
Not familiar with UML Class Diagram? I have written a detailed post on the UML Class diagram.
Implementation steps
- Make sure you can represent your business domain as a primary component with multiple optional layers over it.
- Find out what methods are common to both the primary component and the optional layers. Define those methods in the component interface.
- Develop a concrete component class and define its behavior.
- Define the base decorator class. A field should be provided for storing a reference to a wrapped object. The field should be declared with the component interface type to allow linking to concrete components and decorators. All work must be delegated to the wrapped object.
- All classes must implement the component interface.
- Extend the base decorator to create concrete decorators. The concrete decorator must execute its behavior before or after the call to the parent method.
- Client code must create decorators and compose them as needed.
Source Code Implementation
The Notification (Component) interface defines the common interface for wrappers and wrapped objects.
The Notifier class implements the Notification interface. This class defines the basic behavior of sending email, which can be altered by decorators.
BaseDecorator has a field for referencing a wrapped object. In order to be able to contain both concrete components and decorators, the field’s type should be declared as a Notification interface. All operations are delegated to the wrapped object by the base decorator.
A SMSDecorator defines extra behaviors that can be added dynamically to components. The SMSDecorator overrides the methods of the base decorator and executes their behavior either before or after the parent method is called. In this example, we send SMS after the parent method is called.
Similarly, TelegramDecorator class defines additional behaviors for sending messages as telegram notifications.
Using DecoratorClient, components can be wrapped in multiple layers of decorators, so long as all objects are accessed through the component interface.
// Output
Hello Medium, message sent in email
Hello Medium, message sent in SMS
Hello Medium, message sent in Telegram
When To Apply Decorator Design Pattern
- When you need to be able to add extra behaviors to objects at runtime without breaking the code that uses them, you should use the Decorator pattern.
- The pattern is useful when it is difficult or impossible to extend an object’s behavior through inheritance.
Pros of Decorator Design Pattern
- You can extend an object’s behavior without creating a new subclass.
- At runtime, you can add or remove responsibilities from an object.
- You can wrap an object in multiple decorators to combine multiple behaviors.
- A monolithic class that implements many possible variants can be split up into several smaller classes.