Thursday, 12 December 2024

Decorator Design Pattern | JavaScript Design Patterns

The Decorator Design Pattern is a structural design pattern that allows you to dynamically add behavior or responsibilities to an object without altering its code. The key idea is to create a "wrapper" around an object that adds additional functionality while keeping the original object intact. This is typically done by implementing the same interface or base class and wrapping the original object.

Key Concepts:

  1. Component: The interface or base class that defines the basic functionality.

  2. ConcreteComponent: A class that implements the basic functionality (the object being decorated).

  3. Decorator: A base class or interface that wraps the Component and adds additional behavior.

  4. ConcreteDecorator: A class that extends the Decorator and implements the additional functionality.

Example of Decorator Pattern in JavaScript

Let's say we have a basic Coffee class and we want to add different kinds of "decorations" to it, such as milk, sugar, and chocolate. Each decorator adds something to the coffee's description or cost.

Without Decorators (Inflexible Approach):

class Coffee {

    cost() {

        return 5;

    }

 

    description() {

        return 'Plain coffee';

    }

}

 

class MilkCoffee {

    constructor(coffee) {

        this.coffee = coffee;

    }

 

    cost() {

        return this.coffee.cost() + 1;  // Adds cost of milk

    }

 

    description() {

        return this.coffee.description() + ', with milk';

    }

}

 

class SugarCoffee {

    constructor(coffee) {

        this.coffee = coffee;

    }

 

    cost() {

        return this.coffee.cost() + 0.5;  // Adds cost of sugar

    }

 

    description() {

        return this.coffee.description() + ', with sugar';

    }

}

 

let myCoffee = new Coffee();

console.log(myCoffee.description()); // Plain coffee

console.log(myCoffee.cost()); // 5

 

myCoffee = new MilkCoffee(myCoffee); // Decorate with Milk

console.log(myCoffee.description()); // Plain coffee, with milk

console.log(myCoffee.cost()); // 6

 

myCoffee = new SugarCoffee(myCoffee); // Decorate with Sugar

console.log(myCoffee.description()); // Plain coffee, with milk, with sugar

console.log(myCoffee.cost()); // 6.5

While this approach works, it creates a lot of subclassing (MilkCoffee, SugarCoffee, etc.) and becomes increasingly difficult to extend with additional features like adding chocolate or adjusting the size of the coffee.

With the Decorator Pattern (Flexible Approach):

// Base Component

class Coffee {

    cost() {

        return 5;

    }

 

    description() {

        return 'Plain coffee';

    }

}

 

// Decorator Base Class

class CoffeeDecorator {

    constructor(coffee) {

        this.coffee = coffee;

    }

 

    cost() {

        return this.coffee.cost();

    }

 

    description() {

        return this.coffee.description();

    }

}

 

// Concrete Decorators

class MilkDecorator extends CoffeeDecorator {

    cost() {

        return this.coffee.cost() + 1; // Adds milk cost

    }

 

    description() {

        return this.coffee.description() + ', with milk';

    }

}

 

class SugarDecorator extends CoffeeDecorator {

    cost() {

        return this.coffee.cost() + 0.5; // Adds sugar cost

    }

 

    description() {

        return this.coffee.description() + ', with sugar';

    }

}

 

class ChocolateDecorator extends CoffeeDecorator {

    cost() {

        return this.coffee.cost() + 1.5; // Adds chocolate cost

    }

 

    description() {

        return this.coffee.description() + ', with chocolate';

    }

}

 

// Usage

let myCoffee = new Coffee();

console.log(myCoffee.description()); // Plain coffee

console.log(myCoffee.cost()); // 5

 

myCoffee = new MilkDecorator(myCoffee); // Decorate with Milk

console.log(myCoffee.description()); // Plain coffee, with milk

console.log(myCoffee.cost()); // 6

 

myCoffee = new SugarDecorator(myCoffee); // Decorate with Sugar

console.log(myCoffee.description()); // Plain coffee, with milk, with sugar

console.log(myCoffee.cost()); // 6.5

 

myCoffee = new ChocolateDecorator(myCoffee); // Decorate with Chocolate

console.log(myCoffee.description()); // Plain coffee, with milk, with sugar, with chocolate

console.log(myCoffee.cost()); // 8

Explanation:

  1. Coffee: The base Component that defines the basic functionality of a coffee (cost and description).

  2. CoffeeDecorator: The abstract Decorator that holds a reference to a Coffee object and provides the same interface (cost() and description()). Concrete decorators will extend this class and add specific functionality.

  3. MilkDecorator, SugarDecorator, ChocolateDecorator: These are the ConcreteDecorators that add specific features to the coffee, such as milk, sugar, or chocolate. Each decorator modifies the behavior of the cost() and description() methods.

Advantages of the Decorator Pattern:

  1. Flexibility: You can dynamically add new behavior to an object at runtime without altering its structure. For example, you can mix and match decorators to create different variations of an object.

  2. Single Responsibility Principle: The functionality is added through decorators, which keep the codebase cleaner. Each decorator has a single responsibility: adding one piece of functionality to the object.

  3. Open-Closed Principle: You can extend the functionality of the object by creating new decorators rather than modifying the object itself, adhering to the open-closed principle of object-oriented design.

  4. Composition over inheritance: The decorator pattern avoids deep inheritance hierarchies by allowing new functionality to be added through composition rather than subclassing.

When to Use the Decorator Pattern:

  • When you need to add responsibilities or features to objects dynamically without affecting other objects of the same class.

  • When subclassing would result in an explosion of new classes (for example, multiple combinations of features).

  • When you want to keep the core logic of a class clean and add additional behavior step by step.

  • When you want to extend functionality in a flexible and reusable way without modifying existing code.

 

No comments:

Post a Comment