TeachingBee

Decorator Design Pattern : Low Level System Design

decorator design pattern

The Decorator Design Pattern is a structural pattern that allows behaviour to be added to individual objects, either statically or dynamically, without affecting the behaviour of other objects from the same class.

This pattern is particularly useful for adhering to the Open/Closed Principle, one of the SOLID principles, which states that software entities should be open for extension but closed for modification.

What is Decorator Design Pattern?

The Decorator Design Pattern is a structural pattern used in software development to add new functionality to an object dynamically without altering its structure. This pattern creates a decorator class which wraps the original class and adds new behaviors or responsibilities.

The main goal of the decorator pattern is to attach additional responsibilities to an object dynamically and transparently, meaning that the system can add new features without affecting the behavior of other objects.

Imagine a simple coffee-making application where you start with a basic coffee component and want to add various additives (like milk, sugar, or whipped cream) dynamically.

Interface Coffee {
    Method cost() -> double
    Method ingredients() -> String
}

Class SimpleCoffee implements Coffee {
    Method cost() -> double {
        Return 1
    }

    Method ingredients() -> String {
        Return "Coffee"
    }
}

To add new features or ingredients to the coffee, without the Decorator pattern, you might be tempted to extend the SimpleCoffee class for each ingredient combination. This quickly leads to a proliferation of subclasses (like CoffeeWithMilk, CoffeeWithSugar, CoffeeWithMilkAndSugar, etc.), making the system hard to maintain and extend.

Solving the Issue with the Decorator Design Pattern

The Decorator Design Pattern solves this issue by allowing you to “decorate” objects with new behaviors without affecting other objects of the same class. Decorators provide a flexible alternative to subclassing for extending functionality using:

“Is-a” Relationship: Think of it like this: a decorator is a type of the original object it’s decorating. For example, if you have a basic coffee, a flavored coffee (like vanilla coffee) is still a type of coffee. So, a decorator follows the same interface or extends the same base class as the object it decorates.

“Has-a” Relationship: This is more about what the decorator has. A decorator has an instance of the original object it’s decorating. Imagine you’re adding toppings to an ice cream cone. Each topping (like sprinkles or caramel sauce) is added to the cone. Similarly, decorators add their own functionality while keeping the original object intact.

By combining these relationships, decorators allow you to dynamically add or change the behavior of objects without changing their actual code. It’s like putting different toppings on an ice cream cone to make it tastier without altering the cone itself. This makes the Decorator pattern a powerful tool for adding flexibility and customization to your code.

Let’s see how we can solve this using Decorator Design Pattern.

How to Implement Decorator Design Pattern?

The Decorator Design Pattern consists of following components

Decorator Design Pattern
  1. Component Interface: The Component Interface defines the interface for objects that can have responsibilities added to them dynamically. It establishes a common interface that both Concrete Components and Decorators adhere to, ensuring that they can be used interchangeably.
  2. Concrete Component: A Concrete Component defines an object to which additional responsibilities can be attached.It implements the Component Interface and represents the base object to which decorators can be added, providing the core functionality of the component.
  3. Decorator: Decorator maintains a reference to a Component object and defines an interface that conforms to the Component’s interface.It acts as a wrapper around the Component object, allowing new behaviour to be added dynamically without altering the interface of the original component, thus enabling flexible extension of functionality.
  4. Concrete Decorators: Concrete Decorators add responsibilities to the component. They extend the functionality of the base Component or previous decorators by adding specific behavior or modifying existing behavior, providing a way to enhance the functionality of components in a modular and reusable manner.

Step-by-Step Implementation

 Decorator Design Pattern Example
  1. Define the Component Interface: Define the Component Interface for the Decorator pattern, representing a coffee object. It specifies methods to retrieve the cost and ingredients of the coffee.
Interface Coffee
    Method cost() -> double
    Method ingredients() -> String
EndInterface
  1. Implement the Concrete Component: Implement the Concrete Component in the Decorator pattern, representing a basic coffee object without any decorations. It defines methods to return the cost (which is set to 1) and the ingredients (which is “Coffee”).
Class SimpleCoffee implements Coffee
    Method cost() -> double
        Return 1
    EndMethod

    Method ingredients() -> String
        Return "Coffee"
    EndMethod
EndClass
  1. Create the Abstract Decorator Class: Create a decorator abstarct class with is-a and has-a relationship.
// is-a relationship. CoffeeDecorator is of type Coffee
Abstract Class CoffeeDecorator implements Coffee {
   
    // has-a relationship. CoffeeDecorator has an object of type Coffee 
    Protected final Coffee decoratedCoffee;

    // Constructor to initialize the decoratedCoffee object
    Constructor CoffeeDecorator(Coffee coffee) {
        This.decoratedCoffee = coffee;
    }

    // Implementing the cost() method by delegating to the decoratedCoffee object
    Method cost() -> double {
        Return decoratedCoffee.cost();
    }

    // Implementing the ingredients() method by delegating to the decoratedCoffee object
    Method ingredients() -> String {
        Return decoratedCoffee.ingredients();
    }
}
  1. Implement Concrete Decorators: Implement this decorator to implement various other decorators.
