The Decorator Design Pattern is a structural design pattern that allows behavior to be added to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class. This pattern provides a flexible alternative to subclassing for extending functionality.
Example: Decorator Design Pattern in Java
In this example, we will implement a basic coffee ordering system where different types of add-ons (like milk or sugar) can be “decorated” onto a basic coffee.
Step 1: Define the Coffee
Interface
// Coffee.java
public interface Coffee {
String getDescription();
double cost();
}
Step 2: Create a Concrete Component (Base Coffee)
// SimpleCoffee.java
public class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple Coffee";
}
@Override
public double cost() {
return 2.00; // Base price for a simple coffee
}
}
Step 3: Create the Abstract Decorator Class
The abstract decorator class will implement the Coffee
interface and hold a reference to a Coffee
object.
// CoffeeDecorator.java
public abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}
@Override
public double cost() {
return decoratedCoffee.cost();
}
}
Step 4: Create Concrete Decorators
Now, let’s create some concrete decorators (Milk and Sugar) that will add functionality (extra cost and description) to the base coffee.
// MilkDecorator.java
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", Milk";
}
@Override
public double cost() {
return decoratedCoffee.cost() + 0.50; // Add cost for milk
}
}
// SugarDecorator.java
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", Sugar";
}
@Override
public double cost() {
return decoratedCoffee.cost() + 0.20; // Add cost for sugar
}
}
Step 5: Using the Decorator Pattern in the Main Class
// Main.java
public class Main {
public static void main(String[] args) {
// Order a simple coffee
Coffee myCoffee = new SimpleCoffee();
System.out.println(myCoffee.getDescription() + " $" + myCoffee.cost());
// Add milk to the coffee
myCoffee = new MilkDecorator(myCoffee);
System.out.println(myCoffee.getDescription() + " $" + myCoffee.cost());
// Add sugar to the coffee
myCoffee = new SugarDecorator(myCoffee);
System.out.println(myCoffee.getDescription() + " $" + myCoffee.cost());
// Add another milk (to show multiple decorations)
myCoffee = new MilkDecorator(myCoffee);
System.out.println(myCoffee.getDescription() + " $" + myCoffee.cost());
}
}
Explanation of the Code
- Coffee Interface:
- The
Coffee
interface defines the methodsgetDescription()
andcost()
, which will be implemented by both the base component and decorators.
- SimpleCoffee (Concrete Component):
- The
SimpleCoffee
class implements theCoffee
interface, providing a base description and cost for plain coffee.
- CoffeeDecorator (Abstract Decorator):
- This abstract class implements the
Coffee
interface and contains a reference to aCoffee
object. It forwards the calls to the wrapped coffee object, allowing the decorators to build on the base functionality.
- Concrete Decorators (MilkDecorator and SugarDecorator):
- These classes extend
CoffeeDecorator
and modify thegetDescription()
andcost()
methods to add extra description and cost for milk or sugar.
- Main Class:
- In the
main()
method, we demonstrate how a simple coffee can be “decorated” with milk and sugar by wrapping the baseSimpleCoffee
object with the decorators.
Output
When you run the Main
class, the output will be:
Simple Coffee $2.0
Simple Coffee, Milk $2.5
Simple Coffee, Milk, Sugar $2.7
Simple Coffee, Milk, Sugar, Milk $3.2
Key Points
- The Decorator Pattern allows you to add behavior to individual objects without affecting the behavior of other objects of the same class.
- It is a flexible alternative to subclassing because it provides a way to dynamically add or remove functionality at runtime.
- You can apply multiple decorators to a single object, as demonstrated by adding multiple instances of
MilkDecorator
andSugarDecorator
.
When to Use the Decorator Pattern
- When you need to add responsibilities to individual objects dynamically and transparently.
- When you need to avoid subclassing to extend functionality.
- When you want to combine several behaviors, and these behaviors can be mixed in different combinations.
Conclusion
The Decorator Design Pattern is a powerful and flexible pattern for extending the functionality of an object. By wrapping an object with decorators, we can add behavior dynamically, avoiding the need for an extensive inheritance hierarchy. This promotes composition over inheritance, a key principle in object-oriented design.
+-------------------+
| <<Interface>> |
| Component |
+-------------------+
| +getDescription() |
| +cost() |
+-------------------+
▲
|
|
+-------------------+
| ConcreteComponent |
+-------------------+
| -description: |
| +getDescription() |
| +cost() |
+-------------------+
▲
|
+----------+----------+
| |
+-------------------+ +------------------+
| Decorator | | ConcreteDecorator|
+-------------------+ +------------------+
| -decoratedObject | | -additionalCost |
| +getDescription() | | +getDescription()|
| +cost() | | +cost() |
+-------------------+ +------------------+