Design Pattern: Chain of Responsibility Pattern in TypeScript

Chain of Responsibility decouples the request and response processing of an execution. By chaining the steps of processing this pattern separates the steps clearly and also allows the client to decide the sequence of processing.

This article demonstrates Chain of Responsibility pattern implementations in TypeScript. Check the following examples.

Implementation

Follow the steps below for Chain of responsibility pattern implementation-

  • Create abstract class that will work as a common interface for all the steps of the processing.
  • Define a protected property to store the next step information.
  • In the constructor of the abstract class see the next step.
  • Declare a method for the main execution. Make it abstract.
  • Create classes for each step. Extend the abstract class. In the main execution method implementation, at the end (after all processing is done for this step) call the execution method for the next step.

Here is a simple example-

// Chain of responsibility pattern implementation in TypeScript

// Abstract step
abstract class Step {
    protected nextStep: Step | null;

    constructor(nextStep: Step | null) {
        this.nextStep = nextStep;
    }

    abstract execute(): void;
}

// Step 1
class Step1 extends Step {
    constructor(nextStep: Step | null) {
        super(nextStep);
    }

    execute(): void {
        // Perform all required step for this step
        console.log("Execute all operation for Step1 processing")

        // Call the next step execution if the step is defined
        if (this.nextStep) {
            this.nextStep.execute()
        }
    }
}

// Step 2
class Step2 extends Step {
    constructor(nextStep: Step | null) {
        super(nextStep);
    }

    execute(): void {
        // Perform all required step for this step
        console.log("Execute all operation for Step2 processing")

        // Call the next step execution if the step is defined
        if (this.nextStep) {
            this.nextStep.execute()
        }
    }
}

// Step 3
class Step3 extends Step {
    constructor(nextStep: Step | null) {
        super(nextStep);
    }

    execute(): void {
        // Perform all required step for this step
        console.log("Execute all operation for Step3 processing")

        // Call the next step execution if the step is defined
        if (this.nextStep) {
            this.nextStep.execute()
        }
    }
}

// Demo
const steps = new Step1(new Step2(new Step3(null)));

steps.execute();

Output will be as below-

Execute all operation for Step1 processing

Execute all operation for Step2 processing

Execute all operation for Step3 processing

Examples

Let’s check a few examples of the Chain of Responsibility pattern implementation in TypeScript.

Example #1: Caching Data

In this example we have 3 caching types-

  1. CDN – for CSS and JS files
  2. Disk – for large data
  3. Redis – for smaller data

Let’s see how we can implement that using the Chain of Responsibility.

Data Class

  • Create file “data.ts”.
  • Define an enum for the data type named “DATA_TYPE”. The types defined here are “DATA” (for normal data), “JAVASCRIPT”, “CSS”.
  • Define class Data.
  • Declare private properties – type, data, key. 
  • In constructor accept the params and set the property values.
  • Define getter methods for the properties.
// data.ts

export enum DATA_TYPE {
    DATA,
    JAVASCRIPT,
    CSS
};

class Data {
    private type: DATA_TYPE;
    private data: string;
    private key: string;

    constructor(type: DATA_TYPE, key: string, data: string) {
        this.type = type;
        this.key = key;
        this.data = data;
    }

    getType(): DATA_TYPE {
        return this.type;
    }

    getKey(): string {
        return this.key;
    }

    getData(): string {
        return this.data;
    }
}

export default Data;

Cache Handler

  • Create file “cache-handler.ts”.
  • Define abstract class “CacheHandler”.
  • Define property “nextCacheHandler” of type “CacheHandler”, this will store the next processing step information.
  • Declare an abstract method named “handleRequest”.
// cache-handler.ts

import Data from "./data";

abstract class CacheHandler {
    public nextCacheHandler: CacheHandler | null;

    constructor(nextCacheHandler: CacheHandler | null) {
        this.nextCacheHandler = nextCacheHandler;
    }

    abstract handleRequest(data: Data): void;
}

export default CacheHandler;

