Behavioral Design Patterns

0

Behavioral design patterns deal with how objects interact and communicate with each other. They help in managing the responsibilities and behaviors of objects in a system


behavioral pattern


Here are some examples of common behavioral design patterns:

1. Observer Pattern:

The Observer pattern defines a one-to-many relationship between objects. When one object changes state, all its dependents are notified and updated automatically.

Intent:

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Example:

Event handling in web applications, where multiple subscribers (observers) listen to changes in an event source (subject).

For ex:

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

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

  removeObserver(observer) {
    const index = this.observers.indexOf(observer);
    if (index !== -1) {
      this.observers.splice(index, 1);
    }
  }

  notify(message) {
    this.observers.forEach(observer => observer.update(message));
  }
}

class Observer {
  constructor(name) {
    this.name = name;
  }

  update(message) {
    console.log(`${this.name} received message: ${message}`);
  }
}

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

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

subject.notify('Hello, Observers');


2. Strategy Pattern:

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It allows the client to choose the appropriate algorithm at runtime.

Intent:

Define a family of algorithms, encapsulate each one, and make them interchangeable. The strategy pattern allows the client to choose the appropriate algorithm at runtime.

Example:

Sorting algorithms in a sorting application, where you can dynamically switch between different sorting strategies.

For ex.

class PaymentStrategy {
  pay(amount) {}
}

class CreditCardPayment extends PaymentStrategy {
  pay(amount) {
    console.log(`Paid $${amount} via Credit Card`);
  }
}

class PayPalPayment extends PaymentStrategy {
  pay(amount) {
    console.log(`Paid $${amount} via PayPal`);
  }
}

class ShoppingCart {
  constructor(paymentStrategy) {
    this.paymentStrategy = paymentStrategy;
  }

  checkout(amount) {
    this.paymentStrategy.pay(amount);
  }
}

// Usage
const cart1 = new ShoppingCart(new CreditCardPayment());
const cart2 = new ShoppingCart(new PayPalPayment());

cart1.checkout(100); // Output: Paid $100 via Credit Card
cart2.checkout(50);  // Output: Paid $50 via PayPal


3. Command Pattern:

The Command pattern encapsulates a request as an object, thereby allowing us to parameterize clients with queues, requests, and operations.

Intent:

Encapsulate a request as an object, thereby allowing us to parameterize clients with queues, requests, and operations. It also provides support for undoable operations.

Example:

A remote control for various electronic devices, where each button press corresponds to a command that can be executed and undone.

For ex:

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

  turnOff() {
    console.log('Light is off');
  }
}

class Command {
  constructor(light) {
    this.light = light;
  }
  execute() {}
}

class TurnOnCommand extends Command {
  execute() {
    this.light.turnOn();
  }
}

class TurnOffCommand extends Command {
  execute() {
    this.light.turnOff();
  }
}

class RemoteControl {
  constructor() {
    this.commands = [];
  }

  addCommand(command) {
    this.commands.push(command);
  }

  pressButton() {
    this.commands.forEach(command => command.execute());
  }
}

// Usage
const light = new Light();
const turnOn = new TurnOnCommand(light);
const turnOff = new TurnOffCommand(light);

const remote = new RemoteControl();
remote.addCommand(turnOn);
remote.addCommand(turnOff);

remote.pressButton(); // Output: Light is on, Light is off

4. Chain of Responsibility Pattern:

The Chain of Responsibility Pattern is a design pattern that allows you to pass requests along a chain of handlers. Each handler decides whether to process the request or pass it to the next handler in the chain. Here's a JavaScript example of the Chain of Responsibility Pattern:

Intent:

Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.


Example:

Middleware in web frameworks, where each middleware component processes a part of an HTTP request.

// Define a base handler with a reference to the next handler in the chain
class Handler {
  constructor() {
    this.nextHandler = null;
  }

  setNextHandler(handler) {
    this.nextHandler = handler;
  }

  handleRequest(request) {
    if (this.nextHandler) {
      this.nextHandler.handleRequest(request);
    }
  }
}