Class WithMilk extends CoffeeDecorator {
    // is-a relationship. WithMilk is a CoffeeDecorator

    // Constructor to initialize WithMilk decorator with a Coffee object
    Constructor WithMilk(Coffee coffee) {
        super(coffee);
    }

    // Override the cost method to add the cost of milk
    Method cost() -> double {
        
    }

    // Override the ingredients method to add milk to the ingredients list
    Method ingredients() -> String {
        
    }
}

Class WithSugar extends CoffeeDecorator {
    // is-a relationship. WithSugar is a CoffeeDecorator

    // Constructor to initialize WithSugar decorator with a Coffee object
    Constructor WithSugar(Coffee coffee) {
        super(coffee);
    }

    // Override the cost method to add the cost of sugar
    Method cost() -> double {
        
    }

    // Override the ingredients method to add sugar to the ingredients list
    Method ingredients() -> String {
    }
}
  1. Usage:
// Create a simple coffee object
   Coffee myCoffee = new SimpleCoffee();

// Decorate the coffee object with milk
   myCoffee = new WithMilk(myCoffee);

// Decorate the coffee object with sugar
   myCoffee = new WithSugar(myCoffee);

// Output the cost and ingredients of the decorated coffee
   Display("Cost: " + myCoffee.cost());
   Display("Ingredients: " + myCoffee.ingredients());

Common Usage of the Decorator Design Pattern

The Decorator Pattern is widely used in:

  1. GUI Libraries: For adding behaviors to UI components without changing their core classes.
  2. Stream Decorations: In I/O libraries where input/output streams can be dynamically enhanced with features like buffering, line numbering, or compression.
  3. Web Development Frameworks: For dynamically adding responsibilities or behaviors to objects such as request handlers or middleware.

Benefits of the Decorator Design Pattern

  1. Flexibility: Allows for dynamic extension of an object’s behavior without making changes to the existing code.
  2. Modularity: Decorators can be mixed and matched to create new behaviors.
  3. Adherence to SOLID Principles: Specifically supports the Open/Closed Principle by allowing objects to be open for extension but closed for modification.
  4. Avoids Class Proliferation: Unlike subclassing, it doesn’t lead to an explosion of classes for every combination of behaviors.

Drwabacks of the Decorator Design Pattern

  1. Complexity: Can increase complexity by introducing multiple small classes, making the system harder to understand.
  2. Instantiation Management: The client needs to handle the decorator stack, which can complicate instantiation logic.
  3. Identification: Objects wrapped by decorators may get disguised, making it hard to identify their types or original states.
  4. Overuse: Overusing the pattern can lead to scenarios where simpler inheritance might have been sufficient, unnecessarily complicating the design.

Identifying Where To Apply Decorator Design Pattern

  • Dynamic Extension: When you need to dynamically add as well as remove responsibilities to objects without affecting other objects.
  • Avoiding Subclass Explosion: When extending a class hierarchy would result in an excessive number of subclasses for combination-based behaviors.
  • Compliance with OCP: When maintenance and future extension are priorities, and you need to keep the system compliant with the Open/Closed Principle.

Key TakeAways

  • Decorator pattern allows behaviour to be added or modified dynamically at runtime, providing a flexible alternative to subclassing for extending functionality.
  • It adheres to the Single Responsibility Principle by keeping classes focused on specific tasks, making it easier to understand, maintain, and extend code.
  • Decorators promote composition over inheritance, allowing behavior to be composed from smaller, reusable components, resulting in more flexible and maintainable code.
  • Decorators maintain the same interface as the objects they decorate, ensuring that client code remains unaware of the specific decorators’ presence, thus promoting code interoperability and ease of use.

Similar Posts

What Is the Difference Between Factory and Abstract Factory Design Patterns?

When to Use Factory Design Pattern in Java?

Builder Design Pattern

Factory Design Pattern

Adapter Design Pattern

FAQ

What is a real-world example of decorator design pattern?

What is the difference between wrapper and decorator design pattern?

What is the design pattern of decorator and visitor?

What is decorator pattern also known as?

90% of Tech Recruiters Judge This In Seconds! 👩‍💻🔍

Don’t let your resume be the weak link. Discover how to make a strong first impression with our free technical resume review!

Related Articles

Types of Memory and Storage in system design thumbnail

Types of Computer Memory and Storage

In this article we will look into different types of computer memory, distinguishing between primary memory and secondary memory. We will also see their characteristics, major kinds, usage, and key

latency in system design thumbnail

What is Latency In System Design?

In this article we will look into Latency In System Design, we will see how is latency introduced into the systems and what are various ways to reduce the latency

Why Aren’t You Getting Interview Calls? 📞❌

It might just be your resume. Let us pinpoint the problem for free and supercharge your job search. 

Newsletter

Don’t miss out! Subscribe now

Log In