Definition
The Decorator pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Real World Analogy
Consider we are running a coffee shop. In the shop, we offer a variety of coffee along with options for customers to add extras like extra milk, sugar, coffee powder, and more.
If a customer buys only a Plain Coffee, they should be charged only the price of the Plain Coffee. However, if the customer wants extra coffee powder (or any other add-on) in their Plain Coffee or any other type of coffee, the additional cost of the add-on should be applied on top of the base cost of the selected coffee.
To design such a structure, we implement the Decorator Pattern. This approach eliminates the need for if-else statements in the classes. Instead, we decorate the object within the same class, maintaining flexibility and scalability. See the Example below:
Coffee Prices Chart
The Image shows how the cost is incurred if user adds extras to his coffee. Below is the Implementation of such pattern.
Design
--- title: Decorator Pattern --- classDiagram direction TB Concrete Coffee implementations class PlainCoffee { +getDescription() String +getCost() int } class CappuccinoCoffee { +getDescription() String +getCost() int } Concrete Decorators class MilkDecorator { +MilkDecorator(decoratedCoffee: Coffee) +getDescription() String +getCost() int } class SugarDecorator { +SugarDecorator(decoratedCoffee: Coffee) +getDescription() String +getCost() int } PlainCoffee --|> Coffee CappuccinoCoffee --|> Coffee CoffeeDecorator --|> Coffee MilkDecorator --|> CoffeeDecorator SugarDecorator --|> CoffeeDecorator CoffeeDecorator --> Coffee : _coffee
Design of the Coffee Shop
Coding Decorator Pattern
// Coffee.java
interface Coffee {
public String getDescription();
public int getCost();
}
The Coffee
interface acts as an abstraction layer between the variety of coffee types. By implementing this interface, you can create as many coffee varieties as needed.
// PlainCoffee.java
class PlainCoffee implements Coffee {
@Override
public String getDescription() {
return "PlainCoffee";
}
@Override
public int getCost() {
return 10;
}
}
// CappuccinoCoffee
class CappuccinoCoffee implements Coffee {
@Override
public String getDescription() {
return "Cappuccino";
}
@Override
public int getCost() {
return 20;
}
}
In the code example above, the Coffee
interface is implemented by both the PlainCoffee and CappuccinoCoffee concrete classes. For each coffee type, we override the methods and assign the specific cost for each coffee.
// CoffeeDecorator.java
abstract class CoffeeDecorator implements Coffee {
protected Coffee _coffee;
public CoffeeDecorator(Coffee coffee) {
_coffee = coffee;
}
@Override
public String getDescription() {
return _coffee.getDescription();
}
@Override
public int getCost() {
return _coffee.getCost();
}
}
The CoffeeDecorator
class is the decorator class that implements the Coffee
interface to decorate different varieties of coffee. In our case, the CoffeeDecorator acts as an add-on (e.g., milk, sugar) that can be added to the customer’s coffee if requested.
// MilkDecorator.java
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee decoratedcoffee) {
super(decoratedcoffee);
}
@Override
public String getDescription() {
return super.getDescription() + " + Milk";
}
@Override
public int getCost() {
return super.getCost() + 5;
}
}
// SugarDecorator.java
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public int getCost() {
return super.getCost() + 10;
}
@Override
public String getDescription() {
return super.getDescription() + " + Sugar";
}
}
When creating coffee objects, you can layer decorators as needed. For example, a Cappuccino with milk and sugar is created by wrapping the decorators around the base coffee object.
public class DecoratorPattern {
public static void main(String[] args) {
Coffee plaincoffee = new PlainCoffee();
System.out.println(plaincoffee.getDescription() + " Cost=" + plaincoffee.getCost());
Coffee capuccinocoffee = new CappuccinoCoffee();
capuccinocoffee = new MilkDecorator(capuccinocoffee);
capuccinocoffee = new SugarDecorator(capuccinocoffee);
System.out.println(capuccinocoffee.getDescription() + " Cost=" + capuccinocoffee.getCost());
}
}
Output:
PlainCoffee Cost=10
Cappuccino + Milk + Sugar Cost=35
Complete Code In Java
package decorator;
// Coffee.java
interface Coffee {
public String getDescription();
public int getCost();
}
// PlainCoffee.java
class PlainCoffee implements Coffee {
@Override
public String getDescription() {
return "PlainCoffee";
}
@Override
public int getCost() {
return 10;
}
}
// CappuccinoCoffee
class CappuccinoCoffee implements Coffee {
@Override
public String getDescription() {
return "Cappuccino";
}
@Override
public int getCost() {
return 20;
}
}
// CoffeeDecorator.java
abstract class CoffeeDecorator implements Coffee {
protected Coffee _coffee;
public CoffeeDecorator(Coffee coffee) {
_coffee = coffee;
}
@Override
public String getDescription() {
return _coffee.getDescription();
}
@Override
public int getCost() {
return _coffee.getCost();
}
}
// MilkDecorator.java
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee decoratedcoffee) {
super(decoratedcoffee);
}
@Override
public String getDescription() {
return super.getDescription() + " + Milk";
}
@Override
public int getCost() {
return super.getCost() + 5;
}
}
// SugarDecorator.java
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public int getCost() {
return super.getCost() + 10;
}
@Override
public String getDescription() {
return super.getDescription() + " + Sugar";
}
}
public class DecoratorPattern {
public static void main(String[] args) {
Coffee plaincoffee = new PlainCoffee();
System.out.println(plaincoffee.getDescription() + " Cost=" + plaincoffee.getCost());
Coffee capuccinocoffee = new CappuccinoCoffee();
capuccinocoffee = new MilkDecorator(capuccinocoffee);
capuccinocoffee = new SugarDecorator(capuccinocoffee);
// here we added the sugar and milk to the cappucinocoffee
System.out.println(capuccinocoffee.getDescription() + " Cost=" + capuccinocoffee.getCost());
}
}
Real World Example
The Java I/O library is a textbook example of the Decorator Pattern. The base classes such as InputStream
and OutputStream
are extended by concrete classes (like FileInputStream
) and then wrapped by decorator classes that add additional functionality at runtime.
// Base stream reading from a file
InputStream fileStream = new FileInputStream("data.txt");
// BufferedInputStream decorates fileStream by adding buffering capability
InputStream bufferedStream = new BufferedInputStream(fileStream);
// DataInputStream further decorates bufferedStream to allow reading primitive data types
DataInputStream dataStream = new DataInputStream(bufferedStream);
Design Principles
- Encapsulate What Varies - Identify the parts of the code that are going to change and encapsulate them into separate class just like the Strategy Pattern.
- Favor Composition Over Inheritance - Instead of using inheritance on extending functionality, rather use composition by delegating behavior to other objects.
- Program to Interface not Implementations - Write code that depends on Abstractions or Interfaces rather than Concrete Classes.
- Strive for Loosely coupled design between objects that interact - When implementing a class, avoid tightly coupled classes. Instead, use loosely coupled objects by leveraging abstractions and interfaces. This approach ensures that the class does not heavily depend on other classes.
- Classes Should be Open for Extension But closed for Modification - Design your classes so you can extend their behavior without altering their existing, stable code.