// Concrete handlers
class ConcreteHandlerA extends Handler {
  handleRequest(request) {
    if (request === 'A') {
      console.log('Handler A processed the request');
    } else {
      super.handleRequest(request);
    }
  }
}

class ConcreteHandlerB extends Handler {
  handleRequest(request) {
    if (request === 'B') {
      console.log('Handler B processed the request');
    } else {
      super.handleRequest(request);
    }
  }
}

class ConcreteHandlerC extends Handler {
  handleRequest(request) {
    if (request === 'C') {
      console.log('Handler C processed the request');
    } else {
      console.log('Request cannot be handled.');
    }
  }
}

// Usage
const handlerA = new ConcreteHandlerA();
const handlerB = new ConcreteHandlerB();
const handlerC = new ConcreteHandlerC();

handlerA.setNextHandler(handlerB);
handlerB.setNextHandler(handlerC);

handlerA.handleRequest('A');  // Output: Handler A processed the request
handlerA.handleRequest('B');  // Output: Handler B processed the request
handlerA.handleRequest('C');  // Output: Handler C processed the request
handlerA.handleRequest('D');  // Output: Request cannot be handled.

In this example:

  1. Handler is the base handler class that defines the structure of the chain and a reference to the next handler.
  2. Concrete handlers like ConcreteHandlerA, ConcreteHandlerB, and ConcreteHandlerC inherit from the base Handler class and provide their own implementations of the handleRequest method.
  3. The client sets up the chain of handlers by linking them using the setNextHandler method.
  4. When a request is made, it is passed through the chain of handlers. If a handler can process the request, it does so; otherwise, it passes the request to the next handler in the chain.

The Chain of Responsibility Pattern helps decouple the sender and receiver of a request and allows multiple objects to handle the request without the sender knowing which object ultimately processes it.


5. State Pattern:

The State Pattern is a behavioral design pattern that allows an object to change its behavior when its internal state changes.

Intent:

Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.


Example:

A video player that transitions between different states like playing, pausing, or stopping.

Here's a JavaScript example of the State Pattern:

// Context class that maintains a reference to the current state
class Context {
  constructor() {
    this.state = null;
  }

  setState(state) {
    this.state = state;
  }

  request() {
    if (this.state) {
      this.state.handle();
    }
  }
}

// State interface with a handle method
class State {
  handle() {}
}

// Concrete states
class StateA extends State {
  handle() {
    console.log('State A is handling the request.');
  }
}

class StateB extends State {
  handle() {
    console.log('State B is handling the request.');
  }
}

class StateC extends State {
  handle() {
    console.log('State C is handling the request.');
  }
}

// Usage
const context = new Context();
const stateA = new StateA();
const stateB = new StateB();
const stateC = new StateC();

context.setState(stateA);
context.request(); // Output: State A is handling the request.

context.setState(stateB);
context.request(); // Output: State B is handling the request.

context.setState(stateC);
context.request(); // Output: State C is handling the request.

In this example:

  1. The Context class maintains a reference to the current state and has a request method to trigger state-specific behavior.
  2. The State interface defines a handle method that concrete states must implement.
  3. Concrete states like StateA, StateB, and StateC implement the handle method with their specific behavior.
  4. The client (usage) sets the initial state in the context and triggers the request, which delegates to the current state's handle method.

The State Pattern is useful when an object's behavior depends on its internal state, and it allows for easy addition of new states without modifying the context class. It promotes encapsulation and reduces conditionals in the code.


6. Visitor Pattern:

The Visitor Pattern is a behavioral design pattern that allows you to add new operations to objects without having to modify their class. It is particularly useful when working with complex object structures.

Intent:

Represent an operation to be performed on the elements of an object structure. Visitors allow you to add further operations without having to modify the elements themselves.


Example:

A document structure with different types of elements like paragraphs, images, and tables, where a visitor can be used to perform various operations like rendering or counting elements.

Here's a JavaScript example of the Visitor Pattern:

// Define the Visitor interface with visit methods for different elements
class Visitor {
  visitElementA(element) {}
  visitElementB(element) {}
}

// Define concrete elements that accept visitors
class ElementA {
  accept(visitor) {
    visitor.visitElementA(this);
  }

  operationA() {
    console.log('Operation A on Element A');
  }
}