CDN Cache Handler

  • Create file “cdn-cache-handler.ts”.
  • Create class “CdnCacheHandler”.
  • Extend “CacheHandler” abstract class.
  • Define the constructor and call the parent constructor.
  • In the “handleRequest” method check if data is of type “CSS” or “JAVASCRIPT”. If the type matches then save data in CDN.
  • If the data is of some different type, then call the “handleRequest” method of the “nextCacheHandler”.
// cdn-cache-handler.ts

import CacheHandler from "./cache-handler";
import Data, { DATA_TYPE } from "./data";

class CdnCacheHandler extends CacheHandler {
    constructor(nextCacheHandler: CacheHandler | null) {
        super(nextCacheHandler);
    }

    public handleRequest(data: Data): void {
        if (data.getType() == DATA_TYPE.CSS || data.getType() == DATA_TYPE.JAVASCRIPT) {
            console.log("Caching file \'" + data.getKey() + "\' in CDN");
        }
        else if (this.nextCacheHandler != null) {
            this.nextCacheHandler.handleRequest(data);
        }
    }
}

export default CdnCacheHandler;

Redis Cache Handler

  • Create file “redis-cache-handler.ts”.
  • Create class “RedisCacheHandler”.
  • Extend the “CacheHandler” abstract class.
  • Define the constructor and call the parent constructor.
  • In the “handleRequest” method check if the data is of type “DATA” and the length is 1024 or less, if the type matches then save it in Redis.
  • If the data is of some different type, then call the “handleRequest” method of the “nextCacheHandler”.
// redis-cache-handler.ts

import CacheHandler from "./cache-handler";
import Data, { DATA_TYPE } from "./data";

class RedisCacheHandler extends CacheHandler {
    constructor(nextCacheHandler: CacheHandler | null) {
        super(nextCacheHandler);
    }

    public handleRequest(data: Data): void {
        if (data.getType() == DATA_TYPE.DATA && data.getData().length <= 1024) {
            console.log("Caching data \'" + data.getKey() + "\' in Redis");
        }
        else if (this.nextCacheHandler != null) {
            this.nextCacheHandler.handleRequest(data);
        }
    }
}

export default RedisCacheHandler;

Disk Cache Handler

  • Create file “disk-cache-handler.ts”.
  • Create class “DiskCacheHandler” and extend “CacheHandler”.
  • Define the constructor and call the parent constructor.
  • In the “handleRequest” method check if the data is of type “DATA” and the length is greater than 1024, if the type matches then save it in Disk.
  • If the data is of some different type, then call the “handleRequest” method of the “nextCacheHandler”.
// disk-cache-handler.ts

import CacheHandler from "./cache-handler";
import Data, { DATA_TYPE } from "./data";

class DiskCacheHandler extends CacheHandler {
    constructor(nextCacheHandler: CacheHandler | null) {
        super(nextCacheHandler);
    }

    public handleRequest(data: Data): void {
        if (data.getType() == DATA_TYPE.DATA && data.getData().length > 1024) {
            console.log("Caching data \'" + data.getKey() + "\' in Disk");
        }
        else if (this.nextCacheHandler != null) {
            this.nextCacheHandler.handleRequest(data);
        }
    }
}

export default DiskCacheHandler;

Demo

Chain the cache method calls and create a cache handler from that. Call the “handleReqeust” method of the handler for processing, and pass data as per requirement.

// demo.ts

import CdnCacheHandler from "./cdn-cache-handler";
import Data, { DATA_TYPE } from "./data";
import DiskCacheHandler from "./disk-cache-handler";
import RedisCacheHandler from "./redis-cache-handler";

const cacheHandler = new DiskCacheHandler(new RedisCacheHandler(new CdnCacheHandler(null)));

const data1 = new Data(DATA_TYPE.DATA, "key1", "ABC320489un3429rn29urn29r82n9jfdn2");
cacheHandler.handleRequest(data1);

const data2 = new Data(DATA_TYPE.CSS, "key2", ".some-class{border: 1px solid red; margin: 10px}");
cacheHandler.handleRequest(data2);

Output

Output of the above demo code will be-

Caching data 'key1' in Redis

Caching file 'key2' in CDN

Example #2: Interview

For this example, let’s examine the chain of responsibility implementation for an interview. We are considering the following steps-

  1. Phone Interview
  2. Technical Interview
  3. HR Interview
  4. Interview with the CEO

