behavioral-design-patterns-in-javascript-p1

Behavioral Design Patterns in JavaScript (p1)

Chain of Responsibility Design Pattern

This pattern avoids coupling the sender of a request to its receiver by allowing the request pass along a chain of handlers. Each handler decides either to process the request or to pass it to the next handler in the chain.


chain-of-responsibility-design-pattern-js

Chain of Responsibility pattern is applicable when:

  • the request is handled by multiple objects, determined at runtime
  • you dont want to explicitly specify handlers in code

chain-of-responsibility-design-pattern

handler.ts

// defines an interface for handling the requests
// (optional) implements the successor link
export interface Handler {
  process(file: any): void;
}

file.ts

export class File {
  private _fileName;
  private _fileType;
  private _filePath;

  constructor(fileName: string, fileType: string, filePath: string) {
    this._fileName = fileName;
    this._fileType = fileType;
    this._filePath = filePath;
  }

  get fileName() {
    return this._fileName;
  }
  get fileType() {
    return this._fileType;
  }
  get filePath() {
    return this._filePath;
  }
}

textFileHandler.ts

import { File } from './file.js';
import { Handler } from './handler.js';

// ConcreteHandler 
export class TextFileHandler implements Handler {
  // the reference to the next handler in the chain
  private _handler!: Handler;
  private _handlerName: string = '';

  constructor(handlerName: string) {
    this._handlerName = handlerName;
  }

  set handler(newHandler: any) {
    this._handler = newHandler;
  }

  get handler() {
    return this._handler;
  }

  get handlerName() {
    return this._handlerName;
  }

  // if the ConcreteHandler can't handle the request it forwards it to its successor
  process(file: File): void {
    if (file.fileType == 'text') {
      console.log(`Process file ${JSON.stringify(file)} by ${this.handlerName}`);
    } else if (this.handler != null) {
      console.log(`Forward process from ${this.handlerName} to ${this.handler.handlerName}`);
      this.handler.process(file);
    } else {
      console.log('Unsupported file!');
    }
  }
}

docFileHandler.ts

import { File } from './file.js';
import { Handler } from './handler.js';

export class DocFileHandler implements Handler {
  private _handler!: Handler;
  private _handlerName: string;

  constructor(handlerName: string) {
    this._handlerName = handlerName;
  }

  set handler(newHandler: any) {
    this._handler = newHandler;
  }

  get handler() {
    return this._handler;
  }

  get handlerName() {
    return this._handlerName;
  }

  process(file: File): void {
    if (file.fileType == 'doc') {
      console.log(`Process file ${JSON.stringify(file)} by ${this.handlerName}`);
    } else if (this.handler != null) {
      console.log(`Forward process from ${this.handlerName} to ${this.handler.handlerName}`);
      this.handler.process(file);
    } else {
      console.log('Unsupported file!');
    }
  }
}

excelFileHandler.ts

import { File } from './file.js';
import { Handler } from './handler.js';

export class DocFileHandler implements Handler {
//....
 process(file: File): void {
    if (file.fileType == 'excel') {
      console.log(`Process file ${JSON.stringify(file)} by ${this.handlerName}`);
    } else if (this.handler != null) {
      console.log(`Forward process from ${this.handlerName} to ${this.handler.handlerName}`);
      this.handler.process(file);
    } else {
      console.log('Unsupported file!');
    }
  }
}

app.ts

import { DocFileHandler } from './docFileHandler.js';
import { ExcelFileHandler } from './excelFileHandler.js';
import { File } from './file.js';
import { TextFileHandler } from './textFileHandler.js';

class App {
  run() {
    let file = null;
    const textHandler = new TextFileHandler('Text Handler');
    const docHandler = new DocFileHandler('Doc Handler');
    const excelHandler = new ExcelFileHandler('Excel Handler');

    textHandler.handler = docHandler;
    docHandler.handler = excelHandler;

    file = new File('infos', 'doc', '/home');
    textHandler.process(file);
    console.log('----------');
    file = new File('reports', 'excel', '/home');
    textHandler.process(file);
  }
}

const app = new App();
app.run();

Run app:

tsc
node dist/chain-of-responsibility/app.js

Forward process from Text Handler to Doc Handler
Process file {"_fileName":"infos","_fileType":"doc","_filePath":"/home"} by Doc Handler
----------
Forward process from Text Handler to Doc Handler
Forward process from Doc Handler to Excel Handler
Process file {"_fileName":"reports","_fileType":"excel","_filePath":"/home"} by Excel Handler

Command Design Pattern

Encapsulate a request as an object thus decoupling the object which knows how to call a command and the object which handles the command execution. This pattern can store a list of executed commands and undo the operations.

command-design-pattern-in-javascript

app.ts

import { CopyCommand } from './copyCommand.js';
import { CutCommand } from './cutCommand.js';
import { Document } from './document.js';
import { DocumentInvoker } from './documentInvoker.js';
import { PasteCommand } from './pasteCommand.js';