class ElementB {
  accept(visitor) {
    visitor.visitElementB(this);
  }

  operationB() {
    console.log('Operation B on Element B');
  }
}

// Concrete visitor that defines the operations to be performed on elements
class ConcreteVisitor extends Visitor {
  visitElementA(element) {
    element.operationA();
  }

  visitElementB(element) {
    element.operationB();
  }
}

// Usage
const elementA = new ElementA();
const elementB = new ElementB();
const visitor = new ConcreteVisitor();

elementA.accept(visitor); // Output: Operation A on Element A
elementB.accept(visitor); // Output: Operation B on Element B

In this example:

  1. Visitor is an interface that defines the visitElementA and visitElementB methods for different elements.
  2. ElementA and ElementB are concrete elements that implement an accept method. The accept method allows the elements to accept a visitor and delegate the operation to the visitor.
  3. ConcreteVisitor is a concrete visitor that defines the operations to be performed on elements. It implements the visitElementA and visitElementB methods, which specify what to do when visiting each type of element.
  4. In the usage part, elementA and elementB accept the visitor, and the visitor performs operations on them without modifying the element classes.

The Visitor Pattern is helpful when you need to perform operations on complex object structures without altering their structure. It separates the algorithm from the elements, making it easier to add new operations.


7. Memento Pattern:

The Memento Pattern is a behavioral design pattern that allows an object's internal state to be captured and restored without exposing its details. It's useful for implementing features like undo/redo functionality.

Intent:

Capture and externalize an object's internal state so that the object can be restored to this state later.


Example:

Undo/redo functionality in a text editor, where you can save and restore the document's previous states.

Here's a JavaScript example of the Memento Pattern:

// Originator class (the object whose state needs to be saved)
class Originator {
  constructor(state) {
    this.state = state;
  }

  // Create a memento with the current state
  createMemento() {
    return new Memento(this.state);
  }

  // Restore the state from a memento
  restoreFromMemento(memento) {
    this.state = memento.getState();
  }

  // Getter and setter for the state
  getState() {
    return this.state;
  }

  setState(state) {
    this.state = state;
  }
}

// Memento class (stores the state of the Originator)
class Memento {
  constructor(state) {
    this.state = state;
  }

  getState() {
    return this.state;
  }
}

// Caretaker class (responsible for keeping track of multiple mementos)
class Caretaker {
  constructor() {
    this.mementos = [];
  }

  addMemento(memento) {
    this.mementos.push(memento);
  }

  getMemento(index) {
    return this.mementos[index];
  }
}

// Usage
const originator = new Originator('State 1');
const caretaker = new Caretaker();

caretaker.addMemento(originator.createMemento()); // Save the initial state
originator.setState('State 2'); // Change the state
caretaker.addMemento(originator.createMemento()); // Save the new state
console.log('Current State:', originator.getState());
originator.restoreFromMemento(caretaker.getMemento(0)); // Restore the initial state
console.log('Restored State:', originator.getState());

In this example:

  1. The Originator class is the object whose state needs to be saved. It can create mementos to capture its current state and restore its state from a memento.
  2. The Memento class is used to store the state of the Originator.
  3. The Caretaker class keeps track of multiple mementos. It can add mementos to its collection and retrieve them.
  4. In the usage part, the initial state of the Originator is saved as a memento using createMemento. The state is then changed to a new value, and another memento is created and saved. Finally, the state is restored from the first memento using restoreFromMemento.

The Memento Pattern is a powerful way to implement features like undo/redo or to save and restore an object's state without exposing its internals. It provides a way to decouple the originator from the details of the state storage.


8. Interpreter Pattern:

The Interpreter Pattern is a behavioral design pattern that is used to define a language's grammar and provides an interpreter to interpret and execute expressions in that language. Here's a simple

Intent:

Provide a way to evaluate language grammar or expressions. It defines grammar for a simple language and provides an interpreter for the language.


Example:

A regular expression parser, where it interprets and matches strings based on a regular expression pattern.

JavaScript example of the Interpreter Pattern for evaluating arithmetic expressions:

