Table of Contents
ToggleThe Decorator Design Pattern is a structural pattern that allows behaviour to be added to individual objects, either statically or dynamically, without affecting the behaviour of other objects from the same class.
This pattern is particularly useful for adhering to the Open/Closed Principle, one of the SOLID principles, which states that software entities should be open for extension but closed for modification.
What is Decorator Design Pattern?
The Decorator Design Pattern is a structural pattern used in software development to add new functionality to an object dynamically without altering its structure. This pattern creates a decorator class which wraps the original class and adds new behaviors or responsibilities.
The main goal of the decorator pattern is to attach additional responsibilities to an object dynamically and transparently, meaning that the system can add new features without affecting the behavior of other objects.
Imagine a simple coffee-making application where you start with a basic coffee component and want to add various additives (like milk, sugar, or whipped cream) dynamically.
Interface Coffee {
Method cost() -> double
Method ingredients() -> String
}
Class SimpleCoffee implements Coffee {
Method cost() -> double {
Return 1
}
Method ingredients() -> String {
Return "Coffee"
}
}
To add new features or ingredients to the coffee, without the Decorator pattern, you might be tempted to extend the SimpleCoffee
class for each ingredient combination. This quickly leads to a proliferation of subclasses (like CoffeeWithMilk
, CoffeeWithSugar
, CoffeeWithMilkAndSugar
, etc.), making the system hard to maintain and extend.
Solving the Issue with the Decorator Design Pattern
The Decorator Design Pattern solves this issue by allowing you to “decorate” objects with new behaviors without affecting other objects of the same class. Decorators provide a flexible alternative to subclassing for extending functionality using:
“Is-a” Relationship: Think of it like this: a decorator is a type of the original object it’s decorating. For example, if you have a basic coffee, a flavored coffee (like vanilla coffee) is still a type of coffee. So, a decorator follows the same interface or extends the same base class as the object it decorates.
“Has-a” Relationship: This is more about what the decorator has. A decorator has an instance of the original object it’s decorating. Imagine you’re adding toppings to an ice cream cone. Each topping (like sprinkles or caramel sauce) is added to the cone. Similarly, decorators add their own functionality while keeping the original object intact.
By combining these relationships, decorators allow you to dynamically add or change the behavior of objects without changing their actual code. It’s like putting different toppings on an ice cream cone to make it tastier without altering the cone itself. This makes the Decorator pattern a powerful tool for adding flexibility and customization to your code.
Let’s see how we can solve this using Decorator Design Pattern.
How to Implement Decorator Design Pattern?
The Decorator Design Pattern consists of following components
- Component Interface: The Component Interface defines the interface for objects that can have responsibilities added to them dynamically. It establishes a common interface that both Concrete Components and Decorators adhere to, ensuring that they can be used interchangeably.
- Concrete Component: A Concrete Component defines an object to which additional responsibilities can be attached.It implements the Component Interface and represents the base object to which decorators can be added, providing the core functionality of the component.
- Decorator: Decorator maintains a reference to a Component object and defines an interface that conforms to the Component’s interface.It acts as a wrapper around the Component object, allowing new behaviour to be added dynamically without altering the interface of the original component, thus enabling flexible extension of functionality.
- Concrete Decorators: Concrete Decorators add responsibilities to the component. They extend the functionality of the base Component or previous decorators by adding specific behavior or modifying existing behavior, providing a way to enhance the functionality of components in a modular and reusable manner.
Step-by-Step Implementation
- Define the Component Interface: Define the Component Interface for the Decorator pattern, representing a coffee object. It specifies methods to retrieve the cost and ingredients of the coffee.
Interface Coffee
Method cost() -> double
Method ingredients() -> String
EndInterface
- Implement the Concrete Component: Implement the Concrete Component in the Decorator pattern, representing a basic coffee object without any decorations. It defines methods to return the cost (which is set to 1) and the ingredients (which is “Coffee”).
Class SimpleCoffee implements Coffee
Method cost() -> double
Return 1
EndMethod
Method ingredients() -> String
Return "Coffee"
EndMethod
EndClass
- Create the Abstract Decorator Class: Create a decorator abstarct class with is-a and has-a relationship.
// is-a relationship. CoffeeDecorator is of type Coffee
Abstract Class CoffeeDecorator implements Coffee {
// has-a relationship. CoffeeDecorator has an object of type Coffee
Protected final Coffee decoratedCoffee;
// Constructor to initialize the decoratedCoffee object
Constructor CoffeeDecorator(Coffee coffee) {
This.decoratedCoffee = coffee;
}
// Implementing the cost() method by delegating to the decoratedCoffee object
Method cost() -> double {
Return decoratedCoffee.cost();
}
// Implementing the ingredients() method by delegating to the decoratedCoffee object
Method ingredients() -> String {
Return decoratedCoffee.ingredients();
}
}
- Implement Concrete Decorators: Implement this decorator to implement various other decorators.
Class WithMilk extends CoffeeDecorator {
// is-a relationship. WithMilk is a CoffeeDecorator
// Constructor to initialize WithMilk decorator with a Coffee object
Constructor WithMilk(Coffee coffee) {
super(coffee);
}
// Override the cost method to add the cost of milk
Method cost() -> double {
}
// Override the ingredients method to add milk to the ingredients list
Method ingredients() -> String {
}
}
Class WithSugar extends CoffeeDecorator {
// is-a relationship. WithSugar is a CoffeeDecorator
// Constructor to initialize WithSugar decorator with a Coffee object
Constructor WithSugar(Coffee coffee) {
super(coffee);
}
// Override the cost method to add the cost of sugar
Method cost() -> double {
}
// Override the ingredients method to add sugar to the ingredients list
Method ingredients() -> String {
}
}
- Usage:
// Create a simple coffee object
Coffee myCoffee = new SimpleCoffee();
// Decorate the coffee object with milk
myCoffee = new WithMilk(myCoffee);
// Decorate the coffee object with sugar
myCoffee = new WithSugar(myCoffee);
// Output the cost and ingredients of the decorated coffee
Display("Cost: " + myCoffee.cost());
Display("Ingredients: " + myCoffee.ingredients());
Common Usage of the Decorator Design Pattern
The Decorator Pattern is widely used in:
- GUI Libraries: For adding behaviors to UI components without changing their core classes.
- Stream Decorations: In I/O libraries where input/output streams can be dynamically enhanced with features like buffering, line numbering, or compression.
- Web Development Frameworks: For dynamically adding responsibilities or behaviors to objects such as request handlers or middleware.
Benefits of the Decorator Design Pattern
- Flexibility: Allows for dynamic extension of an object’s behavior without making changes to the existing code.
- Modularity: Decorators can be mixed and matched to create new behaviors.
- Adherence to SOLID Principles: Specifically supports the Open/Closed Principle by allowing objects to be open for extension but closed for modification.
- Avoids Class Proliferation: Unlike subclassing, it doesn’t lead to an explosion of classes for every combination of behaviors.
Drwabacks of the Decorator Design Pattern
- Complexity: Can increase complexity by introducing multiple small classes, making the system harder to understand.
- Instantiation Management: The client needs to handle the decorator stack, which can complicate instantiation logic.
- Identification: Objects wrapped by decorators may get disguised, making it hard to identify their types or original states.
- Overuse: Overusing the pattern can lead to scenarios where simpler inheritance might have been sufficient, unnecessarily complicating the design.
Identifying Where To Apply Decorator Design Pattern
- Dynamic Extension: When you need to dynamically add as well as remove responsibilities to objects without affecting other objects.
- Avoiding Subclass Explosion: When extending a class hierarchy would result in an excessive number of subclasses for combination-based behaviors.
- Compliance with OCP: When maintenance and future extension are priorities, and you need to keep the system compliant with the Open/Closed Principle.
Key TakeAways
- Decorator pattern allows behaviour to be added or modified dynamically at runtime, providing a flexible alternative to subclassing for extending functionality.
- It adheres to the Single Responsibility Principle by keeping classes focused on specific tasks, making it easier to understand, maintain, and extend code.
- Decorators promote composition over inheritance, allowing behavior to be composed from smaller, reusable components, resulting in more flexible and maintainable code.
- Decorators maintain the same interface as the objects they decorate, ensuring that client code remains unaware of the specific decorators’ presence, thus promoting code interoperability and ease of use.
Similar Posts
What Is the Difference Between Factory and Abstract Factory Design Patterns?
When to Use Factory Design Pattern in Java?
FAQ
What is a real-world example of decorator design pattern?
A common real-world example of the Decorator design pattern is in graphical user interface (GUI) frameworks. Consider a text editor application where you can apply various formatting options such as bold, italic, underline, and color to text. Each formatting option acts as a decorator that adds specific styling to the text without altering its underlying structure. This allows users to dynamically apply and remove formatting options while keeping the original text content intact.
What is the difference between wrapper and decorator design pattern?
- In the wrapper design pattern, a class (the wrapper) contains an instance of another class and provides additional functionality or modifications to the wrapped object’s behavior. The wrapper typically modifies or extends the behavior of the wrapped object.
- In contrast, the decorator design pattern also involves wrapping an object, but it focuses on dynamically adding responsibilities or behaviors to objects without altering their original interface. Decorators add functionality in a flexible and transparent way, allowing for combinations of behaviors through nested wrappers.
What is the design pattern of decorator and visitor?
- The Decorator pattern enhances or alters the behavior of individual objects at runtime by wrapping them with one or more decorators.
- The Visitor pattern separates algorithms from the objects on which they operate by encapsulating them in visitor objects, allowing for new operations to be added without modifying the objects themselves. While both patterns involve adding functionality to objects, they serve different purposes and are applied in different contexts.
What is decorator pattern also known as?
The Decorator pattern is also known as the Wrapper pattern or the Decorator Wrapper pattern. These alternative names highlight the pattern’s role in wrapping or decorating objects with additional functionality.