// Create a receiver
const RECEIVER = new Document('bala ma cara home');

// Create Commands
const cutCommand = new CutCommand(RECEIVER);
const copyCommand = new CopyCommand(RECEIVER);
const pasteCommand = new PasteCommand(RECEIVER);

// Register the commands with the invoker
const invoker = new DocumentInvoker();
invoker.register('CUT', cutCommand);
invoker.register('COPY', copyCommand);
invoker.register('PASTE', pasteCommand);

// Execute the commands registered on the Invoker
invoker.execute('CUT', 1, 3);
invoker.undo('CUT');
console.log('--------');

invoker.execute('COPY', 2, 4);
console.log('--------');

invoker.execute('PASTE', 1, 'XZ');

command.ts

// Command: an interface or an abstract class defining operations for the command objects
export interface Command {
  execute(...args: any[]): void;
  // undoing the operation
  undo(): void;
  // return true if the command can be undone, false otherwise
  isReversible(): boolean;
}

document.ts

// The Receiver - invoked by command class to perform the requested operation
export class Document {
  #text = '';

  get text() {
    return this.#text;
  }

  constructor(text: string) {
    this.#text = text;
    console.log(text + '\n');
  }

  insert(pos: number, str: string) {
    this.#text = this.#text.slice(0, pos) + str + this.#text.slice(pos);
    console.log(`insert ${str} at pos ${pos} => ${this.#text}`);
  }

  delete(pos: number, noChars: number): string {
    const deleted = this.#text.slice(pos, noChars + 1);
    this.#text = this.#text.slice(0, pos) + this.#text.slice(pos + noChars);
    console.log(`delete ${noChars} chars from pos ${pos} => ${deleted} => ${this.text}`);
    return deleted;
  }

  copy(pos: number, noChars: number): string {
    const res = this.#text.slice(pos, pos + noChars);
    console.log(`copy ${noChars} chars from pos ${pos} => ${res}`);
    return res;
  }
}

documentInvoker.ts

import { Command } from './command.js';

// Invoker Class - exposed to the client, responsible for invoking the appropriate 
// command object to complete a task
export class DocumentInvoker {
    #commands: { [id: string]: Command }
  
    constructor() {
        this.#commands = {}
    }

    register(commandName: string, command: Command) {
        // Register commands in the Invoker
        this.#commands[commandName] = command
    }

    execute(...args: any[]) {
        // Execute any registered commands
        const commandName = args[0];
        if (commandName in this.#commands) {
          this.#commands[commandName].execute(args[1], args[2]);
        } else {
          console.log(`Command [${commandName}] not recognised!`);
        }
    }
  
    undo(commandName: string) {
        this.#commands[commandName].undo();
    }
}

cutCommand.ts

import { Command } from './command.js';
import { Document } from './document.js';

export class CutCommand implements Command {
  #receiver: Document;
  #startPosition = 0;
  #text = '';

  constructor(receiver: Document) {
    this.#receiver = receiver;
  }

  execute(pos: number, noChars: number) {
    this.#text = this.#receiver.delete(pos, noChars);
    this.#startPosition = pos;
  }

  undo() {
    this.#receiver.insert(this.#startPosition, this.#text);
  }

  isReversible(): boolean {
    return true;
  }
}

copyCommand.ts

import { Command } from './command.js';
import { Document } from './document.js';

// Concrete Command - holds the actual implementation for a specific command
export class CopyCommand implements Command {
  #receiver: Document;
  #text: string = '';

  constructor(receiver: Document) {
    this.#receiver = receiver;
  }

  execute(pos: number, noChars: number) {
      this.#text = this.#receiver.copy(pos, noChars);
  }

  undo() {
    console.log('Copy undo unavailable!');
  }

  isReversible(): boolean {
    return false;
  }
}

pasteCommand.ts

import { Command } from './command.js';
import { Document } from './document.js';

export class PasteCommand implements Command {
  #receiver: Document;
  #startPosition = 0;
  #text = '';

  constructor(receiver: Document) {
    this.#receiver = receiver;
  }

  execute(pos: number, text: string) {
    this.#receiver.insert(pos, text);
    this.#startPosition = pos;
    this.#text = text;
  }

  undo() {
    this.#receiver.delete(this.#startPosition, this.#text.length);
  }

  isReversible(): boolean {
    return false;
  }
}

tsc
node dist/command/app.js

bala ma cara home

delete 3 chars from pos 1 => ala => b ma cara home
insert ala at pos 1 => bala ma cara home
--------
copy 4 chars from pos 2 => la m
--------
insert XZ at pos 1 => bXZala ma cara home
email-newsletter

Dev'Letter

Professional opinion about Web Development Techniques, Tools & Productivity.
Only once a month!

Any suggestion on what to write next time ? Submit now

Opinions

avatar
550
  Subscribe  
Notify of