こんにちは、現象数理学科4年の@RyosukeClaです。
今日も明治大学 advent calendar 四日目やっていきます。
はじめに
Reduxのmiddleware実装をみて試して、これは一般的なmiddleware patternとして使えることを知りました。それが本記事の内容です。本記事の目的はタイトルにある通り、jsでmiddleware patternを理解し実装することです。Middlewareを活用し、柔軟なシステムを構築できるといいなという思いです。
Middleware Pattern
Middlewareとは、2つのシステムの中間に入るシステムのことです。たとえば、expressのmiddlewareやreduxのmiddlewareなどにあたります。Expressにおけるmiddlewareとは以下のような図をイメージしてください。図のように、HandlerによってRequestが処理されるまでの間にmiddlewareを通します。
Middlewareを上手く扱えれば、処理の共通化や拡張を容易にできるようになります。たとえば、Expressを使った現実的なウェブサーバーは下図のような構造でしょう。ときには、ウェブサーバーに新しいAPIを生やしたり、Proxyを入れたりすることもあるでしょう。
現実的なウェブ開発では様々な機能が必要になります。時を隔てて、システムは多機能化、肥大化していきます。そうとなると、メンテナビリティやスケーラビリティの高い、複雑化する機能要件に耐えうる柔軟なシステム構築が必要です。Middleware patternはそのようなシステムを構築する力になるのです。
Redux middleware
Middleware Patternを実装するにあたって、僕はRedux middlewareを参考にしました。ReduxとはReactとセットで語られることが多い状態管理ライブラリです。そんなReduxにはmiddlewareという機能があります。Reduxのdispatcherを拡張する機能になります。
基本的には、Reduxは次の図のようにdispatchを呼びます。
middlewareを用いると次の図のようにdipatchを呼ぶことになります。dispatchにmiddleware patternを適応した形です。これからは、手順を踏んでmiddleware patternを実装していきます。
example
まずは、Reduxにおけるmiddlewareがどんな感じかを載せます。本記事が目指すmiddleware patternのイメージと思ってください。これは、Reduxにlogger middlewareを追加する例です。
loggerの内部では、actionのログを取り、次の処理を実行するためにnext(action)を呼びます。
Actionをdispatchすると、logger、middlewareA、middlewareBと順に実行されて行きます。
const logger = store => next => action => {
console.log('dispatch', action)
return next(action)
}
const middlewares = [
logger,
middlewareA,
middlewareB
]
const store = createStore(
App,
applyMiddleware(...middlewares)
)
Implementation
Middleware patternに必要な機能は、複数のmiddlewareを1つに押し込めることと、middleware内部で次のmiddlewareの処理に移すことです。まずは、1つに押し込めるための関数合成について説明します。次に、next関数によって直列的にmiddlewareを呼ぶ機能を説明します。その他、middleware内部で扱うAPIのインジェクションの説明もします。
Function composition
関数型言語には関数合成(function composition)という機能があります。数学における関数合成と同じです。具体的には、
$$(f \circ g)(x) = f(g(x))$$
となります。
jsで関数合成を実装すると次となります。
export default function compose(...funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
関数合成は非常に便利です。複数の関数を一つの関数にできるからです。Middlewareもcomposeを上手く使うことで簡単に実装することができます。
たとえば、3倍して自乗して1を足すコードを書いてみます。単純に実装すれば、
const func = x => ((3 * x) * (3 * x)) + 1
ですが、関数合成を用いると次のようになります。
const funcs = [
(x) => x + 1,
(x) => x * x,
(x) => 3 * x,
]
const composed = compose(...funcs)
next(action)
先ほどの例では気になる点が一つあります。それは、関数の実行順番が下からになることです。つまり、定義順番は以下のようになってますが、実行される順番は逆です。
順番 | 定義 | 処理 |
---|---|---|
1 | x + 1 | 3 * x |
2 | x * x | x * x |
3 | 3 * x | x + 1 |
Middlewareを繋げるとなると、直感に反します。また、実装者の意図するポイントで次のmiddlewareを実行したいこともあるでしょう。そのようなニーズを満たすには俗に言うnext
があると便利です。
次の例は、1、2、3を順にログするmiddlewareです。
const middlewares = [
next => action => {
console.log(1)
return next(action)
},
next => action => {
console.log(2)
return next(action)
},
next => action => {
console.log(3)
return next(action)
},
]
const composed = compose(...middlewares)
const dispatch = composed(action => action)
dispatch({})
MiddlewareAPI
先ほどの例でmiddlewareの実装は終わりと言ってもいいでしょう。しかし、middleware内で使用可能なAPIを提供したいこともあるでしょう。redux middlwareにもexpress middlewareにもAPIが提供されていますね。次のコードのように、reduxであればstore
、expressであればres
にあたります。
// redux middleware
const logger = store => next => action => {
console.log(action)
return next(action)
}
// express middleware
const logger = (req, res, next) => {
console.log('logged')
next()
}
redux middleware内では、applyMiddleware という関数が、APIをmiddlewareに渡しているようです。redux middlewareを習い、middlewareにAPIを渡します。
const dispatchFactory = (middlewareApi, middlewares, dispatch) => {
const chain = middlewares.map((middleware => middleware(middlewareApi)))
return compose(...chain)(dispatch)
}
ものすごく単純ですね。この場合、middlewareは以下のように実装します。先ほどの、1、2、3を順にログするコードです。console.log
ではなく、api.log
を用いていることに注目してください。
const middlewares = [
api => next => action => {
api.log(1)
return next(action)
},
api => next => action => {
api.log(2)
return next(action)
},
next => action => {
api.log(3)
return next(action)
},
]
const api = {
log: console.log
}
const dispatch = dispatchFactory(api, middlewares, () => {})
dispatch({})
reduxのほうの実装は次のようになっています。もちろん、実際のコードとは違い、本記事に向けて改変しています。
const api = {
getState: store.getState,
dispatch: () => {}
}
const dispatch = dispatchFactory(api, middlewares, store.dispatch)
api.dispatch = dispatch
dispatch({})
Overall
これまでのをまとめると完成です!次のようなコードになります。これで色々な部分をmiddleware化することができるようになりました。ものすごく単純ですね。
function compose(...funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
export function dispatchFactory(middlewareApi, middlewares, dispatch) {
const chain = middlewares.map((middleware => middleware(middlewareApi)))
return compose(...chain)(dispatch)
}
使い方の例です。
import { dispatchFactory } from './middleware'
const middlewareAPI = {
logger: {
info (...messages) {
console.log('[info]', ...messages)
}
}
}
const middlewares = [
api => next => action => {
api.logger.info('1', action)
action.n++
return next(action)
},
api => next => action => {
api.logger.info('2', action)
action.n++
return next(action)
},
api => next => action => {
api.logger.info('3', action)
action.n++
return next(action)
}
]
const dispatch = dispatchFactory(middlewareAPI, middlewares, () => { console.log(‘Hello’) })
dispatch({ n: 0 })
// Out:
// [info] 1 { n: 0 }
// [info] 2 { n: 1 }
// [info] 3 { n: 2 }
// Hello
また、middlewareを変形させて、express middleware likeにすることもできます。便利ですね。
let middlewares = [
(action, api, next) => {...}
]
middlewares.map(middleware => {
return api => next => action => {
return middleware(action, api, next)
}
})
最後に
最後まで読んでくれてありがとう。Middleware Patternはどうでしたか。自身の文章力の低さを噛み締めつつ、開発の助けになればなと思います。
おつ!