Middleware Pipelines - Chain of Responsibility
24th Jun 2018
Out of curiosity, I've been experimenting with what you can do with a simple and generic middleware builder. I've been surprised at how powerful it can be with such a small amount of code.
This series:
A simple but powerful function
I did a lot of reading, and gained a fair amount of inspiration from researching various implementations of the Chain of Responsibility pattern along with diving into the express.js and redux codebases.
It has essentially boiled down to using reduceRight
on an array of functions. The following code has simplified types to make it easier to read but shows the concept.
const buildMiddleware =<Env, Context>(...middlewares: Array<TMiddleware<Env, Context>>) =>(env: Env) =>(req: Context): Context => {const runFinal = (context: any) => contextconst chain = middlewares.reduceRight((next: any, middleware) => middleware(env, next),runFinal)return chain(req)}
With this simple function I can build a wide range of middleware handlers.
Chain of Responsibility Pipeline
The first one I tried was a simple synchronous, chain of responsibility pipeline. Here's a few examples of middleware which keep adding to a string.
// First middlewareconst sweets = (env, next) => context => {context += ` ate ${env.numberOfSweets()} sweets`return next(context)}// Second middlewareconst enjoyed = (env, next) => context => {if (env.didEnjoy()) {context += ' and enjoyed it.'} else {context += ' and stuck tongue out.'}return next(context)}// We can stop the middleware chain// and return early if neededconst early = (env, next) => context => {if (env.isTakingPart()) {return next(context)} else {return context + ' did not want to take part'}}
To use them you build the pipeline, then you can apply a shared environment, and finally pass a context through it.
// Add the middleware to form the pipelineconst buildSweetsSentenceWith = buildMiddleware(early,sweets,enjoyed)// Apply the environment to the pipelineconst getSweetsSentence = buildSweetsSentenceWith({isTakingPart: () => true,didEnjoy: () => true,numberOfSweets: () => 20})//console.log(getSweetsSentence('Barney'))// => 'Barney ate 20 sweets and enjoyed it.'
I'd expect the environment would tend to be more dynamic to the context, and I'd also expect the context to be an object with most of the data needed for the middleware. This as an experiment shows a lot of potential for such a simple script.
Next article, I look at using Promises
In the next post, I'll look at using the same code to build a Promise based pipeline. With promises we will be able to use the same pattern for asychronous tasks. I will then show how you can use it with Either
and TaskEither
.
You can find the full working code and tests in the exploring-fp-ts github repo
... A more involved example
If you can't wait for more on this, here is another example of a synchronous pipeline which has a bit more going on. It's probably closer to real-world usage.
Here we are running calculations with an object of data as the context. I'm delegating the responsibility of calculating the number of each size bank note for an atm.
import { buildMiddleware, TMiddleware } from './middleware'const env = {getBalance: () => 500}interface IChainOfResponsibility {name: stringones: numbertens?: numbertwenties?: numberhundreds?: numberhasEnoughMoney?: string}const hasEnoughMoney: TMiddleware<typeof env, IChainOfResponsibility> =(env, next) => atm => {const result = {...atm,hasEnoughMoney: atm.ones < env.getBalance() ? 'Yes!' : 'No'}return next(result)}const hundreds: TMiddleware<typeof env, IChainOfResponsibility> =(env, next) => atm => {const ones = atm.ones % 100const result = {...atm,hundreds: (atm.ones - ones) / 100,ones}return next(result)}const twenties: TMiddleware<typeof env, IChainOfResponsibility> =(env, next) => atm => {const ones = atm.ones % 20const result = {...atm,twenties: (atm.ones - ones) / 20,ones}return next(result)}const tens: TMiddleware<typeof env, IChainOfResponsibility> =(env, next) => atm => {const ones = atm.ones % 10const result = {...atm,tens: (atm.ones - ones) / 10,ones}return next(result)}export const handleRequestPipeline =buildMiddleware(hasEnoughMoney, hundreds, twenties, tens)export const chainOfResponsibility = handleRequestPipeline(env)chainOfResponsibility({ name: 'Mary', ones: 1335 })// => result is// {// name: 'Mary',// hasEnoughMoney: 'No',// ones: 5,// tens: 1,// twenties: 1,// hundreds: 13// }
Cover photo by Gerrie van der Walt