Object Oriented Design - Decorator Pattern
The Decorator Pattern is a structural design pattern that allows for the dynamic extension of an object's functionality by wrapping it with additional 'decorator' classes. It provides an alternative to subclassing for extending behavior. It is used for additional functionalities to individual objects without affecting behavior of other objects from same class.
How it Works?
- Implement the core functionality in the Component class.
- Implement the Decorator class, ensuring it conforms to the Component interface and contains a reference to a Component.
- At runtime, wrap the Component with one or more Decorators to dynamically add behaviors and responsibilities.
Design a Coffee Shop Order System
Imagine a coffee shop where customers can order a base beverage like Espresso, House Blend, or Decaf and then choose add-ons like Milk, Soy, Mocha, or Whip. Each item and add-on has a cost associated with it. The system should be able to calculate the total cost of the beverage including all add-ons.
Classes:
- Beverage: The base class for all beverages.
- CondimentDecorator: The abstract decorator class that extends
Beverage
.
// Component
public abstract class Beverage {
String description = "Unknown Beverage";
public String getDescription() {
return description;
}
public abstract double cost();
}
// Decorator
public abstract class CondimentDecorator extends Beverage {
public abstract String getDescription();
}
Concrete Classes:
- Espresso, HouseBlend, etc., as concrete classes extending
Beverage
. - Milk, Mocha, etc., as concrete decorators extending
CondimentDecorator
.
public class Espresso extends Beverage {
public Espresso() {
description = "Espresso";
}
public double cost() {
return 1.99;
}
}
public class Milk extends CondimentDecorator {
Beverage beverage;
public Milk(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + ", Milk";
}
public double cost() {
return beverage.cost() + 0.50;
}
}
// Usage
Beverage beverage = new Espresso();
beverage = new Milk(beverage);
beverage = new Mocha(beverage);
How does Decorator Pattern follow SOLID Principles?
The Decorator pattern aligns well with the SOLID principles, which are guidelines for designing maintainable and extendable object-oriented software. Let's examine how it adheres to each of the SOLID principles:
1. Single Responsibility Principle (SRP)
- Each decorator class in the Decorator pattern has one specific responsibility or behavior to add to the component it decorates.
- In the Coffee Shop Order System example,
Espresso
provides a basic coffee, while each decorator likeMilkDecorator
orSugarDecorator
adds a specific feature.
2. Open/Closed Principle (OCP)
- The Decorator pattern is a quintessential example of the Open/Closed Principle. It allows objects to be extended with new functionality (open for extension) without modifying their existing code (closed for modification). By using decorators, new functionalities can be added to objects at runtime without altering the underlying object's code.
3. Liskov Substitution Principle (LSP)
- Decorators and the components they wrap are interchangeable because they conform to the same interface. This means that a decorated component can be used in place of the original component without affecting the correctness of the program.
- Decorators can be substituted for their base type (
Beverage
).
4. Interface Segregation Principle (ISP)
- Each decorator provides only the methods relevant to its added functionality, which keeps interfaces segregated and specialized
- The
Beverage
interface is simple and specific to the needs of the beverages and decorators.
5. Dependency Inversion Principle (DIP)
- The high-level modules (decorators) and low-level modules (components) depend on abstractions, which promotes loose coupling and greater flexibility in the system.
- Decorators and components both depend on the
Beverage
interface, not on concrete implementations.
Design Examples for Decorator Pattern
1. Customizable Pizza Ordering System
- Use Case: Allow customers to choose a base pizza (like Margherita, Pepperoni, Veggie) and add various toppings (like Extra Cheese, Olives, Chicken, Peppers). The system should dynamically calculate the total price based on the selected base and additional toppings.
2. Parking Lot
- Use Case: Offer additional services for parking spots like EV charging, premium parking, or enhanced security.
- Application: Parking spots can be decorated with features like an EVChargingDecorator for electric vehicle charging capabilities.
3. Hotel Reservation
- Use Case: Provide additional amenities for room bookings, like Wi-Fi, breakfast options, or room upgrades.
- Application: Room bookings can be enhanced with decorators like a WifiDecorator for internet access or a BreakfastOptionDecorator for including breakfast with the stay.
4. E-commerce Product Customization
- Use Case: Develop a system for an e-commerce platform that allows customers to customize products. For example, a customer should be able to choose a basic laptop model and add customizations like additional RAM, extended warranty, and pre-installed software.
5. API Response Wrapper for Additional Metadata
- Use Case: Design a system where API responses can be wrapped with additional metadata like execution time, request tracing information, or error handling without changing the core response structure.