21
NovDecorator Design Pattern
Decorator Design Pattern
Decorator Design Pattern is essential in software design, as it allows behavior to be added to individual objects dynamically without altering their structure. This pattern helps in extending the functionalities of objects transparently and flexibly, keeping the base code clean and scalable. It provides a more elegant way to handle responsibilities than traditional inheritance.
In this design patterns tutorial, we will explore the Decorator Design Pattern including, "What is the Decorator Design Pattern?". Additionally, we will see, "When should I use the Decorator Design Pattern?" We'll also look at an example of the Decorator Design Pattern in action. So, let's start with, "What is the Decorator Design Pattern?"
What is the Decorator's Design Pattern?
- The Decorator Design Pattern is a structural pattern that allows you to dynamically add behavior or responsibilities to individual objects without altering their code.
- It involves wrapping objects with decorator classes, which extend their functionality while adhering to the original object's interface.
- Clients interact with the original object through the decorator, promoting flexibility and allowing functionality to be layered transparently.
- This pattern promotes code reusability and helps keep the base classes simple while enabling new functionalities to be easily added.
Characteristics of the Decorator Method Design Pattern
- This approach enhances flexibility and extensibility in software systems by allowing developers to create objects with various functionalities during runtime.
- It follows to the open/closed paradigm, allowing new decorators to be added without altering current code, making it an effective tool for creating modular and adaptable software components.
- The Decorator Pattern is widely used in circumstances where a range of alternative features or behaviors must be added to objects in a flexible and reusable manner, such as text formatting, graphical user interfaces, or product customization,such as coffee or ice cream.
Why do we need a Decorator Design Pattern?
1. Dynamic Behavior Addition
- The Decorator Pattern allows behaviors to be added to objects dynamically at runtime, making it more flexible than inheritance.
- This pattern helps avoid the complexity of subclassing and provides a way to extend functionality without modifying existing classes.
2. Transparency and Flexibility
The Decorator Pattern maintains the transparency of object interaction by keeping the original interface intact. Unlike inheritance, decorators can be combined in different ways, offering flexibility in how new behaviors are introduced.
3. Open-Closed Principle
- The Decorator Pattern follows the available-closed Principle, in which objects are available for expansion but closed for alteration.
- It lets new features be added without changing the old code, lowering the chance of breaking functionality.
4. Combination of Behaviors
- The Decorator Pattern allows behaviors to be layered and combined in various ways, enabling the construction of complex functionalities by stacking decorators.
- This modular approach promotes code reusability and helps manage different combinations of features without duplication.
5. Simplification of Core Classes
- By offloading additional responsibilities to decorator classes, the Decorator Pattern simplifies the core class design.
- It prevents the base class from becoming overly complex with numerous subclasses or conditional logic, resulting in a cleaner and more maintainable architecture.
Real-world Illustration of Decorator Design Pattern
- Imagine a coffee shop where you can order a basic coffee and then customize it with various add-ons like milk, sugar, or syrup.
- The coffee remains the same, but the add-ons enhance its flavor without altering the original recipe.
- You can layer these customizations in any combination, such as adding both milk and syrup or just sugar.
- This setup mirrors the Decorator Pattern, where each add-on dynamically extends the coffee’s behavior, allowing you to enhance it without changing the underlying coffee itself.
Decorator Design Pattern Basic Structure and Implementation
The structure for implementing the Decorator Design Pattern is outlined below:
The classes, interfaces, and objects in this structure are as follows:
1. Component
- This is an interface or abstract class that defines the common operations that concrete components and decorators must implement.
- It declares methods that allow the client to interact uniformly with both basic objects and decorated objects.
- By defining this contract, the client doesn’t need to know whether it is dealing with the base component or a decorated version of it.
2. ConcreteComponent
- The ConcreteComponent class implements the Component interface and represents the basic object that can have responsibilities added dynamically.
- For instance, in a coffee system, this could represent a simple Coffee object without any add-ons.
- It performs the default functionality expected by the client.
3. Decorator
- The Decorator class also implements the Component interface but adds extra responsibilities dynamically to the ConcreteComponent.
- It holds a reference to a Component object (which could be either a base object or another decorator) and forwards requests to it.
- For example, decorators like Milk or Sugar can wrap the basic Coffee object to add extra behavior.
4. ConcreteDecorator
- The ConcreteDecorator class extends the Decorator class and adds specific behaviors.
- It modifies or extends the functionality of the component it decorates.
- For example, a Milk decorator adds the behavior of adding milk to the coffee, while a Sugar decorator adds the behavior of adding sugar.
Real-Life Example
Car Rental Service (Basic Car with Add-ons)
In a car rental service, customers can rent a basic car and add optional features such as GPS, child seats, or extra insurance.
Decorator (Car Rental Customizations)
- Base Component: Basic car rental includes essential features like seating and an engine, allowing the customer to drive from one place to another.
- Additional Features (Decorators): Customers can add extra features like GPS navigation, child seats, or insurance. Each feature adds to the cost of the rental without altering the core function of the car.
Operation Flow
- Basic Car: The customer rents a basic car without any additional features and pays the base rental fee.
- Add-On Features: The customer selects optional features like GPS, which are "decorators" that enhance the rental car’s functionality (or the rental experience) without changing the core driving functionality.
- Unified Billing: The final cost is calculated by combining the base rental fee with the cost of each additional feature.
How the Decorator Helps
- Flexible Customization: The customer can choose which features to add to the basic car, allowing for a tailored experience. You only pay for the features you need.
- Scalability: New features, like Wi-Fi or road assistance, can easily be added without modifying the existing car rental process.
- Separation of Concerns: The base car remains unchanged, while additional features are layered on through decorators, ensuring modular and reusable code in system implementation.
Example
// C# Code - Car Rental Service with Decorator Pattern
using System;
public interface ICarRental {
string GetDescription();
double GetCost();
}
public class BasicCarRental : ICarRental {
public string GetDescription() {
return "Standard Sedan";
}
public double GetCost() {
return 50.0;
}
}
public abstract class CarRentalDecorator : ICarRental {
protected ICarRental carRental;
public CarRentalDecorator(ICarRental carRental) {
this.carRental = carRental;
}
public virtual string GetDescription() {
return carRental.GetDescription();
}
public virtual double GetCost() {
return carRental.GetCost();
}
}
public class GPSDecorator : CarRentalDecorator {
public GPSDecorator(ICarRental carRental) : base(carRental) { }
public override string GetDescription() {
return carRental.GetDescription() + ", GPS Navigation";
}
public override double GetCost() {
return carRental.GetCost() + 10.0;
}
}
public class ChildSeatDecorator : CarRentalDecorator {
public ChildSeatDecorator(ICarRental carRental) : base(carRental) { }
public override string GetDescription() {
return carRental.GetDescription() + ", Child Seat";
}
public override double GetCost() {
return carRental.GetCost() + 15.0;
}
}
public class InsuranceDecorator : CarRentalDecorator {
public InsuranceDecorator(ICarRental carRental) : base(carRental) { }
public override string GetDescription() {
return carRental.GetDescription() + ", Extra Insurance";
}
public override double GetCost() {
return carRental.GetCost() + 20.0;
}
}
public class Program {
public static void Main() {
ICarRental rental = new BasicCarRental();
Console.WriteLine($"Description: {rental.GetDescription()}");
Console.WriteLine($"Cost: ${rental.GetCost()}");
rental = new GPSDecorator(rental);
Console.WriteLine($"Description: {rental.GetDescription()}");
Console.WriteLine($"Cost: ${rental.GetCost()}");
rental = new ChildSeatDecorator(rental);
Console.WriteLine($"Description: {rental.GetDescription()}");
Console.WriteLine($"Cost: ${rental.GetCost()}");
rental = new InsuranceDecorator(rental);
Console.WriteLine($"Description: {rental.GetDescription()}");
Console.WriteLine($"Cost: ${rental.GetCost()}");
}
}
# Python Code - Car Rental Service with Decorator Pattern
class CarRental:
def get_description(self):
return "Standard Sedan"
def get_cost(self):
return 50.00
class CarRentalDecorator(CarRental):
def __init__(self, car_rental):
self._car_rental = car_rental
def get_description(self):
return self._car_rental.get_description()
def get_cost(self):
return self._car_rental.get_cost()
class GPSDecorator(CarRentalDecorator):
def get_description(self):
return self._car_rental.get_description() + ", GPS Navigation"
def get_cost(self):
return self._car_rental.get_cost() + 10.00
class ChildSeatDecorator(CarRentalDecorator):
def get_description(self):
return self._car_rental.get_description() + ", Child Seat"
def get_cost(self):
return self._car_rental.get_cost() + 15.00
class InsuranceDecorator(CarRentalDecorator):
def get_description(self):
return self._car_rental.get_description() + ", Extra Insurance"
def get_cost(self):
return self._car_rental.get_cost() + 20.00
# Client code
rental = CarRental()
print("Description:", rental.get_description())
print("Cost: $", rental.get_cost())
rental = GPSDecorator(rental)
print("Description:", rental.get_description())
print("Cost: $", rental.get_cost())
rental = ChildSeatDecorator(rental)
print("Description:", rental.get_description())
print("Cost: $", rental.get_cost())
rental = InsuranceDecorator(rental)
print("Description:", rental.get_description())
print("Cost: $", rental.get_cost())
// Java Code - Car Rental Service with Decorator Pattern
public class CarRentalService {
public interface CarRental {
String getDescription();
double getCost();
}
public static class BasicCarRental implements CarRental {
@Override
public String getDescription() {
return "Standard Sedan";
}
@Override
public double getCost() {
return 50.00;
}
}
public abstract static class CarRentalDecorator implements CarRental {
protected CarRental carRental;
public CarRentalDecorator(CarRental carRental) {
this.carRental = carRental;
}
@Override
public abstract String getDescription();
@Override
public abstract double getCost();
}
public static class GPSDecorator extends CarRentalDecorator {
public GPSDecorator(CarRental carRental) {
super(carRental);
}
@Override
public String getDescription() {
return carRental.getDescription() + ", GPS Navigation";
}
@Override
public double getCost() {
return carRental.getCost() + 10.00;
}
}
public static class ChildSeatDecorator extends CarRentalDecorator {
public ChildSeatDecorator(CarRental carRental) {
super(carRental);
}
@Override
public String getDescription() {
return carRental.getDescription() + ", Child Seat";
}
@Override
public double getCost() {
return carRental.getCost() + 15.00;
}
}
public static class InsuranceDecorator extends CarRentalDecorator {
public InsuranceDecorator(CarRental carRental) {
super(carRental);
}
@Override
public String getDescription() {
return carRental.getDescription() + ", Extra Insurance";
}
@Override
public double getCost() {
return carRental.getCost() + 20.00;
}
}
public static void main(String[] args) {
CarRental rental = new BasicCarRental();
System.out.println("Description: " + rental.getDescription());
System.out.println("Cost: $" + rental.getCost());
rental = new GPSDecorator(rental);
System.out.println("Description: " + rental.getDescription());
System.out.println("Cost: $" + rental.getCost());
rental = new ChildSeatDecorator(rental);
System.out.println("Description: " + rental.getDescription());
System.out.println("Cost: $" + rental.getCost());
rental = new InsuranceDecorator(rental);
System.out.println("Description: " + rental.getDescription());
System.out.println("Cost: $" + rental.getCost());
}
}
// TypeScript Code - Car Rental Service with Decorator Pattern
interface CarRental {
getDescription(): string;
getCost(): number;
}
class BasicCarRental implements CarRental {
getDescription(): string {
return "Standard Sedan";
}
getCost(): number {
return 50.0;
}
}
class CarRentalDecorator implements CarRental {
protected carRental: CarRental;
constructor(carRental: CarRental) {
this.carRental = carRental;
}
getDescription(): string {
return this.carRental.getDescription();
}
getCost(): number {
return this.carRental.getCost();
}
}
class GPSDecorator extends CarRentalDecorator {
getDescription(): string {
return this.carRental.getDescription() + ", GPS Navigation";
}
getCost(): number {
return this.carRental.getCost() + 10.0;
}
}
class ChildSeatDecorator extends CarRentalDecorator {
getDescription(): string {
return this.carRental.getDescription() + ", Child Seat";
}
getCost(): number {
return this.carRental.getCost() + 15.0;
}
}
class InsuranceDecorator extends CarRentalDecorator {
getDescription(): string {
return this.carRental.getDescription() + ", Extra Insurance";
}
getCost(): number {
return this.carRental.getCost() + 20.0;
}
}
// Client code
let rental: CarRental = new BasicCarRental();
console.log(`Description: ${rental.getDescription()}`);
console.log(`Cost: $${rental.getCost()}`);
rental = new GPSDecorator(rental);
console.log(`Description: ${rental.getDescription()}`);
console.log(`Cost: $${rental.getCost()}`);
rental = new ChildSeatDecorator(rental);
console.log(`Description: ${rental.getDescription()}`);
console.log(`Cost: $${rental.getCost()}`);
rental = new InsuranceDecorator(rental);
console.log(`Description: ${rental.getDescription()}`);
console.log(`Cost: $${rental.getCost()}`);
// JavaScript Code - Car Rental Service with Decorator Pattern
class CarRental {
getDescription() {
return "Standard Sedan";
}
getCost() {
return 50.0;
}
}
class CarRentalDecorator extends CarRental {
constructor(carRental) {
super();
this.carRental = carRental;
}
getDescription() {
return this.carRental.getDescription();
}
getCost() {
return this.carRental.getCost();
}
}
class GPSDecorator extends CarRentalDecorator {
getDescription() {
return this.carRental.getDescription() + ", GPS Navigation";
}
getCost() {
return this.carRental.getCost() + 10.0;
}
}
class ChildSeatDecorator extends CarRentalDecorator {
getDescription() {
return this.carRental.getDescription() + ", Child Seat";
}
getCost() {
return this.carRental.getCost() + 15.0;
}
}
class InsuranceDecorator extends CarRentalDecorator {
getDescription() {
return this.carRental.getDescription() + ", Extra Insurance";
}
getCost() {
return this.carRental.getCost() + 20.0;
}
}
// Client code
let rental = new CarRental();
console.log(`Description: ${rental.getDescription()}`);
console.log(`Cost: $${rental.getCost()}`);
rental = new GPSDecorator(rental);
console.log(`Description: ${rental.getDescription()}`);
console.log(`Cost: $${rental.getCost()}`);
rental = new ChildSeatDecorator(rental);
console.log(`Description: ${rental.getDescription()}`);
console.log(`Cost: $${rental.getCost()}`);
rental = new InsuranceDecorator(rental);
console.log(`Description: ${rental.getDescription()}`);
console.log(`Cost: $${rental.getCost()}`);
Explanation
- This program demonstrates the Decorator Design Pattern applied to a car rental service.
- The CarRental interface defines methods for describing and costing a rental. BasicCarRental is the base component of a standard car rental.
- Decorators like GPSDecorator, ChildSeatDecorator, and InsuranceDecorator extend the CarRentalDecorator abstract class, adding features (GPS, child seat, insurance) and their respective costs to the base rental.
- The main method shows how each decorator can be applied sequentially to enhance the basic rental with additional features, illustrating the flexibility and extensibility of the Decorator Pattern.
Applications of Decorator Design Pattern
1. Use the Decorator pattern when you need to be able to assign additional behaviors to objects at runtime without disturbing the code that utilizes them.
- The Decorator allows you to divide your business logic into layers, create decorators for each layer, and assemble objects with different combinations of this logic at runtime.
- Because all of these objects share a common interface, the client programs may treat them all similarly.
2. Use the pattern when it is difficult or impossible to extend an object's functionality through inheritance.
- Many programming languages provide a final keyword that can be used to prevent further extensions of a class.
- For a final class, the only method to utilize the existing functionality is to create your own wrapper using the Decorator pattern.
Why do we need a Decorator Design Pattern?
1. Extending Functionality Dynamically
- Scenario: You want to add new functionality to objects without modifying their structure or affecting other objects.
- Need: The Decorator design pattern enables dynamic extension of an object’s behavior by wrapping it with additional responsibilities without altering the original code.
2. Adhering to Open/Closed Principle
- Scenario: You need to extend the behavior of classes, but making changes to their existing code would violate the open/closed principle.
- Need: The Decorator pattern allows you to add features by creating decorators, keeping the base class closed to modification and open to extension.
3. Combining Multiple Behaviors
- Scenario: You want to combine multiple behaviors (e.g., logging, authentication) in a flexible way for certain objects.
- Need: The Decorator pattern allows you to stack multiple behaviors by wrapping objects with different decorators, enabling a modular composition of functionalities.
5. Avoiding Subclass Explosion
- Scenario: Creating subclasses for every possible combination of extended behaviors would lead to an impractical number of subclasses.
- Need: The Decorator pattern avoids subclass explosion by enabling the creation of various combinations through decorator objects, reducing the need for excessive subclassing.
6. Runtime Modification of Behavior
- Scenario: You need to modify an object’s behavior at runtime based on specific conditions or requirements.
- Need: The Decorator pattern supports runtime behavior modification, allowing flexibility in extending object functionality as needed without altering the underlying object.
When not to use the Decorator Design Pattern?
1. When Simpler Inheritance Works
- Scenario: If extending functionality can be easily handled by inheritance without violating the open/closed principle.
- Reason: The Decorator pattern adds complexity, and if subclassing provides a simpler and cleaner solution, there’s no need to use decorators.
2. When Only a Few Extensions Are Needed
- Scenario: You only need to add a small number of behaviors, and creating decorators would add unnecessary layers.
- Reason: If only a limited number of features need to be added, the overhead of creating multiple decorators may not be worth it compared to straightforward implementation.
3. When Performance is Critical
- Scenario: In applications where high performance is essential, the overhead introduced by the Decorator pattern could slow things down.
- Reason: The additional objects created by decorators may introduce performance bottlenecks, making them unsuitable for performance-critical scenarios.
4. When Object Structure Becomes Complex
- Scenario: If the use of multiple decorators leads to a highly complex object structure that is difficult to manage or understand.
- Reason: Excessive use of decorators can make the system harder to comprehend, leading to issues with maintainability and debugging.
5. When Behavior Should Not Change Dynamically
- Scenario: If the behavior of objects should remain fixed and not change dynamically at runtime.
- Reason: The Decorator pattern allows behavior to be modified at runtime, so if this flexibility is unnecessary, simpler alternatives like static composition might be preferable.
Relationship between Decorator Pattern and Other Patterns
- Adapter Pattern: Unlike the Adapter pattern, the Decorator provides new functionality to objects without affecting their interfaces, whereas the Adapter modifies an interface to allow incompatible systems to work together.
- Facade Pattern: While both give distinct approaches to dealing with complexity, the Decorator improves an object's functionality, whilst the Facadepattern simplifies interactions by providing a single interface to a group of subsystems without changing their core functionality.
- Composite Pattern: The Decorator and the Composite pattern are similar because they both use recursive composition. However, the Decorator adds functionality to individual objects, but the Composite manages object hierarchies.
- Proxy Pattern: The Decorator and Proxy patterns are structurally similar. However, the Proxy controls access to the object and may add behavior relevant to access control, whilst the Decorator concentrates solely on adding functionality.
- Strategy Pattern: The Decorator pattern complements the Strategy pattern by dynamically adding functionality to an object, whereas the Strategy pattern allows for the swapping or modification of an object's algorithm at runtime.
Summary
The Decorator Design Pattern lets you add actions to objects dynamically without changing their structure. It accomplishes this by enclosing the object in decorator classes that add functionality while preserving the original interface. This design increases flexibility and adheres to the Open-Closed Principle, which allows code to be extended without modification. Decorators can be layered, making them excellent for mixing various characteristics such as logging, security, and runtime features. To master design patterns, enroll in ScholarHat's Master Software Architecture and Design Certification Training.