Behavioral Patterns Handbook

12 October, 2023
behavioral patterns

Behavioral patterns

  • Behavioral design patterns focus on defining algorithms and distributing responsibilities among objects, emphasizing patterns of communication between them.
  • These patterns facilitate the management of complex control flow, allowing a shift from controlling the flow to understanding how objects interact.
  • Key patterns include:
    1. Template Method:
      • Intent: Define a generic algorithm structure with customizable steps.
      • Why: Allows subclasses to implement specific steps, providing flexibility.
    2. Interpreter:
      • Intent: Interpret sentences in a language using a class hierarchy.
      • Why: Useful for language processing, with each class representing a grammar rule.
    3. Mediator:
      • Intent: Centralize communication between objects to reduce direct dependencies.
      • Why: Promotes loose coupling by introducing a mediator object for managing interactions.
chain of responsibility pattern

Chain of Responsibility Pattern

  • Intent: To provide a mechanism for handling requests through a chain of candidate objects, where any candidate may fulfill the request based on run-time conditions.

  • Why: Enables even looser coupling, allowing requests to be sent implicitly through a chain of objects. The handling candidate is determined dynamically at runtime.

  • How: Define a chain of handler objects, each with a method to process the request and a reference to the next handler in the chain. Handlers decide whether to process the request or pass it along.

  // Handler interface
  class OrderHandler 
  {
    setNext(handler) 
    { this.nextHandler = handler; }

    handleOrder(order) 
    {
      if (this.nextHandler) 
      { this.nextHandler.handleOrder(order); }
    }
  }

  // Concrete handlers
  class PaymentHandler extends OrderHandler 
  {
    handleOrder(order) 
    {
      if (order.paymentValidated) 
      {
        console.log('Payment processed successfully.');
      } else 
      {
        console.log('Payment validation failed. Passing to the next handler.');
        super.handleOrder(order);
      }
    }
  }

  class StockHandler extends OrderHandler 
  {
    handleOrder(order) 
    {
      if (order.stockAvailable) 
      {
        console.log('Stock is available. Processing order.');
      } else 
      {
        console.log('Stock not available. Passing to the next handler.');
        super.handleOrder(order);
      }
    }
  }

  class ShippingHandler extends OrderHandler 
  {
    handleOrder(order) 
    { console.log('Shipping the order.'); }
  }

  // Example Usage
  const paymentHandler = new PaymentHandler();
  const stockHandler = new StockHandler();
  const shippingHandler = new ShippingHandler();

  paymentHandler.setNext(stockHandler);
  stockHandler.setNext(shippingHandler);

  // Simulating an order
  const order = 
  {
    paymentValidated: true,
    stockAvailable: true,
  };

  // Handling the order
  paymentHandler.handleOrder(order);
  • Explanation:

    1. PaymentHandler, StockHandler, and ShippingHandler are concrete handlers that handle payment validation, stock availability, and shipping, respectively.
    2. Each handler decides whether it can handle the order or should pass it to the next handler in the chain.
    3. The client sets up the chain by linking the handlers (paymentHandler, stockHandler, and shippingHandler) using the setNext method.
    4. The order is then passed through the chain using paymentHandler.handleOrder(order).
    5. Handlers process the order based on run-time conditions, and if a condition is not met, they pass the order to the next handler in the chain.
  • Benefits:

    1. Flexibility: Easily adjust the handling chain at runtime by adding or removing handlers.
    2. Loose Coupling: The sender of a request is decoupled from its receivers, allowing for easy extension without modifying existing code.
    3. Dynamic Handling: Candidates in the chain dynamically determine whether they can handle a request based on run-time conditions.
command pattern

Command Pattern (a.k.a Action, Transaction): Dynamic Request Encapsulation

  • Intent: To encapsulate requests as objects, enabling parameterization of clients with various command implementations, queuing or logging of command executions, and support for undoable operations.

  • Why: Offers a flexible and decoupled approach for handling requests without requiring knowledge of specific request details. Facilitates queuing, logging, and undoing of command executions.

  • How: Define a Command interface with an execute operation. Concrete Command subclasses specify an action-receiver pair, allowing clients to parameterize objects with different commands. An Invoker sets and executes the command, unaware of the specific details of the command's implementation.

