Decorator Pattern

The Decorator Pattern is a Structural Design Pattern that allows behavior to be added to individual objects dynamically without affecting the behavior of other objects from the same class. In other words, it provides a way to “decorate” an object with additional features or functionalities. Unlike subclassing, which adds behavior at compile time, the Decorator Pattern allows for adding behavior at runtime, making it a more flexible and powerful approach for extending functionality.

The Decorator Pattern is particularly useful in situations where you want to add responsibilities to objects without modifying their class or without affecting other objects of the same class.

Key Characteristics of Decorator Pattern

  1. Extends Functionality Dynamically:
    • The pattern allows functionality to be added to objects dynamically. Instead of modifying a class directly, you wrap the object with additional features or behavior.
  2. Adheres to Open-Closed Principle:
    • The Decorator Pattern adheres to the Open-Closed Principle: classes are open for extension but closed for modification. This means that you can add functionality without modifying existing code, making it ideal for scalable applications.
  3. Flexible Composition:
    • The pattern promotes flexible composition of behaviors. You can combine multiple decorators to add several layers of functionality to an object without altering the object itself.
  4. Transparency:
    • Decorators are transparent to the client code, meaning the client doesn’t need to know if an object is wrapped with decorators. The object behaves just like any other object of its type, with additional behaviors seamlessly added.
  5. Supports Multiple Decorations:
    • The Decorator Pattern supports stacking of multiple decorators on a single object, allowing for highly customizable functionality.
  6. Uses Composition over Inheritance:
    • Unlike subclassing (which uses inheritance), the Decorator Pattern uses composition to extend behavior, enabling you to add features without creating subclasses for every possible feature combination.

Decorator Pattern in Node.js

In Node.js, the Decorator Pattern can be useful for adding functionality to objects dynamically, such as adding logging, caching, or validation. This is particularly helpful in middleware patterns or when enhancing APIs with additional behaviors.

Let’s illustrate this with an example of an online coffee shop where customers can order different types of coffee. Each coffee can be decorated with additional items (such as milk, sugar, or chocolate) that add to its description and cost.

Example Scenario: Coffee Order System

Step 1: Define the Base Component (Coffee)

The Coffee class serves as the base component, which can be decorated with additional items.

// Component Interface
class Coffee {
  getDescription() {
    return 'Basic Coffee';
  }

  cost() {
    return 5; // Base price for a simple coffee
  }
}

Step 2: Create the Decorators

Each decorator class will extend the functionality of the Coffee class by adding new features, such as milk, sugar, or chocolate.

// Decorator: Milk
class MilkDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }

  getDescription() {
    return `${this.coffee.getDescription()} + Milk`;
  }

  cost() {
    return this.coffee.cost() + 1.5; // Milk adds $1.5
  }
}

// Decorator: Sugar
class SugarDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }

  getDescription() {
    return `${this.coffee.getDescription()} + Sugar`;
  }

  cost() {
    return this.coffee.cost() + 0.5; // Sugar adds $0.5
  }
}

// Decorator: Chocolate
class ChocolateDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }

  getDescription() {
    return `${this.coffee.getDescription()} + Chocolate`;
  }

  cost() {
    return this.coffee.cost() + 2.0; // Chocolate adds $2.0
  }
}

Step 3: Using the Decorator Pattern

Now we can use these decorators to add additional features to our coffee order dynamically.

// Order a basic coffee with milk and chocolate
let myCoffee = new Coffee();
myCoffee = new MilkDecorator(myCoffee);
myCoffee = new ChocolateDecorator(myCoffee);

console.log(myCoffee.getDescription()); // Basic Coffee + Milk + Chocolate
console.log(`Total cost: $${myCoffee.cost()}`); // Total cost: $8.5

// Order a basic coffee with sugar
let simpleCoffee = new Coffee();
simpleCoffee = new SugarDecorator(simpleCoffee);

console.log(simpleCoffee.getDescription()); // Basic Coffee + Sugar
console.log(`Total cost: $${simpleCoffee.cost()}`); // Total cost: $5.5

Output:

Basic Coffee + Milk + Chocolate
Total cost: $8.5
Basic Coffee + Sugar
Total cost: $5.5

Explanation:

  • We started with a Coffee object and used decorators to add additional features (milk, chocolate, and sugar).
  • The getDescription method returns the full description of the coffee order, including all added ingredients.
  • The cost method returns the total cost, with each decorator adding its own price to the base coffee cost.

Real-World Examples of Decorator Pattern

Express Middleware:

const express = require('express');
const app = express();

// Decorator Middleware
app.use((req, res, next) => {
  console.log(`Request made to: ${req.url}`);
  next();
});

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(3000, () => console.log('Server running on port 3000'));

Logging and Monitoring:

function logExecutionTime(fn) {
  return function(...args) {
    console.time('Execution Time');
    const result = fn(...args);
    console.timeEnd('Execution Time');
    return result;
  };
}

const sayHello = () => 'Hello!';
const decoratedSayHello = logExecutionTime(sayHello);

console.log(decoratedSayHello());

Data Caching:

When working with data that is retrieved from an external source (such as a database or API), a decorator can be used to cache the data, reducing the number of requests made. For example, a decorator could check if the data is already cached and only make an API call if it’s not.

User Interface Components:

In graphical user interface (GUI) development, decorators can add features to UI components dynamically. For example, a Button object might be decorated with additional behavior, such as being disabled, colored, or adding tooltips.

Encryption and Compression in I/O Streams:

In file handling, decorators can add encryption or compression to streams without modifying the underlying stream class. For example, you could have a decorator that compresses data before writing to a file or encrypts data before sending it over a network.

Payment Systems:

In e-commerce systems, the Decorator Pattern can be used to add additional features to a payment object, such as discount calculation, tax calculation, or handling additional fees. Each decorator would apply its own calculation to the base payment amount, allowing for flexible and reusable payment processing logic.

Conclusion

The Decorator Pattern is a powerful structural design pattern that provides a flexible way to add functionality to individual objects without altering their classes. It allows for dynamic composition of behaviors, promotes code reuse, and adheres to the Open-Closed Principle. In Node.js, the Decorator Pattern is commonly seen in middleware systems like Express, where decorators dynamically add features to requests and responses.

Whether you’re building a logging system, implementing caching, or working with complex GUI components, the Decorator Pattern is a versatile tool for extending functionality. By using this pattern, you can avoid creating subclasses for every possible feature combination and instead apply features as needed, making your code more maintainable and scalable.

Leave a comment

I’m Tran Minh

Hi, I’m Trần Minh, a Solution Architect passionate about crafting innovative and efficient solutions that make technology work seamlessly for you. Whether you’re here to explore the latest in tech or just to get inspired, I hope you find something that sparks joy and curiosity. Let’s embark on this exciting journey together!

Let’s connect