Decorator Design Pattern in Java
Introduction
In software development, there are situations where you want to add new functionality to objects dynamically without modifying their original code. The Decorator Pattern is a structural design pattern that provides a flexible alternative to subclassing for extending functionality.
Think of it like customizing a coffee order: you start with a basic coffee and dynamically add milk, sugar, or chocolate toppings without altering the base coffee class.
Definition
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.
Key Idea: Wrap objects with decorator classes that enhance or modify behavior.
When to Use the Decorator Pattern
- When you need to add responsibilities to objects at runtime.
- When subclassing would result in an explosion of classes for every combination of behaviors.
- When you want to extend object functionality in a flexible and reusable way.
Real-World Example
- Coffee Shop: Base coffee can have milk, sugar, whipped cream, or chocolate toppings. Each topping can be applied independently.
- Graphical User Interfaces (GUI): A TextView can have borders, scrollbars, or shadows added dynamically using decorators.
Java Implementation
Let’s implement a coffee shop example using the Decorator Pattern.
Step 1: Define the Component Interface
// Component interface
interface Coffee {
String getDescription();
double getCost();
}
Step 2: Create the Concrete Component
// Concrete Component
class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple Coffee";
}
@Override
public double getCost() {
return 5.0;
}
}
Step 3: Define the Decorator Abstract Class
// Decorator abstract class
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee; // The object being decorated
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
public String getDescription() {
return coffee.getDescription(); // Delegate to wrapped object
}
public double getCost() {
return coffee.getCost(); // Delegate to wrapped object
}
}
Step 4: Create Concrete Decorators
// Milk decorator
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Milk";
}
@Override
public double getCost() {
return coffee.getCost() + 1.5;
}
}
// Sugar decorator
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Sugar";
}
@Override
public double getCost() {
return coffee.getCost() + 0.5;
}
}
Step 5: Test the Decorator Pattern
public class Main {
public static void main(String[] args) {
Coffee simpleCoffee = new SimpleCoffee();
System.out.println(simpleCoffee.getDescription() + " → $" + simpleCoffee.getCost());
Coffee milkCoffee = new MilkDecorator(simpleCoffee);
System.out.println(milkCoffee.getDescription() + " → $" + milkCoffee.getCost());
Coffee milkSugarCoffee = new SugarDecorator(milkCoffee);
System.out.println(milkSugarCoffee.getDescription() + " → $" + milkSugarCoffee.getCost());
}
}
Output:
Simple Coffee → $5.0
Simple Coffee, Milk → $6.5
Simple Coffee, Milk, Sugar → $7.0
Advantages of the Decorator Pattern
- Adds Flexibility: Modify object behavior at runtime without altering the base class.
- Reduces Class Explosion: Avoid creating multiple subclasses for each combination of features.
- Promotes Open/Closed Principle: Classes are open for extension but closed for modification.
- Enhances Reusability: Decorators can be combined in different ways to achieve varied behavior.
Disadvantages
- Complexity: Multiple layers of wrapping can make the system harder to understand.
- Debugging Difficulty: Tracing decorated objects can be challenging.
- Overhead: Each decorator introduces additional object references, which may slightly impact performance.
Summary
The Decorator Pattern is ideal when you need dynamic behavior modification of objects. By wrapping objects with decorators, it allows flexible, reusable, and extensible design without altering existing code.
Next, we will explore the Facade Pattern, which provides a simplified interface to a complex subsystem, making it easier for clients to interact with large systems.