// Command interface
class Command 
{
  execute() {}
  undo() {}
  redo() {}
}

// Concrete Command for Typing
class TypeCommand extends Command 
{
  constructor(text, editor) 
  {
    super();
    this.previousText = editor.getText();
    this.newText = text;
    this.editor = editor;
  }

  execute() 
  { this.editor.type(this.newText); }

  undo() 
  { this.editor.setText(this.previousText); }

  redo() 
  { this.execute(); }
}

// Concrete Command for Deleting
class DeleteCommand extends Command 
{
  constructor(editor) 
  {
    super();
    this.previousText = editor.getText();
    this.editor = editor;
  }

  execute() 
  { this.editor.delete(); }

  undo() 
  { this.editor.setText(this.previousText); }

  redo() 
  { this.execute(); }
}

// Receiver
class TextEditor {
  constructor() 
  { this.text = ''; }

  getText() 
  { return this.text; }

  setText(text) 
  {
    this.text = text;
    console.log(`Editor content: ${text}`);
  }

  type(newText) 
  {
    this.text += newText;
    console.log(`Typed: ${newText}`);
  }

  delete() 
  {
    if (this.text.length > 0) 
    {
      this.text = this.text.slice(0, -1);
      console.log('Deleted last character');
    }
  }
}

// Invoker with Undo/Redo
class UndoRedoManager 
{
  constructor() 
  {
    this.commandHistory = [];
    this.undoIndex = -1;
  }

  executeCommand(command) 
  {
    command.execute();
    this.commandHistory.push(command);
    this.undoIndex = this.commandHistory.length - 1;
  }

  undo() 
  {
    if (this.undoIndex >= 0) 
    {
      const command = this.commandHistory[this.undoIndex];
      command.undo();
      this.undoIndex -= 1;
    } else 
    {
      console.log('Nothing to undo.');
    }
  }

  redo() 
  {
    if (this.undoIndex < this.commandHistory.length - 1) 
    {
      this.undoIndex += 1;
      const command = this.commandHistory[this.undoIndex];
      command.redo();
    } else 
    {
      console.log('Nothing to redo.');
    }
  }
}

// Usage
const textEditor = new TextEditor();
const undoRedoManager = new UndoRedoManager();

const typeCommand1 = new TypeCommand('Hello, ', textEditor);
const typeCommand2 = new TypeCommand('world!', textEditor);
const deleteCommand = new DeleteCommand(textEditor);

undoRedoManager.executeCommand(typeCommand1);
undoRedoManager.executeCommand(typeCommand2);
undoRedoManager.undo(); // Undo typing "world!"
undoRedoManager.executeCommand(deleteCommand);
undoRedoManager.redo(); // Redo deleting last character
  • Explanation:

    1. TypeCommand represents typing text, and DeleteCommand represents deleting text.
    2. TextEditor is the receiver that holds the current text.
    3. UndoRedoManager manages the execution, undo, and redo of commands.
  • Usage:

    1. Typing "Hello, " and "world!" into the text editor.
    2. Undoing the last command (deleting "world!").
    3. Deleting the last character.
    4. Redoing the undone command (repeating the deletion).
  • Benefits:

    1. Flexibility: Commands encapsulate operations, making it easy to add, modify, or extend functionality without changing client code.

    2. Decoupling: The Command pattern decouples senders (clients) from receivers (objects that perform actions), reducing dependencies.

    3. Undo/Redo: Commands often support undo and redo operations, providing a convenient way to reverse or repeat user actions.

    4. Queueing and Logging: Commands can be queued for execution or logged, allowing for task management, history tracking, and auditing.

    5. Dynamic Behavior: Enables dynamic behavior as clients can parameterize objects with different commands at runtime.

    6. Composite Commands: Commands can be composed into composite commands, providing a higher level of abstraction for complex operations.

    7. Testing: Easier unit testing as commands are isolated and can be tested independently.

          // Unit Test
        function testTypeCommand() 
        {
          const textEditor = new TextEditor();
          const typeCommand = new TypeCommand('Hello, ', textEditor);
      
          // Execute the command
          typeCommand.execute();
      
          // Check if the editor's text is updated
          const result = textEditor.getText() === 'Hello, ';
          console.assert(result, 'TypeCommand execution failed.');
        }
      
        // Run the unit test
        testTypeCommand();
      • Explanation:

        1. testTypeCommand is a simple unit test function for the TypeCommand.
        2. It creates a TextEditor and a TypeCommand, executes the command, and checks if the text editor's content is updated as expected.
        3. By isolating the command logic into a separate class, we can easily test individual commands independently of the rest of the system. This promotes a modular and testable design.
    8. Command History: Keeps a history of executed commands, useful for implementing features like undo/redo or auditing.

    9. Separation of Concerns: Encourages separation of concerns by isolating the logic for an operation in a command class.

    10. Extensions: Easily extend the system by introducing new commands without modifying existing code.

