The Decorator Pattern is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors.
Think of it like wearing clothes. You are a person (the object). When you are cold, you put on a sweater (the decorator). When it rains, you put on a raincoat. You are still a "Person," but you now have the added property of being "warm" and "waterproof."
The Problem
Imagine you are building a Coffee Shop ordering system for your website. You have a base Coffee class. Customers can add extras: Milk, Sugar, Whipped Cream, or Caramel.
If you use inheritance, you get a "Class Explosion":
CoffeeWithMilkCoffeeWithMilkAndSugarCoffeeWithWhippedCreamAndCaramel...and so on. Every time you add a new topping, you'd have to create dozens of new classes.
The Solution
Instead of inheritance, you use Composition. You create a base Coffee and "wrap" it with decorators. Each decorator adds its own cost and description to the original object.
Step-by-Step Java Implementation
1. The Component Interface
This defines the behavior for both the base object and the decorators.
// Coffee.java
public interface Coffee {
String getDescription();
double getCost();
}
2. The Concrete Component
The basic object that can have responsibilities added to it.
// SimpleCoffee.java
public class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple Coffee";
}
@Override
public double getCost() {
return 2.00;
}
}
3. The Base Decorator
This class implements the Coffee interface and contains a reference to a Coffee object.
// CoffeeDecorator.java
public abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
public String getDescription() {
return decoratedCoffee.getDescription();
}
public double getCost() {
return decoratedCoffee.getCost();
}
}
4. Concrete Decorators
These classes actually add the new behavior/state.
// MilkDecorator.java
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return super.getDescription() + ", Milk";
}
@Override
public double getCost() {
return super.getCost() + 0.50;
}
}
// SugarDecorator.java
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return super.getDescription() + ", Sugar";
}
@Override
public double getCost() {
return super.getCost() + 0.20;
}
}
Full Code for Testing
// Save as DecoratorTest.java
interface Coffee {
String getDescription();
double getCost();
}
class SimpleCoffee implements Coffee {
public String getDescription() { return "Plain Coffee"; }
public double getCost() { return 2.0; }
}
abstract class CoffeeDecorator implements Coffee {
protected Coffee tempCoffee;
public CoffeeDecorator(Coffee coffee) { this.tempCoffee = coffee; }
public String getDescription() { return tempCoffee.getDescription(); }
public double getCost() { return tempCoffee.getCost(); }
}
class Milk extends CoffeeDecorator {
public Milk(Coffee coffee) { super(coffee); }
public String getDescription() { return super.getDescription() + " + Milk"; }
public double getCost() { return super.getCost() + 0.5; }
}
class Sugar extends CoffeeDecorator {
public Sugar(Coffee coffee) { super(coffee); }
public String getDescription() { return super.getDescription() + " + Sugar"; }
public double getCost() { return super.getCost() + 0.2; }
}
public class DecoratorTest {
public static void main(String[] args) {
System.out.println("--- Coffee Shop Order ---\n");
// Order 1: Plain Coffee
Coffee basic = new SimpleCoffee();
System.out.println("Order: " + basic.getDescription() + " | Cost: $" + basic.getCost());
// Order 2: Coffee with Milk and Sugar
// We wrap the objects like an onion
Coffee fancyCoffee = new Sugar(new Milk(new SimpleCoffee()));
System.out.println("Order: " + fancyCoffee.getDescription());
System.out.println("Total Cost: $" + fancyCoffee.getCost());
System.out.println("\n--- Order Complete ---");
}
}
Why use Decorator?
- Flexibility: You can add or remove responsibilities from an object at runtime.
- Open/Closed Principle: You can add new decorators to change an object's behavior without modifying the original class.
- No Class Explosion: You don't need a unique class for every possible combination of features.
Real-World Example
The Java I/O library is the most famous example:
BufferedReader reader = new BufferedReader(new FileReader("file.txt"));
Here, BufferedReader is a decorator that adds buffering capabilities to the standard FileReader.