Interview Interface

  • Create file “interview.ts”.
  • Define abstract class “Interview”.
  • Declare a protected property named “nextInterview” of type “Interview”.
  • In the constructor accept and set the value of “nextInterview”.
  • Declare abstract method “execute”.
// interview.ts

abstract class Interview {
    protected nextInterview: Interview | null;

    constructor(nextInterview: Interview | null) {
        this.nextInterview = nextInterview;
    }

    abstract execute(): void;
}

export default Interview;

Phone Interview

  • Create file “phone-interview.ts”.
  • Define class “PhoneInterview”.
  • In the constructor call the parent constructor and set the next step.
  • In the execute method implementation, write all commands for the phone interview. Call the “execute” method of the “nextInterview” at the end.
// phone-interview.ts

import Interview from "./interview";

class PhoneInterview extends Interview {
    constructor(nextInterview: Interview | null) {
        super(nextInterview);
    }

    execute(): void {
        // Ask all questions for phone interview
        // Perform any other action required for phone interview
        console.log("Ask phone interview questions")

        // Execute the next interview set while creating new struct
        if (this.nextInterview) {
            this.nextInterview.execute();
        }
    }

}

export default PhoneInterview;

Technical Interview

  • Create file “technical-interview.ts”.
  • Define class “TechniaclInterview”.
  • In the constructor call the parent constructor and set the next step.
  • In the execute method implementation, write all commands for the technical interview. Call the “execute” method of the “nextInterview” at the end.
// technical-interview.ts

import Interview from "./interview";

class TechnicalInterview extends Interview {
    constructor(nextInterview: Interview | null) {
        super(nextInterview);
    }

    execute(): void {
        // Ask all questions for technical interview
        // Perform any other action required for technical interview
        console.log("Ask technical interview questions")

        // Execute the next interview set while creating new struct
        if (this.nextInterview) {
            this.nextInterview.execute();
        }
    }

}

export default TechnicalInterview;

HR Interview

  • Create file “hr-interview.ts”.
  • Define class “HrInterview”.
  • In the constructor call the parent constructor and set the next step.
  • In the execute method implementation, write all commands for the HR interview. Call the “execute” method of the “nextInterview” at the end.
// hr-interview.ts

import Interview from "./interview";

class HrInterview extends Interview {
    constructor(nextInterview: Interview | null) {
        super(nextInterview);
    }

    execute(): void {
        // Ask all questions for hr interview
        // Perform any other action required for hr interview
        console.log("Ask hr interview questions")

        // Execute the next interview set while creating new struct
        if (this.nextInterview) {
            this.nextInterview.execute();
        }
    }

}

export default HrInterview;

CEO Interview

  • Create file “ceo-interview.ts”.
  • Define class “CeoInterview”. Call the parent constructor and set the next step in the class constructor.
  • In the execute method implementation, write all commands for the CEO interview. Call the “execute” method of the “nextInterview” at the end.
// ceo-interview.ts

import Interview from "./interview";

class CeoInterview extends Interview {
    constructor(nextInterview: Interview | null) {
        super(nextInterview);
    }

    execute(): void {
        // Ask all questions for ceo interview
        // Perform any other action required for ceo interview
        console.log("Ask ceo interview questions")

        // Execute the next interview set while creating new struct
        if (this.nextInterview) {
            this.nextInterview.execute();
        }
    }

}

export default CeoInterview;

Demo

For the usage, chain the interview step objects by passing one step object to another. The sequence of the chaining will decide the sequence of the processing of the step.

// demo.ts

import CeoInterview from "./ceo-interview";
import HrInterview from "./hr-interview";
import PhoneInterview from "./phone-interview";
import TechnicalInterview from "./technical-interview";

const interviews = new PhoneInterview(new TechnicalInterview(new HrInterview(new CeoInterview(null))));

interviews.execute();

Output

Output will be as below-

Ask phone interview questions

Ask technical interview questions

Ask hr interview questions

Ask ceo interview questions

Source Code

Use the following link to get the source code:

Other Code Implementations

Use the following links to check the Chain of Responsibility pattern implementation in other programming languages.

Leave a Comment


The reCAPTCHA verification period has expired. Please reload the page.