Commissioned for this museum · after Gamma, Helm, Johnson & Vlissides, 1994
Chain of Responsibility
TypeScript·1994·34 lines·1083 bytes
/**
* Chain of Responsibility — each handler decides whether to answer, or to
* pass the request along and do something with what comes back.
*/
type Request = { path: string; user?: string };
type Next = () => string;
type Middleware = (request: Request, next: Next) => string;
const logging: Middleware = (request, next) => {
console.log("-> " + request.path);
return next();
};
const auth: Middleware = (request, next) =>
request.user ? next() : "401 unauthorised";
const route: Middleware = (request) => "200 " + request.path;
function chain(middlewares: Middleware[]): (request: Request) => string {
return (request) => {
// Each step is handed the next one as a function it may or may not
// call. Declining to call it ends the chain there.
const step = (i: number): string =>
i < middlewares.length
? middlewares[i](request, () => step(i + 1))
: "404 not found";
return step(0);
};
}
const app = chain([logging, auth, route]);
console.log(app({ path: "/hall/composition", user: "visitor" }));
console.log(app({ path: "/hall/composition" }));Curator’s note
You have used this today. It is the middleware stack in every web framework written in the last twenty years — Express, Koa, Rack, Django, the routing layer of whatever is serving this page — and it is one of the few patterns from 1994 that arrived at ubiquity under its own name rather than dissolving into a language feature.
The book drew it as a linked list of handlers, each holding a reference to its successor, each deciding whether to handle a request or forward it. The version that won is subtly more powerful: because next is a function the handler calls rather than a pointer it delegates to, a handler runs code *before and after* the rest of the chain. That is how timing, logging, transactions and error boundaries are written. The linked list can only decline; the closure can wrap.
The cost is that the control flow becomes invisible. Look at auth: whether anything happens after it depends on a value it received, and nothing in the file tells you what next leads to — that was decided in the array passed to chain, possibly in another module, possibly at runtime. Every framework built on this has the same two bug reports for the same reason: somebody forgot to call next and the request hangs or silently 404s, or somebody called it twice and the rest of the chain ran twice.
Notice also that the chain has no idea whether anyone will handle the request. step bottoms out in a 404, and that fallback is not decoration — a chain where nothing is guaranteed to answer needs an answer for when nothing does.