interpreter pattern

Interpreter Pattern

  • Intent: To provide a mechanism for defining a grammar for a language, creating representations for sentences in that language, and interpreting these sentences using the representations.

  • Why: When a problem can be expressed in sentences of a language and occurs frequently, interpreting these sentences provides a solution. Regular expressions are a common example where sentences (patterns) can be interpreted to match strings.

  • How: Define classes to represent grammar rules (non-terminal symbols) and use these classes to create an abstract syntax tree. Each class has an interpret method, and instances of these classes are used to interpret sentences based on the language grammar.

// Abstract class representing a grammar rule
class RegularExpression 
{
  interpret(context) 
  { throw new Error('interpret method must be implemented by concrete expressions'); }
}

// Concrete expression classes
class LiteralExpression extends RegularExpression 
{
  constructor(literal) 
  {
    super();
    this.literal = literal;
  }

  interpret(context) 
  { return context.includes(this.literal); }
}

class AlternationExpression extends RegularExpression 
{
  constructor(expression1, expression2) 
  {
    super();
    this.expression1 = expression1;
    this.expression2 = expression2;
  }

  interpret(context) 
  { return this.expression1.interpret(context) || this.expression2.interpret(context); }
}

class SequenceExpression extends RegularExpression 
{
  constructor(expression1, expression2) 
  {
    super();
    this.expression1 = expression1;
    this.expression2 = expression2;
  }

  interpret(context) 
  { return this.expression1.interpret(context) && this.expression2.interpret(context); }
}

class RepetitionExpression extends RegularExpression 
{
  constructor(expression) 
  {
    super();
    this.expression = expression;
  }

  interpret(context) 
  {
    // Simulating zero or more repetitions
    let currentContext = context;
    while (this.expression.interpret(currentContext)) 
    {
      currentContext = currentContext.substring(1); // Consume one character
    }
    return true; // Always true as we are simulating zero or more repetitions
  }
}

// Example Usage
const pattern = new SequenceExpression(
  new LiteralExpression('raining'),
  new AlternationExpression(
    new LiteralExpression('dogs'),
    new LiteralExpression('cats')
  ),
  new RepetitionExpression()
);

// Test the pattern
const testSentence1 = 'rainingdogs';
const testSentence2 = 'rainingcatsanddogs';
console.log(pattern.interpret(testSentence1)); // true
console.log(pattern.interpret(testSentence2)); // true
  • Explanation:

    1. RegularExpression is the abstract class representing a grammar rule.
    2. Concrete expression classes (LiteralExpression, AlternationExpression, etc.) implement the interpret method to perform interpretation based on the current context.
    3. The example creates a pattern for sentences like "raining dogs" or "raining cats and dogs."
    4. The interpret method simulates the interpretation of sentences based on the defined grammar.
  • Benefits:

    1. Flexibility: Easily extend the language by adding new grammar rule classes.
    2. Reusability: Reuse existing grammar rule classes to create new language constructs.
    3. Separation of Concerns: Separates the grammar from the interpretation logic, promoting a modular design.
    4. Ease of Maintenance: Changes to language constructs can be localized to their respective classes, making maintenance easier.
iterator pattern

