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).