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 pattern is applicable when:
- the request is handled by multiple objects, determined at runtime
- you dont want to explicitly specify handlers in code
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.
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
Opinions