Iterator Pattern (a.k.a Iterator)

  • Intent: To provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

  • Why: An aggregate object, such as a list, should allow access to its elements without revealing its internal structure. Additionally, the need for different traversal methods without bloating the aggregate interface calls for a solution. The Iterator pattern separates access and traversal responsibility, letting iterator objects handle these concerns.

  • How: Define an Iterator interface with methods like first, next, isDone, and currentItem. Implement a concrete iterator (ListIterator) for each aggregate class (List, SkipList). Clients use the iterator to traverse elements without knowing the internal structure of the collection.

    // Iterator interface
    class Iterator 
    {
      first();
      next();
      isDone();
      currentItem();
    }

    // ConcreteIterator for List
    class ListIterator extends Iterator 
    { // Implementation of iterator methods }

    // Abstract Aggregate class
    class AbstractList 
    { createIterator(); }

    // Concrete Aggregate classes
    class List extends AbstractList 
    {
      createIterator() 
      { return new ListIterator(this); }
    }

    class SkipList extends AbstractList 
    {
      createIterator() 
      { return new SkipListIterator(this); }
    }
  • Explanation:

    1. Iterator defines an interface for accessing and traversing elements.
    2. ListIterator is a concrete iterator implementation for arrays.
    3. AbstractList is the aggregate object (collection) that contains elements.
    4. createIterator is a factory method in the aggregate to create an iterator.
    5. Clients use the iterator (e.g., ListIterator) to traverse the elements without knowing the internal structure of the collection.
  • Benefits:

    1. Decoupling: Separates the client code from the details of the collection, promoting a more modular design.
    2. Flexibility: Enables multiple traversal methods for the same collection without modifying the collection itself.
    3. Reusability: Iterator classes can be reused across different collections that implement the same interface.
    4. Polymorphic Iteration: Supports polymorphic iteration by allowing different types of iterators for different collections.
mediator pattern

Mediator Pattern

  • Intent: To define an object that encapsulates how a set of objects interact. It promotes loose coupling by keeping objects from referring to each other explicitly and allows you to vary their interactions independently.

  • Why: In a system with many objects that communicate with each other, direct communication can lead to high coupling, making it difficult to maintain and extend. The Mediator pattern centralizes communication logic, reducing dependencies between objects.

  • How: Define a Mediator interface that declares communication methods. Concrete Mediator implements these methods and keeps references to the Colleague objects. Colleague classes refer to the Mediator interface rather than directly to each other.

    // Mediator interface
    class Mediator 
    { send(message, colleague); }
    
    // Concrete Mediator
    class ConcreteMediator extends Mediator 
    {
      constructor() 
      {
        this.colleague1 = null;
        this.colleague2 = null;
      }
    
      setColleague1(colleague) 
      { this.colleague1 = colleague; }
    
      setColleague2(colleague) 
      { this.colleague2 = colleague; }
    
      send(message, colleague) 
      {
        if (colleague === this.colleague1) 
        {
          this.colleague2.receive(message);
        } else 
        {
          this.colleague1.receive(message);
        }
      }
    }
    
    // Colleague class
    class Colleague 
    {
      constructor(mediator) 
      { this.mediator = mediator; }
    
      send(message) 
      { this.mediator.send(message, this); }
    
      receive(message) 
      { console.log(`Colleague received: ${message}`); }
    }
    
    // Example usage
    const mediator = new ConcreteMediator();
    
    const colleague1 = new Colleague(mediator);
    const colleague2 = new Colleague(mediator);
    
    mediator.setColleague1(colleague1);
    mediator.setColleague2(colleague2);
    
    colleague1.send('Hello from colleague1!');
    // Output: Colleague received: Hello from colleague1!
    
    colleague2.send('Hi from colleague2!');
    // Output: Colleague received: Hi from colleague2!
  • Explanation:

    1. Mediator declares the communication interface, and ConcreteMediator implements it, keeping references to Colleague objects.
    2. Colleague objects communicate through the Mediator, reducing direct dependencies between them.
    3. Changes in communication logic are encapsulated within the Mediator, promoting flexibility and maintainability.
  • Example usage:

    // Mediator: Chatroom
    class Chatroom 
    {
      constructor() 
      { this.users = {}; }
    
      registerUser(user) 
      {
        this.users[user.name] = user;
        user.chatroom = this;
      }
    
      sendMessage(sender, receiverName, message) 
      {
        const receiver = this.users[receiverName];
        if (receiver) 
        {
          receiver.receiveMessage(sender, message);
        } else 
        {
          console.log(`User '${receiverName}' not found.`);
        }
      }
    }
    
    // Colleague: User
    class User 
    {
      constructor(name) 
      {
        this.name = name;
        this.chatroom = null;
      }
    
      send(message, receiverName) 
      {
        if (this.chatroom) 
        {
          console.log(`${this.name} sends message to ${receiverName}: ${message}`);
          this.chatroom.sendMessage(this, receiverName, message);
        } else 
        {
          console.log(`${this.name} is not part of any chatroom.`);
        }
      }
    
      receiveMessage(sender, message) 
      { console.log(`${this.name} receives message from ${sender.name}: ${message}`); }
    }
    
    /*---- Test the Chat Application ----*/
    
    // Create a chatroom
    const chatroom = new Chatroom();
    
    // Create users
    const alice = new User('Alice');
    const bob = new User('Bob');
    const charlie = new User('Charlie');
    
    // Register users with the chatroom
    chatroom.registerUser(alice);
    chatroom.registerUser(bob);
    chatroom.registerUser(charlie);
    
    // Users send messages
    alice.send('Hi, Bob!', 'Bob');
    bob.send('Hello, everyone!', 'Alice');
    charlie.send('Greetings!', 'Bob');
    
    // Expected Output:
    // Alice sends message to Bob: Hi, Bob!
    // Bob receives message from Alice: Hi, Bob!
    // Bob sends message to Alice: Hello, everyone!
    // Alice receives message from Bob: Hello, everyone!
    // Charlie sends message to Bob: Greetings!
    // Bob receives message from Charlie: Greetings!
    • Explanation:

      1. The Chatroom class acts as the mediator. It keeps track of registered users and facilitates message distribution.
      2. The User class represents a user in the chat application. Users can send messages to each other using the send method.
      3. When a user sends a message, it goes through the Chatroom mediator. The mediator ensures that the message is delivered to the appropriate user.
      4. In this example, user1 sends a message to Bob, user2 sends a message to Alice, and user3 sends a message to Bob. The messages are appropriately distributed by the chatroom mediator.
  • Benefits:

    1. Decoupling: Objects are decoupled from each other, as they communicate through the Mediator.
    2. Centralized Control: Communication logic is centralized, making it easier to manage and modify.
    3. Reusability: Mediators can be reused with different sets of colleagues.
    4. Easy Maintenance: Changes in communication logic are localized to the Mediator, reducing the impact on other objects.
memento pattern

Memento Pattern (a.k.a Token)

  • Intent: To capture an object's internal state such that it can be restored to this state later.
  • Why: Enables an object to revert to a previous state, supporting undo mechanisms, history tracking, and recovery.
  • How: Define three main roles: Originator (the object whose state needs to be saved), Memento (an object storing the state of the Originator), and Caretaker (responsible for keeping track of and restoring the Originator's state). The Memento captures the internal state of the Originator without exposing it.
// Originator: Object whose state needs to be saved
class Editor 
{
  constructor() 
  { this.content = ''; }
  type(text) 
  { this.content += text; }

  save() 
  { return new EditorMemento(this.content); }

  restore(memento) 
  { this.content = memento.getContent(); }

  getContent() 
  { return this.content; }
}
// Memento: Object storing the state of the Originator
class EditorMemento 
{
  constructor(content) 
  { this.content = content; }

  getContent() 
  { return this.content; }
}
// Caretaker: Responsible for keeping track of and restoring the Originator's state
class History 
{
  constructor() {
    this.states = [];
  }

  push(editorMemento) 
  { this.states.push(editorMemento); }

  pop() 
  { return this.states.pop(); }
}

// Example Usage
const editor = new Editor();
const history = new History();

editor.type('This is the first sentence. ');
history.push(editor.save());

editor.type('This is the second sentence. ');
history.push(editor.save());

editor.type('This is the third sentence. ');

// Restore to the second state
const secondState = history.pop();
editor.restore(secondState);

console.log(editor.getContent()); // Output: This is the first sentence. This is the second sentence.
  • Explanation:

    • Editor: The object whose state needs to be saved.
    • EditorMemento: Object storing the state of the Editor.
    • History: Keeps track of Editor states and allows reverting.
  • Benefits:

    1. Undo Mechanism: Enables undoing operations by reverting to previous states.
    2. History Tracking: Facilitates keeping a history of an object's state changes.
    3. Isolation of Concerns: Separates the responsibility of state management from the actual object.
  • Use Cases:

    1. Text editors with undo/redo functionality.
    2. Version control systems.
    3. Form data persistence and recovery in web applications.
observer pattern

Observer Pattern (a.k.a Dependents,Publish-Subscribe)

  • Intent: To define a one-to-many dependency between objects, where if one object (the subject) changes state, all its dependents (observers) are notified and updated automatically.

  • Why: Enables a decoupled communication mechanism between objects, allowing changes in one object to propagate to multiple other objects without them being tightly coupled. It ensures a consistent state across dependent objects.

  • How:

    1. Define a Subject interface with methods to add, remove, and notify observers.
    2. Create a ConcreteSubject class implementing the Subject interface. This class maintains state and notifies observers when the state changes.
    3. Define an Observer interface with an update method.
    4. Create ConcreteObserver classes implementing the Observer interface. These are the objects interested in the state changes of the subject.
    5. The subject maintains a collection of observers and notifies them whenever its state changes.
    6. Observers register with the subject to receive updates.
    7. When the subject's state changes, it iterates through its observers and calls their update methods.
  • Example Implementation:

    
    // Observer Pattern: Distributed Event Handling
      // EventBroker: Centralized component managing events
      class EventBroker 
      {
        constructor() 
        { this.subscriptions = {}; }
    
        subscribe(eventType, subscriber) 
        {
          if (!this.subscriptions[eventType]) 
          {
            this.subscriptions[eventType] = [];
          }
          this.subscriptions[eventType].push(subscriber);
        }
        unsubscribe(eventType, subscriber) 
        {
          if (this.subscriptions[eventType]) 
          {
            this.subscriptions[eventType] = this.subscriptions[eventType].filter(sub => sub !== subscriber);
          }
        }
    
        publish(eventType, data) 
        {
          if (this.subscriptions[eventType]) 
          {
            this.subscriptions[eventType].forEach(subscriber => subscriber.notify(eventType, data));
          }
        }
      }
    
      // Service: Represents a service that can publish or subscribe to events
      class Service 
      {
        constructor(name, eventBroker) 
        {
          this.name = name;
          this.eventBroker = eventBroker;
        }
    
        publishEvent(eventType, data) 
        {
          console.log(`${this.name} publishing event: ${eventType}`);
          this.eventBroker.publish(eventType, data);
        }
    
        subscribeToEvent(eventType, callback) 
        {
          console.log(`${this.name} subscribing to event: ${eventType}`);
          const subscriber = new EventSubscriber(this, callback);
          this.eventBroker.subscribe(eventType, subscriber);
          return subscriber;
        }
    
        unsubscribeFromEvent(eventSubscriber) 
        {
          console.log(`${this.name} unsubscribing from event: ${eventSubscriber.eventType}`);
          this.eventBroker.unsubscribe(eventSubscriber.eventType, eventSubscriber);
        }
      }
    
      // EventSubscriber: Represents an object that subscribes to events
      class EventSubscriber 
      {
        constructor(service, callback) 
        {
          this.service = service;
          this.callback = callback;
          this.eventType = null;
        }
    
        notify(eventType, data) 
        {
          console.log(`${this.service.name} received event: ${eventType}`);
          this.callback(data);
        }
    
        unsubscribe() 
        { this.service.unsubscribeFromEvent(this); }
      }
    
      // Example Usage
      const eventBroker = new EventBroker();
    
      const serviceA = new Service('ServiceA', eventBroker);
      const serviceB = new Service('ServiceB', eventBroker);
    
      const subscriberA = serviceA.subscribeToEvent('userLoggedIn', data => {
        console.log(`SubscriberA handling userLoggedIn event: ${JSON.stringify(data)}`);
      });
    
      const subscriberB = serviceB.subscribeToEvent('userLoggedIn', data => {
        console.log(`SubscriberB handling userLoggedIn event: ${JSON.stringify(data)}`);
      });
    
      serviceA.publishEvent('userLoggedIn', { userId: 123, username: 'userA' });
    
      // Output:
      // ServiceA publishing event: userLoggedIn
      // ServiceB subscribing to event: userLoggedIn
      // SubscriberA handling userLoggedIn event: {"userId":123,"username":"userA"}
      // SubscriberB handling userLoggedIn event: {"userId":123,"username":"userA"}
  • Explanation:

    1. EventBroker is a centralized component managing event subscriptions and notifications.

    2. Service represents a service that can publish or subscribe to events.

    3. EventSubscriber is an object that subscribes to events and handles notifications.

    • ServiceA publishes a "userLoggedIn" event, and both ServiceA and ServiceB subscribe to that event. The subscribers (SubscriberA and SubscriberB) handle the event by logging the data.
  • Benefits:

    1. Loose coupling between subjects and observers.

    2. Supports broadcast communication.

    3. Enables dynamically adding or removing observers.

    4. Promotes a clear separation between concerns, enhancing maintainability and scalability.

    • Use Cases:

      1. User interface components that need to react to changes in underlying data models.
      2. Event handling systems in distributed architectures.
      3. Implementing listeners in graphical user interfaces.
      4. Maintaining consistency in state across different components of a system.
    • Related Patterns:

      1. Mediator Pattern: Defines an object that centralizes communication between objects, reducing direct connections.
      2. Publish-Subscribe Pattern: Similar to the Observer Pattern, but with a third component (message broker) managing subscriptions.
state pattern

State Pattern (a.k.a Objects for States)

  • Intent: To allow an object to alter its behavior when its internal state changes. The object will appear to change its class.

  • Why: The State Pattern is used to represent the various states of an object as separate classes. Instead of using a monolithic and conditional approach to manage an object's behavior, the State Pattern encapsulates each state in a separate class, making the object's behavior more modular, extensible, and maintainable.

  • How:

    1. Define a Context class that maintains a reference to the current state object.
    2. Create a State interface or an abstract class that declares methods representing the various states.
    3. Implement concrete state classes that extend the State interface. Each concrete state class implements the behavior associated with a specific state.
    4. The Context class delegates state-specific behavior to the current state object.
    5. The Context class can change its current state, effectively changing its behavior.
  • Example:

      // State Interface
      class WorkflowState 
      {
        handle(context) {}
      }
      // Concrete State Classes
      class DraftState extends WorkflowState 
      {
        handle(context) 
        {
          console.log('Document is in Draft state. Ready for review.');
          context.setState(new ReviewState());
        }
      }
    
      class ReviewState extends WorkflowState 
      {
        handle(context) 
        {
          console.log('Document is in Review state. Awaiting approval.');
          // Logic for review and transitions
          // For simplicity, directly transitioning to the ApprovedState
          context.setState(new ApprovedState());
        }
      }
    
      class ApprovedState extends WorkflowState 
      {
        handle(context) 
        {
          console.log('Document is Approved. Ready for finalization.');
          // Logic for finalization
        }
      }
    
      class RejectedState extends WorkflowState 
      {
        handle(context) 
        {
          console.log('Document is Rejected. Requires revision.');
          // Logic for handling rejection and transitions
          // For simplicity, transitioning back to DraftState
          context.setState(new DraftState());
        }
      }
    
      // Context Class
      class Document 
      {
        constructor() 
        { this.state = new DraftState(); // Initial state is Draft }
    
        setState(state) 
        { this.state = state; }
    
        process() 
        { this.state.handle(this); }
      }
    
      // Example Usage
      const document = new Document();
    
      document.process();  // Output: Document is in Draft state. Ready for review.
      document.process();  // Output: Document is in Review state. Awaiting approval.
      document.process();  // Output: Document is Approved. Ready for finalization.
    
  • Explanation:

    1. State Interface: Define a WorkflowState interface with a handle method.
    2. Concrete States: Implement concrete state classes (e.g., Draft, Review) that follow the WorkflowState interface.
    3. Context (Document):
      • Create a Document class with a current state reference.
      • The Document delegates state-specific behavior to the current state's handle method.
    4. Usage:
      • Instantiate a Document.
      • Call handle to trigger state-specific actions.
      • Easily add or modify states without changing the Document class.
  • Benefits:

    1. Modularity: Encapsulates the behavior associated with each state in a separate class, promoting modular design.
    2. Extensibility: Easy to add new states by creating new state classes without modifying existing code.
    3. Simplifies Code: Eliminates large conditional statements for managing state transitions, making code more readable and maintainable.
    4. Promotes Clean Design: Clarifies the object's behavior by explicitly defining each state.
strategy pattern

Strategy Pattern (a.k.a Policy)

  • Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
template method pattern

Template Method Pattern

  • Intent: Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.

  • Why: Offers a framework for algorithms while allowing subclasses to implement specific steps.

  • How: Define a template method in an abstract class, breaking the algorithm into steps. Concrete subclasses implement these steps, customizing the behavior.

  • Example usage: A simple example of a template method pattern for sorting algorithms. In this case, we have a generic SortAlgorithm class with a template method, and then two subclasses (BubbleSort and QuickSort) that provide specific implementations for comparison and swapping steps.

class SortAlgorithm 
{
  // Template method defining the generic sorting algorithm structure
  sort(array) 
  {
    const n = array.length;

    for (let i = 0; i < n - 1; i++) 
    {
      for (let j = 0; j < n - i - 1; j++) 
      {
        // Algorithm-specific comparison and swapping steps
        if (this.shouldSwap(array[j], array[j + 1])) 
        { this.swap(array, j, j + 1); }
      }
    }
  }
  // Abstract methods to be implemented by subclasses
  shouldSwap(element1, element2) 
  { throw new Error('shouldSwap must be implemented by subclasses'); }

  swap(array, index1, index2) 
  { throw new Error('swap must be implemented by subclasses'); }
}
// Concrete Sorting Algorithm: BubbleSort
class BubbleSort extends SortAlgorithm 
{
  shouldSwap(element1, element2) 
  {
    // Algorithm-specific comparison logic for BubbleSort
    return element1 > element2;
  }

  swap(array, index1, index2) 
  {
    // Algorithm-specific swapping logic for BubbleSort
    const temp = array[index1];
    array[index1] = array[index2];
    array[index2] = temp;
  }
}

// Concrete Sorting Algorithm: QuickSort
class QuickSort extends SortAlgorithm 
{
  shouldSwap(element1, element2) 
  {
    // Algorithm-specific comparison logic for QuickSort
    return element1 > element2;
  }

  swap(array, index1, index2) 
  {
    // Algorithm-specific swapping logic for QuickSort
    const temp = array[index1];
    array[index1] = array[index2];
    array[index2] = temp;
  }
}
  • Explanation:
    1. SortAlgorithm is the abstract class defining the template method (sort) and declaring abstract methods (shouldSwap and swap) to be implemented by concrete subclasses.
    2. BubbleSort and QuickSort are concrete subclasses that inherit from SortAlgorithm and provide specific implementations for the abstract methods based on their sorting strategies.
    3. The sort method in SortAlgorithm uses the template method pattern, calling the abstract methods for algorithm-specific comparison and swapping steps.
// Example Usage
  const arrayToSort = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];

  // BubbleSort
  const bubbleSort = new BubbleSort();
  const arrayBubbleSort = arrayToSort.slice();
  bubbleSort.sort(arrayBubbleSort);
  console.log('BubbleSort Result:', arrayBubbleSort);

  // QuickSort
  const quickSort = new QuickSort();
  const arrayQuickSort = arrayToSort.slice();
  quickSort.sort(arrayQuickSort);
  console.log('QuickSort Result:', arrayQuickSort);
  /*-- Logs --*/
  BubbleSort Result: [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]
  QuickSort Result: [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]
visitor pattern

Visitor Pattern

  • Intent: Represent an operation to be performed on the elements of an objectstructure. Visitor lets you define a new operation without changing theclasses of the elements on which it operates.

Wikipedia-Sofware design patterns

Subscribe to the Newsletter

Get my latest posts and project updates by email