Decorator Design Pattern in JavaScript

1. Definition

The Decorator Design Pattern is a structural 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.

2. Problem Statement

Imagine you're building a library for UI components. You have a base component, let's say a Button. Now, you want to add additional functionalities to certain buttons, like making one a primary button, another one having a tooltip, and another being disabled, without creating subclasses for each variant. How can you add these functionalities without bloating the main Button class and while keeping the Single Responsibility Principle?

3. Solution

Use the Decorator Design Pattern. Instead of adding all possible features to the Button class, create separate decorator classes for each additional feature and wrap the original button object with these decorators.

4. Real-World Use Cases

1. Graphical User Interfaces: Enhancing widgets with additional behavior like borders, shadows, etc.

2. Middleware in Express.js: Adding functionalities to request-response cycle without modifying the core request object.

3. File Streams: Decorating basic streams with additional features like buffering, encoding, etc.

5. Implementation Steps

1. Define a component interface.

2. Create a concrete component implementing this interface.

3. Create decorator classes that also implement the component interface.

4. Decorators contain a reference to a component and can wrap functions, add new ones, or do both.

6. Implementation in JavaScript

// Step 1: Component Interface
function Component() {
    this.operation = function() {};
}
// Step 2: Concrete Component
function Button(label) {
    this.label = label;
}
Button.prototype = Object.create(Component.prototype);
Button.prototype.operation = function() {
    return "Button: " + this.label;
};
// Step 3: Create decorators
function PrimaryButtonDecorator(button) {
    this.button = button;
}
PrimaryButtonDecorator.prototype = Object.create(Component.prototype);
PrimaryButtonDecorator.prototype.operation = function() {
    return this.button.operation() + " (Primary)";
};
function DisabledButtonDecorator(button) {
    this.button = button;
}
DisabledButtonDecorator.prototype = Object.create(Component.prototype);
DisabledButtonDecorator.prototype.operation = function() {
    return this.button.operation() + " (Disabled)";
};
// Client Code
const simpleButton = new Button("Click me");
const primaryButton = new PrimaryButtonDecorator(simpleButton);
const disabledPrimaryButton = new DisabledButtonDecorator(primaryButton);
console.log(disabledPrimaryButton.operation());

Output:

Button: Click me (Primary) (Disabled)

Explanation:

1. Component is the base interface that both the main components and decorators adhere to.

2. Button is a concrete component.

3. PrimaryButtonDecorator and DisabledButtonDecorator are decorators. They wrap around a component and enhance its functionality.

4. In the client code, we wrapped a simple button with the PrimaryButtonDecorator and then wrapped the result again with the DisabledButtonDecorator. The result is a button that is both primary and disabled.

7. When to use?

Use the Decorator pattern when:

1. You need to add responsibilities to individual objects dynamically and transparently, without affecting other objects.

2. Extension by subclassing is impractical or impossible due to a large number of independent extensions possible.


Comments