Skip to main content

The Decorator Pattern

Category: Structural

The Decorator pattern allows you to dynamically attach new behaviors or responsibilities to an object at runtime by wrapping it in an object of a decorator class.

This pattern is a highly flexible alternative to subclassing (inheritance) for extending functionality.


The Problem with Subclassing

Imagine you have a Coffee class. Now you want to add options for Milk, Sugar, and Caramel.

If you use inheritance, you will end up creating an explosive number of subclasses: MilkCoffee, SugarCoffee, MilkAndSugarCoffee, CaramelCoffee, etc. Every new ingredient doubles the number of classes!

The Decorator pattern solves this by wrapping the base Coffee object inside decorator objects (MilkDecorator, SugarDecorator) at runtime.


Example: The Coffee Shop

1. The Component Interface

This is the base interface that both the concrete component and the decorators will implement.

public interface Coffee {
String getDescription();
double getCost();
}

2. The Concrete Component

This is the core object we want to wrap (decorate).

public class SimpleCoffee implements Coffee {

@Override
public String getDescription() {
return "Simple Coffee";
}

@Override
public double getCost() {
return 2.00;
}
}

3. The Base Decorator Class

The base decorator implements the component interface and holds a reference to a wrapped component object.

public abstract class CoffeeDecorator implements Coffee {

// The object being decorated
protected final Coffee decoratedCoffee;

public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}

// Delegate the work to the wrapped object by default
public String getDescription() {
return decoratedCoffee.getDescription();
}

public double getCost() {
return decoratedCoffee.getCost();
}
}

4. Concrete Decorators

These extend the base decorator and add their own specific behaviors (like adding cost or description) before or after delegating to the wrapped object.

public class MilkDecorator extends CoffeeDecorator {

public MilkDecorator(Coffee coffee) {
super(coffee);
}

@Override
public String getDescription() {
// Add "Milk" to the existing description
return super.getDescription() + ", Milk";
}

@Override
public double getCost() {
// Add $0.50 to the existing cost
return super.getCost() + 0.50;
}
}

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;
}
}

5. Client Code

Now look at how we can infinitely combine ingredients at runtime without creating a single new class!

public class LabDecorator1 {

public static void main(String[] args) {
// 1. Order a simple coffee
Coffee myCoffee = new SimpleCoffee();
System.out.println(myCoffee.getDescription() + " $" + myCoffee.getCost());

// 2. Wrap it in Milk!
myCoffee = new MilkDecorator(myCoffee);
System.out.println(myCoffee.getDescription() + " $" + myCoffee.getCost());

// 3. Wrap it in Sugar!
myCoffee = new SugarDecorator(myCoffee);
System.out.println(myCoffee.getDescription() + " $" + myCoffee.getCost());

// Output:
// Simple Coffee $2.0
// Simple Coffee, Milk $2.5
// Simple Coffee, Milk, Sugar $2.7
}
}

[!NOTE] Have you ever wondered why Java's I/O library looks like this? new BufferedReader(new InputStreamReader(new FileInputStream("file.txt")))

It's because the entire java.io package was explicitly designed using the Decorator Pattern! A basic FileInputStream is wrapped in an InputStreamReader (which adds char translation), which is wrapped in a BufferedReader (which adds buffering capabilities).