The Pattern
The Chain of Responsibility encapsulates a behaviour into an object usually referred to as a handler. Each handler then stores a reference to the next handler in the chain, and can decide if it should process that request or pass it to the next handler in the chain.
The Chain of Responsibility is essentially an Object Orientated version of if … else if… else if …
There are some considerations around the execution of a chain that are worth remembering.
- A chain can be composed of a single item
- A chain may not result in the handling of a request at all
- Requests may not get to the end of the chain
When to use
This pattern is best used when there are multiple objects that could potentially handle a request, and when that decision is made at runtime.
Example
Let’s consider an automated system for raising incidents in an e-commerce platform. Incidents have two properties; a type and a severity. Both of these things are defined by your business and both will impact what has to be done in response. These may be things like:
- Notify senior leadership
- Update the status page
- Stop users from buying anything (extreme, but possible)
- … many others that are likely to grow in time
In code, this might begin by looking something like this:
type IncidentType = "Security" | "Network" | "Data Breach";
type Severity = "SEV 0" | "SEV 1" | "SEV 2"
const handleIncident = (incidentType: IncidentType, sev: Severity) => {
if (type === "Security") {
if (sev === "SEV 0") {
notifySeniorLeadershipTeam();
notifySecurityEngineers();
} else if (sev === "SEV 1") {
notifySecurityEngineers();
}
notifyOnCallEngineer();
}
else if (type === "Network") {
if (sev === "SEV 0") {
notifyOnCallEngineer();
} else {
waitAndNotify()
}
}
else if (type === "Data Breach") {
notifySeniorLeadershipTeam();
}
}
We’re only dealing with a couple of use cases but already things are getting out of hand. It’s difficult to see when and who are notified, and this problem will only get worse as the number of types or response behaviours increases. Let’s try out our Chain of Responsibility.
Step 1 - Define the handler interface
interface Request {
incidentType: IncidentType;
sev: Severity;
}
class BaseHandler {
next: BaseHandler | undefined;
constructor() {
this.next = undefined;
}
handle(request: Request) {
// reduce boilerplate by handling common behaviour in the parent
if (this.next) {
this.next.handle(request);
}
}
}
Step 2 - Create concrete classes
Each handler makes two decisions:
- if it’ll process the request
- if it’ll pass the request to next handler along the chain
class SecurityHandler extends BaseHandler {
handle(request: Request) {
if (request.incidentType != "Security") {
super.handle(request);
return;
}
if (sev === "SEV 0") {
notifySeniorLeadershipTeam();
notifySecurityEngineers();
} else if (sev === "SEV 1") {
notifySecurityEngineers();
}
notifyOnCallEngineer();
}
}
class NetworkHandler extends BaseHandler {
handle(request: Request) {
if (request.incidentType != "Network") {
super.handle(request);
return;
}
if (sev === "SEV 0") {
notifyOnCallEngineer();
} else {
waitAndNotify()
}
}
}
class DataBreachHandler extends BaseHandler {
handle(request: Request) {
if (request.incidentType != "Data Breach") {
super.handle(request);
return;
}
notifySeniorLeadershipTeam();
}
}
Step 3 - Build the chain
Depending on the problem, clients may build chains dynamically or use prebuilt ones. Our example calls for a prebuilt chain, but if a dynamic one is required then a factory can be used to handle the creation for you.
const buildChain = (handlers: BaseHandler[]) => {
let baseHandler = handlers.shift();
let prev = baseHandler;
while (handlers.length > 0) {
const current = handlers.shift();
prev.next = current;
prev = current;
}
return baseHandler;
}
// The ordering here will be the order in which the handlers are invoked
const incidentHandlers: BaseHandler[] = [
new SecurityHandler(),
new NetworkHandler(),
new DataBreachHandler(),
]
export const incidentChain = buildChain(incidentHandlers);
Step 4 - Replace the code
Now we have our chain, all that remains is to replace our old code with it.
type IncidentType = "Security" | "Network" | "Data Breach";
type Severity = "SEV 0" | "SEV 1" | "SEV 2"
const handleIncident = (incidentType: IncidentType, sev: Severity) => {
incidentChain.handle({ incidentType, sev });
}
The are many benefits to the application of this pattern in this case. We’ve create more cohesive code that adheres to the Single Responsibility and Open / Closed design principles, resulting in code that is much easier to understand and test. \o/