Introduction
The Decorator pattern is a structural design pattern that allows behavior to be added to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class. This pattern is particularly useful for adhering to the Single Responsibility Principle by allowing functionality to be divided between classes with unique areas of concern.
Key Concepts
- Component: The interface or abstract class defining the methods that will be implemented.
- Concrete Component: The class that implements the Component interface.
- Decorator: An abstract class that implements the Component interface and contains a reference to a Component object.
- Concrete Decorators: Classes that extend the Decorator class and add functionalities to the Component.
Structure
The Decorator pattern can be visualized with the following UML diagram:
Component + operation() | ConcreteComponent + operation() | Decorator - component: Component + operation() | ConcreteDecoratorA - addedState: Type + operation() + addedBehavior() | ConcreteDecoratorB - addedState: Type + operation() + addedBehavior()
Example
Let's consider a simple example where we have a Coffee
interface and we want to add different types of condiments to the coffee dynamically.
Step 1: Define the Component Interface
Step 2: Create a Concrete Component
public class SimpleCoffee implements Coffee { @Override public double cost() { return 5.0; } @Override public String description() { return "Simple Coffee"; } }
Step 3: Create the Decorator Abstract Class
public abstract class CoffeeDecorator implements Coffee { protected Coffee decoratedCoffee; public CoffeeDecorator(Coffee coffee) { this.decoratedCoffee = coffee; } @Override public double cost() { return decoratedCoffee.cost(); } @Override public String description() { return decoratedCoffee.description(); } }
Step 4: Create Concrete Decorators
public class MilkDecorator extends CoffeeDecorator { public MilkDecorator(Coffee coffee) { super(coffee); } @Override public double cost() { return super.cost() + 1.5; } @Override public String description() { return super.description() + ", Milk"; } } public class SugarDecorator extends CoffeeDecorator { public SugarDecorator(Coffee coffee) { super(coffee); } @Override public double cost() { return super.cost() + 0.5; } @Override public String description() { return super.description() + ", Sugar"; } }
Step 5: Use the Decorators
public class Main { public static void main(String[] args) { Coffee coffee = new SimpleCoffee(); System.out.println(coffee.description() + " $" + coffee.cost()); coffee = new MilkDecorator(coffee); System.out.println(coffee.description() + " $" + coffee.cost()); coffee = new SugarDecorator(coffee); System.out.println(coffee.description() + " $" + coffee.cost()); } }
Output
Practical Exercises
Exercise 1: Implement a Decorator
Task: Implement a WhippedCreamDecorator
that adds whipped cream to the coffee.
Solution:
public class WhippedCreamDecorator extends CoffeeDecorator { public WhippedCreamDecorator(Coffee coffee) { super(coffee); } @Override public double cost() { return super.cost() + 2.0; } @Override public String description() { return super.description() + ", Whipped Cream"; } }
Exercise 2: Chain Multiple Decorators
Task: Create a SimpleCoffee
and add Milk
, Sugar
, and WhippedCream
to it. Print the final description and cost.
Solution:
public class Main { public static void main(String[] args) { Coffee coffee = new SimpleCoffee(); coffee = new MilkDecorator(coffee); coffee = new SugarDecorator(coffee); coffee = new WhippedCreamDecorator(coffee); System.out.println(coffee.description() + " $" + coffee.cost()); } }
Output
Common Mistakes
- Not using the Decorator pattern correctly: Ensure that the decorators are extending the abstract decorator class and not the concrete component directly.
- Forgetting to call the super methods: When overriding methods in the decorator, always call the corresponding method of the decorated component to maintain the chain of responsibility.
Conclusion
The Decorator pattern is a powerful tool for extending the functionality of objects dynamically. It promotes code reusability and adheres to the Single Responsibility Principle by allowing functionalities to be divided among different classes. By practicing the exercises and understanding the examples, you should now have a solid grasp of how to implement and use the Decorator pattern in your projects.
Software Design Patterns Course
Module 1: Introduction to Design Patterns
- What are Design Patterns?
- History and Origin of Design Patterns
- Classification of Design Patterns
- Advantages and Disadvantages of Using Design Patterns
Module 2: Creational Patterns
Module 3: Structural Patterns
Module 4: Behavioral Patterns
- Introduction to Behavioral Patterns
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
Module 5: Application of Design Patterns
- How to Select the Right Pattern
- Practical Examples of Pattern Usage
- Design Patterns in Real Projects
- Refactoring Using Design Patterns
Module 6: Advanced Design Patterns
- Design Patterns in Modern Architectures
- Design Patterns in Microservices
- Design Patterns in Distributed Systems
- Design Patterns in Agile Development