decorator pattern in Java
The Decorator pattern is a structural pattern that allows you to add new functionalities or responsibilities to an object dynamically without altering its core structure. It achieves this by wrapping the original object within a special decorator object.
It's an alternative to inheritance for extending functionality.
Real-Life Analogy: Ordering Coffee ☕
Imagine ordering a basic coffee. You can add extra features (decorations) like milk, whipped cream, or flavor shots to the basic coffee without changing the coffee itself. Each addition wraps the previous one.
The Structure
| Component | Role in Coffee Analogy |
|---|---|
Coffee | Component Interface: Defines the core product and methods (cost(), getDescription()). |
SimpleCoffee | Concrete Component: The basic, undecorated object (the plain black coffee). |
CoffeeDecorator | Decorator Abstract Class: Holds a reference to the Coffee object it's wrapping and implements the Coffee interface. |
Milk / Whip | Concrete Decorators: Add specific behavior/cost to the wrapped coffee. |
Java Code Example: Coffee Shop
1. The Component Interface
This defines the contract for all coffee items, decorated or not.
// 1. Component Interface
interface Coffee {
double cost();
String getDescription();
}
2. The Concrete Component
The basic, core object.
// 2. Concrete Component
class SimpleCoffee implements Coffee {
@Override
public double cost() {
return 5.00; // Base cost
}
@Override
public String getDescription() {
return "Simple Coffee";
}
}
3. The Abstract Decorator
This is the key piece: it implements the Coffee interface and wraps another Coffee object.
// 3. Decorator Abstract Class
abstract class CoffeeDecorator implements Coffee {
protected final Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
// Delegates to the wrapped object by default
@Override
public double cost() {
return decoratedCoffee.cost();
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}
}
4. Concrete Decorators
These add the specific new behavior and cost.
// 4. Concrete Decorator 1: Adds Milk
class Milk extends CoffeeDecorator {
public Milk(Coffee coffee) {
super(coffee);
}
@Override
public double cost() {
// Adds $1.00 to the wrapped coffee's cost
return super.cost() + 1.00;
}
@Override
public String getDescription() {
// Adds "with Milk" to the wrapped coffee's description
return super.getDescription() + ", with Milk";
}
}
// 4. Concrete Decorator 2: Adds Whipped Cream
class Whip extends CoffeeDecorator {
public Whip(Coffee coffee) {
super(coffee);
}
@Override
public double cost() {
// Adds $1.50 to the wrapped coffee's cost
return super.cost() + 1.50;
}
@Override
public String getDescription() {
// Adds "with Whip" to the wrapped coffee's description
return super.getDescription() + ", with Whip";
}
}
5. Client Usage
The client can stack decorators to create complex objects.
public class DecoratorDemo {
public static void main(String[] args) {
// Start with a basic coffee
Coffee myCoffee = new SimpleCoffee();
System.out.println("Order 1: " + myCoffee.getDescription() + " - $" + myCoffee.cost());
// Output: Order 1: Simple Coffee - $5.0
// Wrap the coffee with Milk
myCoffee = new Milk(myCoffee);
System.out.println("Order 2: " + myCoffee.getDescription() + " - $" + myCoffee.cost());
// Output: Order 2: Simple Coffee, with Milk - $6.0
// Now, wrap the Milk-coffee with Whip (stacking)
myCoffee = new Whip(myCoffee);
System.out.println("Order 3: " + myCoffee.getDescription() + " - $" + myCoffee.cost());
// Output: Order 3: Simple Coffee, with Milk, with Whip - $7.5
}
}
This stacking is why the Decorator pattern is used extensively in Java I/O streams (e.g., wrapping a FileInputStream in a BufferedInputStream to add buffering functionality).