The Decorator pattern enables the dynamic addition of behaviors to an individual object without altering its underlying structure or inheritance hierarchy. It provides a flexible alternative to subclassing by wrapping objects with decorator classes that perform additional tasks before or after delegating to the wrapped component.
Architectural Components
- Component: An interface or abstract class defining the contract for objects that can receive dynamic additions.
- ConcreteComponent: The base implementation of the component that can be decorated.
- Decorator: An abstract class that implements the Component interface while holding a reference to a Component object.
- ConcreteDecorator: Classes that extend the Decorator to add specific responsibilities or modifications to the wrapped component.
Implementation Example: Customizing Food Orders
Consider a restaurant system where customers can order base items like noodles or rice and add extra toppings, each with its own cost. Using the Decorator pattern avoids creating a complex subclass hierarchy for every possible combination.
public abstract class Menu {
public abstract String getLabel();
public abstract double calculatePrice();
}
public class Rice extends Menu {
public String getLabel() { return "Rice"; }
public double calculatePrice() { return 5.0; }
}
public abstract class ToppingDecorator extends Menu {
protected Menu baseOrder;
public ToppingDecorator(Menu base) { this.baseOrder = base; }
}
public class EggTopping extends ToppingDecorator {
public EggTopping(Menu base) { super(base); }
public String getLabel() { return baseOrder.getLabel() + " + Egg"; }
public double calculatePrice() { return baseOrder.calculatePrice() + 1.5; }
}
public class Main {
public static void main(String[] args) {
Menu myOrder = new Rice();
myOrder = new EggTopping(myOrder);
System.out.println(myOrder.getLabel() + ": $" + myOrder.calculatePrice());
}
}
Advantages
- Scalability: It avoids "subclass explosion" where unique combinations of features lead to an unmanageable number of subclasses.
- Adherence to Open/Closed Principle: New functionality can be introduced without modifying existing code.
- Dynamic Composition: Behaviors can be added or removed at runtime, unlike inheritance which is fixed at compile-time.
Typical Use Cases
- Extending objects when inheritance is prohibited (e.g., final classes) or impractical.
- Adding responsibilities to objects that need to be transparent to the rest of the system.
- Scenarios where features need to be toggled on or off dynamically during runtime execution.
Decorator Pattern in Java I/O
Standard Java library classes, specifically the I/O stream hierarchy, utilize this pattern. For instance, BufferedWriter wraps a Writer instance to add buffering capabilities, thereby improving performance without altering the core writing logic of the underlying stream.
Writer fileWriter = new FileWriter("data.txt");
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
bufferedWriter.write("Enhanced write performance");
bufferedWriter.close();
Decorator vs. Static Proxy
While both patterns implement a common interface and hold a reference to a target object, they serve different design goals:
- Purpose: The Decorator is primarily concerned with enhancing the functionality of the target. A Static Proxy is typically used to control access, manage lifecycle, or hide the original object.
- Instantiation: In a Decorator, the component is past to the constructor from an external source. In a Static Proxy, the proxy often manages the instantiation of the target internally, effectively shielding the client from the implementation details.