Decorating Your Objects: A Guide to the Decorator Design Pattern


Imagine you're at Subway, ready to order your sandwich. You start with the basic sandwich, choosing your bread and selecting a filling – whether it's a hearty meat option or a delicious veggie alternative.
Suppose you want something extra – some cheese, extra veggies, and sauces.
Adding cheese to your sandwich at Subway is like adding a special touch, making it even more delicious.
In the world of the Decorator Pattern, this act of customization – adding these extra layers of cheese, veggies, and sauces – is what decorators are all about. It's like giving your sandwich a little upgrade, enhancing the flavor, and making it uniquely yours.
What is a Decorator Pattern?
A decorator pattern is a structural design pattern that allows new behaviors to be added to existing objects by placing these objects inside special wrapper/decorator objects that contain the new behaviors. This is often used to extend the functionalities of classes in a flexible and reusable way.
Why do we need this?
Whenever we come across a use case to add new functionality to an existing class, the first thing that comes to our mind is Inheritance, correct?
Consider the above Subway example, to add extra cheese to the base sandwich we can create a subclass BaseSandwich+Cheese
and our task will be done.
Now to add veggies we can create BaseSandwich+Veggies
To add veggies and cheese, we can create BaseSandwich+Veggies+Cheese.
But is this approach efficient? With every combination, there will be a new class which will lead to a class explosion.
Solution
The decorator pattern comes into the picture in these cases, where instead of inheritance we will make use of aggregation, and we will understand everything in the below example.
Let's design a coffee ordering system where customers can customize their coffee with various additives. We'll implement the Decorator Design Pattern to handle these customizations.
Requirements for Coffee Ordering System:
Basic Coffee:
The system should allow customers to order a basic coffee.
The cost of a basic coffee should be set to 10.
Add Milk:
Customers should have the option to add extra milk to their coffee.
The cost of the coffee should increase by 3.5 when adding extra milk.
Add Sugar:
Customers should have the option to add extra sugar to their coffee.
The cost of the coffee should increase by 1.0 when adding extra sugar.
Combining Additives:
Customers should be able to combine multiple additives to customize their coffee.
For example, a customer should be able to order a coffee with both extra milk and extra sugar.
Total Cost Calculation:
The system should calculate the total cost of the ordered coffee based on the selected options.
The total cost should reflect the sum of the costs associated with the chosen additives.
Class Diagram
Coffee:
Represents the base component or interface for different types of coffee.
Defines two methods:
getDescription()
(providing a textual description of the coffee) andgetCost()
(returning the cost of the coffee).
BasicCoffee:
A concrete class that implements the
Coffee
interface.Represents the basic coffee without any additives.
Provides specific implementations for
getDescription()
andgetCost()
.
CoffeeDecorator:
An abstract class that also implements the
Coffee
interface.Contains a private field,
coffee
representing the wrapped coffee component or the coffee that came for additives.Defines methods to get the description and cost, delegating these operations to the wrapped
coffee
component.Relationships:
To add extra features to the coffee, the
CoffeeDecorator
needs to have the coffee instance, so we have a HAS-A relationship(aggregation) with theCoffee
interfaceOnce additives are added, it will still be a Coffee, so IS-A relationship with
Coffee
interface as well.
MilkDecorator:
A concrete decorator class that extends
CoffeeDecorator
.Adds extra functionality to the coffee (in this case, the ability to add milk).
Overrides the methods to modify the description and cost based on the addition of milk.
SugarDecorator:
Another concrete decorator class extending
CoffeeDecorator
.Adds the functionality to add sugar to the coffee.
Overrides methods to adjust the description and cost accordingly.
Implementation
1. Coffee Interface:
public interface Coffee {
String getDescription();
double getCost();
}
2. BasicCoffee Class:
public class BasicCoffee implements Coffee {
@Override
public String getDescription() {
return "Basic Coffee";
}
@Override
public double getCost() {
return 10;
}
}
This is the concrete implementation of the Coffee
interface, representing a basic coffee. It provides a description ("Basic Coffee") and has a cost of 10.
3. CoffeeDecorator Abstract Class:
abstract class CoffeeDecorator implements Coffee {
private Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public double getCost() {
return coffee.getCost();
}
}
This is an abstract class that implements the Coffee
interface. It contains an instance variable coffee
of type Coffee
. It serves as the base class for all decorators.
4. MilkDecorator Class:
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
public String getDescription() {
return super.getDescription() + " with extra Milk";
}
public double getCost() {
return super.getCost() + 3.5;
}
}
This is a concrete decorator that extends CoffeeDecorator
. It adds functionality to the coffee, in this case, extra milk. It overrides getDescription()
to append " with extra Milk" and getCost()
to add the cost of extra milk.
5. SugarDecorator Class:
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
public String getDescription() {
return super.getDescription() + " with extra Sugar";
}
public double getCost() {
return super.getCost() + 1.0;
}
}
Similar to MilkDecorator
, this is another concrete decorator that adds functionality for extra sugar.
6. DecoratorExample Class:
javaCopy codepublic class DecoratorExample {
public static void main(String[] args) {
Coffee basicCoffee = new BasicCoffee();
printInvoice(basicCoffee);
Coffee basicCoffeeWithExtraMilk = new MilkDecorator(basicCoffee);
printInvoice(basicCoffeeWithExtraMilk);
Coffee basicCoffeeWithExtraMilkAndSugar = new SugarDecorator(new MilkDecorator(basicCoffee));
printInvoice(basicCoffeeWithExtraMilkAndSugar);
}
private static void printInvoice(Coffee coffee) {
System.out.println("Order: " + coffee.getDescription());
System.out.println("Cost: " + coffee.getCost());
System.out.println("--------");
}
}
This is the main class demonstrating the usage of the Decorator Pattern. It creates instances Coffee
with various decorators and prints their descriptions and costs.
Execution
Let's break down the execution flow of the line:
Coffee basicCoffeeWithExtraMilkAndSugar = new SugarDecorator(new MilkDecorator(basicCoffee));
basicCoffee
Creation:basicCoffee
is an instance of theBasicCoffee
class, representing a basic coffee without any additives.
MilkDecorator
Wrapping:new MilkDecorator(basicCoffee)
creates a new instance ofMilkDecorator
and wraps it around the existingbasicCoffee
.Now,
basicCoffee
is enclosed within theMilkDecorator
, allowing the addition of milk functionalities.
SugarDecorator
Wrapping:new SugarDecorator(new MilkDecorator(basicCoffee))
further wraps the result from the previous step (MilkDecorator
) with aSugarDecorator
.This nesting allows for multiple decorators to be stacked, each extending the functionalities of the previous one.
basicCoffeeWithExtraMilkAndSugar
:The final result is assigned to the variable
basicCoffeeWithExtraMilkAndSugar
.This variable represents a complex composition of decorators around the original
basicCoffee
instance.
Execution Flow for getCost:
Let's break down the
getCost()
flow step by step for thebasicCoffeeWithExtraMilkAndSugar
:SugarDecorator.getCost()
:The
getCost()
method ofSugarDecorator
is called.It calls
getCost()
on the wrapped decorator (MilkDecorator
), adding its cost to the total.
MilkDecorator.getCost()
:The
getCost()
method ofMilkDecorator
is called.It calls
getCost()
on the wrapped coffee (basicCoffee
), adding its cost to the total.
BasicCoffee.getCost()
:The
getCost()
methodBasicCoffee
is called.It returns the base cost of
10
.
Adding Costs:
The cost of
BasicCoffee
is10
.The cost of
MilkDecorator
is3.5
.The cost of
SugarDecorator
is1.0
.The final total cost is the sum of these:
10 + 3.5 + 1.0 = 14.5
.
So, the getCost()
flow is: SugarDecorator -> MilkDecorator -> BasicCoffee
, and the final cost is 14.5
.
This is all about decorator design pattern, I hope you find this useful.
Do give it a like and share it among your friends, if you will this was helpful.
Till then, Happy Coding!
Complete implementations can be found on Github
##
Subscribe to my newsletter
Read articles from Ritesh Panigrahi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ritesh Panigrahi
Ritesh Panigrahi
🙌Hi, I'm Ritesh. Software Engineer at Walmart. Documenting my learning journey!