// Abstract Expression class
class Expression {
  interpret(context) {
    // To be implemented by concrete expressions
  }
}

// Terminal Expression
class NumberExpression extends Expression {
  constructor(value) {
    super();
    this.value = value;
  }

  interpret(context) {
    return this.value;
  }
}

// Non-terminal Expression (Addition)
class AdditionExpression extends Expression {
  constructor(left, right) {
    super();
    this.left = left;
    this.right = right;
  }

  interpret(context) {
    return this.left.interpret(context) + this.right.interpret(context);
  }
}

// Non-terminal Expression (Subtraction)
class SubtractionExpression extends Expression {
  constructor(left, right) {
    super();
    this.left = left;
    this.right = right;
  }

  interpret(context) {
    return this.left.interpret(context) - this.right.interpret(context);
  }
}

// Context to hold variables and interpret expressions
class Context {
  constructor() {
    this.variables = {};
  }

  setVariable(name, value) {
    this.variables[name] = value;
  }

  getVariable(name) {
    return this.variables[name];
  }
}

// Usage
const context = new Context();
context.setVariable('x', 10);
context.setVariable('y', 5);

const expression = new AdditionExpression(
  new NumberExpression(context.getVariable('x')),
  new SubtractionExpression(
    new NumberExpression(context.getVariable('y')),
    new NumberExpression(2)
  )
);

const result = expression.interpret(context);
console.log('Result:', result); // Output: Result: 13

In this example:

  1. The Expression class is the abstract expression that defines the interpret method. Subclasses implement this method for specific types of expressions.
  2. NumberExpression is a terminal expression that interprets and returns a numeric value.
  3. AdditionExpression and SubtractionExpression are non-terminal expressions that represent addition and subtraction operations. They evaluate their left and right expressions.
  4. The Context class holds variables and provides a way to set and get variable values.
  5. In the usage part, a context is created and variables 'x' and 'y' are set. An arithmetic expression is constructed using terminal and non-terminal expressions. Finally, the expression is interpreted, and the result is computed.

The Interpreter Pattern can be used to build interpreters for various domain-specific languages or to define rules for parsing and interpreting expressions. It decouples the grammar and interpretation from the client code.


9. Template Method Pattern:

The Template Method Pattern is a behavioral design pattern that defines the skeleton of an algorithm in the base class but lets subclasses override specific steps of the algorithm without changing its structure.

Intent:

Define the skeleton of an algorithm in the base class but let subclasses override specific steps of the algorithm without changing its structure.


Example:

In a game development framework, a base class for game objects defines the game loop, collision detection, and rendering, while concrete subclasses implement game-specific behavior.

Here's a JavaScript example of the Template Method Pattern:

// Abstract class defining the template method
class AbstractClass {
  templateMethod() {
    this.step1();
    this.step2();
    this.step3();
  }

  // Abstract methods to be implemented by subclasses
  step1() {
    throw new Error('Abstract method step1 must be implemented');
  }

  step2() {
    throw new Error('Abstract method step2 must be implemented');
  }

  step3() {
    throw new Error('Abstract method step3 must be implemented');
  }
}

// Concrete subclass that implements the abstract methods
class ConcreteClass extends AbstractClass {
  step1() {
    console.log('ConcreteClass: Step 1');
  }

  step2() {
    console.log('ConcreteClass: Step 2');
  }

  step3() {
    console.log('ConcreteClass: Step 3');
  }
}

// Usage
const concreteInstance = new ConcreteClass();
concreteInstance.templateMethod();

In this example:

  1. AbstractClass is an abstract class that defines the template method templateMethod. It also declares three abstract methods step1, step2, and step3 that are meant to be overridden by concrete subclasses.
  2. ConcreteClass is a concrete subclass that extends AbstractClass and implements the abstract methods step1, step2, and step3.
  3. In the usage part, an instance of ConcreteClass is created and the templateMethod is called, which executes the steps in a predefined order. The concrete subclass provides its own implementation for each step.

The Template Method Pattern is useful when you have an algorithm that follows a common structure but allows some variation in the specific steps. It promotes reusability of code and enforces the structure of the algorithm.


 

Post a Comment

0Comments
Post a Comment (0)