Web Development

Behavioral Design Patterns for Node.js: Observer, Strategy, and Command Patterns

In this blog post, we'll explore three key behavioral design patterns—Observer, Strategy, and Command Patterns—and explore how they can be implemented in Node.js applications.

Mar 4, 2024 4 min read
Behavioral Design Patterns for Node.js: Observer, Strategy, and Command Patterns

Design patterns give you reusable solutions to recurring problems. Behavioral patterns are one category; they govern how objects interact and hand off responsibilities to each other. This post covers three of them: the Observer, Strategy, and Command patterns, with concrete Node.js examples for each.

What are Behavioral Design Patterns?

Behavioral patterns define how objects collaborate and hand off work. Rather than dictating structure (that's creational and structural patterns), they focus on the responsibilities each object takes on and how those responsibilities flow at runtime. Done well, they reduce coupling and make codebases easier to extend without touching existing logic.

Observer Pattern

The Observer Pattern is a behavioral design pattern where an object (the subject) maintains a list of dependents (observers) and notifies them of state changes, typically by calling one of their methods. It's widely used in event-driven architectures.

Key Components

  • Subject: The object being observed. It maintains a list of observers and notifies them of state changes.
  • Observer: The objects that are interested in the state changes of the subject. They register themselves with the subject and receive notifications.

Implementation in Node.js

// Subject
class Subject {
  constructor() {
    this.observers = [];
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  notifyObservers(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

// Observer
class Observer {
  update(data) {
    console.log('Received data:', data);
  }
}

// Usage
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

subject.addObserver(observer1);
subject.addObserver(observer2);

subject.notifyObservers('New data');

Important Points

  • Loose Coupling: The Observer Pattern promotes loose coupling between subjects and observers, allowing for easier maintenance and scalability.
  • Event-Driven Architecture: It is commonly used in event-driven architectures, such as in Node.js applications handling asynchronous operations.

Strategy Pattern

The Strategy Pattern lets you select an algorithm's behavior at runtime. It defines a family of algorithms, encapsulates each one, and makes them interchangeable, so the algorithm can vary independently from the clients that use it.

Key Components

  • Context: The object that maintains a reference to the strategy object and delegates its behavior.
  • Strategy: The interface or abstract class defining a family of algorithms.
  • Concrete Strategies: The concrete implementations of the strategy interface.

Implementation in Node.js

// Strategy Interface
class SortingStrategy {
  sort(array) {}
}

// Concrete Strategies
class BubbleSort extends SortingStrategy {
  sort(array) {
    return array.sort((a, b) => a - b);
  }
}

class QuickSort extends SortingStrategy {
  sort(array) {
    return array.sort((a, b) => a - b);
  }
}

// Context
class SortContext {
  constructor(strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  executeStrategy(array) {
    return this.strategy.sort(array);
  }
}

// Usage
const bubbleSortStrategy = new BubbleSort();
const sortContext = new SortContext(bubbleSortStrategy);
const array = [3, 1, 2];

console.log(sortContext.executeStrategy(array)); // [1, 2, 3]

const quickSortStrategy = new QuickSort();
sortContext.setStrategy(quickSortStrategy);

console.log(sortContext.executeStrategy(array)); // [1, 2, 3]

Important Points

  • Encapsulation: The Strategy Pattern encapsulates algorithms, making them interchangeable without affecting the client's code.
  • Run-Time Selection: It allows algorithms to be selected at runtime based on specific requirements or conditions.

Command Pattern

The Command Pattern encapsulates a request as an object, so you can parameterize clients with different operations, queue them, and execute them at different times. It decouples the sender from the receiver, which makes the code more flexible and easier to extend.

Key Components

  • Command: The interface or abstract class defining the command's execution method.
  • Concrete Command: The concrete implementations of the command interface, encapsulating a specific action.
  • Invoker: The object that invokes the command and forwards the request to the appropriate receiver.
  • Receiver: The object that performs the actual action associated with the command.

Implementation in Node.js

// Command Interface
class Command {
  execute() {}
}

// Concrete Command
class LightOnCommand extends Command {
  constructor(light) {
    super();
    this.light = light;
  }

  execute() {
    this.light.turnOn();
  }
}

// Receiver
class Light {
  turnOn() {
    console.log('Light is on');
  }
}

// Invoker
class RemoteControl {
  constructor() {
    this.command = null;
  }

  setCommand(command) {
    this.command = command;
  }

  pressButton() {
    this.command.execute();
  }
}

// Usage
const light = new Light();
const lightOnCommand = new LightOnCommand(light);
const remoteControl = new RemoteControl();

remoteControl.setCommand(lightOnCommand);
remoteControl.pressButton(); // Output: Light is on

Important Points

  • Decoupling: The Command Pattern decouples the sender from the receiver, allowing for flexible and extensible code.
  • Undo Operations: It enables undo operations by storing the history of commands executed, facilitating the reversal of actions if necessary.

Conclusion

Observer, Strategy, and Command each solve a different coordination problem. Observer decouples event producers from their consumers. Strategy lets you swap algorithms without touching client code. Command turns operations into objects you can queue, log, or reverse. Start with whichever one matches a pain point you already have. Retrofitting patterns onto code that doesn't need them adds complexity without benefit. When the problem fits, though, the payoff in testability and flexibility is real.


Applying these patterns to your Node.js applications pays off most when the underlying problem genuinely matches: Observer for event-driven flows, Strategy when behavior needs to vary per caller, Command when you need audit trails or undo support. Pick the pattern that fits; don't force one that doesn't.

Observer Pattern Node.jsStrategy Pattern Node.jsCommand Pattern Node.js
Grow your business with us

Take your business to the next level.

Tell us what you're building. We'll come back inside one business day with a fixed scope, timeline, and team — or an honest “this isn't a fit”.

ENGINEERING PHILOSOPHY

Code is useless if it's not comprehensible to those who maintain it. We write code the next person can actually understand.