Skip to Content
Course content

152: The Decorator Pattern

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably encountered situations where you want to add functionality to an object, but you don't want to create a massive inheritance tree to do it. If you have a base class and ten different optional features, creating a subclass for every possible combination of those features is a nightmare. That's where the Decorator pattern comes in. It lets us "wrap" an object in another object that adds behavior, and we can stack these wrappers as deep as we want.

Avoiding the Subclass Explosion

Let's imagine we're building a system for a coffee shop. We have a basic coffee, but customers can add milk, sugar, whip cream, or caramel. If we used standard inheritance, we'd end up with classes like CoffeeWithMilk, CoffeeWithMilkAndSugar, and CoffeeWithMilkSugarAndWhip. It's a mess. Instead, we'll define a common interface that both the base coffee and the "decorators" implement.

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

Now, we create our simplest possible version of a beverage. This is the "concrete component" that we'll be decorating later.

public class SimpleCoffee implements Beverage {
    @Override
    public String getDescription() {
        return "Simple Coffee";
    }

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

Wrapping Behavior with Decorators

To make the decorator work, we need a base decorator class. This class is a Beverage, but it also has a Beverage. This is the secret sauce: the decorator implements the interface it's decorating, which allows it to be passed around as if it were the original object.

public abstract class BeverageDecorator implements Beverage {
    protected Beverage decoratedBeverage;

    public BeverageDecorator(Beverage beverage) {
        this.decoratedBeverage = beverage;
    }

    public String getDescription() {
        return decoratedBeverage.getDescription();
    }

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

Now we can create specific additions, like milk. The MilkDecorator overrides the methods to add its own specific cost and description to whatever is already there.

public class MilkDecorator extends BeverageDecorator {
    public MilkDecorator(Beverage beverage) {
        super(beverage);
    }

    @Override
    public String getDescription() {
        return decoratedBeverage.getDescription() + ", Milk";
    }

    @Override
    public double getCost() {
        return decoratedBeverage.getCost() + 0.50;
    }
}

Where I accidentally gave away free coffee

When I first implemented this in a project a few years back, I made a classic rookie mistake. I forgot that the decorator's job is to augment the existing value, not replace it. I wrote my SugarDecorator like this:

// DON'T DO THIS
public class SugarDecorator extends BeverageDecorator {
    public SugarDecorator(Beverage beverage) { super(beverage); }

    @Override
    public double getCost() {
        return 0.20; // I forgot to add decoratedBeverage.getCost()!
    }
}

I spent twenty minutes wondering why my "Coffee with Milk and Sugar" only cost 20 cents. I had completely wiped out the cost of the coffee and the milk by returning only the price of the sugar. Always remember: a decorator should almost always call the wrapped object's method and then add its own logic to the result.

Stacking the Flavors

The real beauty of this pattern is how we use it at runtime. We don't need a CoffeeWithMilkAndSugar class because we can just wrap the objects in the order we want.

public class CoffeeShop {
    public static void main(String[] args) {
        // Start with a simple coffee
        Beverage myCoffee = new SimpleCoffee();
        
        // Wrap it in milk
        myCoffee = new MilkDecorator(myCoffee);
        
        // Wrap that in sugar
        myCoffee = new SugarDecorator(myCoffee);
        
        System.out.println("Order: " + myCoffee.getDescription());
        System.out.println("Total: $" + myCoffee.getCost());
    }
}

In this example, myCoffee is now a SugarDecorator wrapping a MilkDecorator wrapping a SimpleCoffee. When you call getCost(), it triggers a chain reaction: the sugar asks the milk for the price, the milk asks the coffee, the coffee returns 2.00, the milk adds 0.50, and the sugar adds 0.20. Clean, flexible, and no class explosion.




📋 Practical Task

Implement a Pizza Topping System

You are tasked with creating a customizable Pizza ordering system using the Decorator Pattern. Follow these requirements:

  • Create a Pizza interface with methods String getDescription() and double getCost().
  • Implement a PlainPizza class that costs $8.00.
  • Create an abstract PizzaDecorator class that implements Pizza and holds a reference to a Pizza object.
  • Implement two concrete decorators: CheeseDecorator (adds $1.50) and PepperoniDecorator (adds $2.00).
  • In a Main class, simulate a customer ordering a pizza with both cheese and pepperoni, and print the final description and cost to the console.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.