Help us understand the problem. What is going on with this article?

jsでmiddleware patternを実装する

More than 1 year has passed since last update.

こんにちは、現象数理学科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を通します。
スクリーンショット 2018-12-04 4.33.07.png

Middlewareを上手く扱えれば、処理の共通化や拡張を容易にできるようになります。たとえば、Expressを使った現実的なウェブサーバーは下図のような構造でしょう。ときには、ウェブサーバーに新しいAPIを生やしたり、Proxyを入れたりすることもあるでしょう。

現実的なウェブ開発では様々な機能が必要になります。時を隔てて、システムは多機能化、肥大化していきます。そうとなると、メンテナビリティやスケーラビリティの高い、複雑化する機能要件に耐えうる柔軟なシステム構築が必要です。Middleware patternはそのようなシステムを構築する力になるのです。

スクリーンショット 2018-12-04 4.56.46.png

Redux middleware

Middleware Patternを実装するにあたって、僕はRedux middlewareを参考にしました。ReduxとはReactとセットで語られることが多い状態管理ライブラリです。そんなReduxにはmiddlewareという機能があります。Reduxのdispatcherを拡張する機能になります。

基本的には、Reduxは次の図のようにdispatchを呼びます。
スクリーンショット 2018-12-04 7.24.09.png
middlewareを用いると次の図のようにdipatchを呼ぶことになります。dispatchにmiddleware patternを適応した形です。これからは、手順を踏んでmiddleware patternを実装していきます。
スクリーンショット 2018-12-04 7.24.15.png

example

まずは、Reduxにおけるmiddlewareがどんな感じかを載せます。本記事が目指すmiddleware patternのイメージと思ってください。これは、Reduxにlogger middlewareを追加する例です。

loggerの内部では、actionのログを取り、次の処理を実行するためにnext(action)を呼びます。
Actionをdispatchすると、logger、middlewareA、middlewareBと順に実行されて行きます。

redux-example.js
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で関数合成を実装すると次となります。

compose.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です。

oneTwoThree.js
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にあたります。

example.js
// 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を渡します。

dispatchFactory.js
const dispatchFactory = (middlewareApi, middlewares, dispatch) => {
  const chain = middlewares.map((middleware => middleware(middlewareApi)))
  return compose(...chain)(dispatch)
}

ものすごく単純ですね。この場合、middlewareは以下のように実装します。先ほどの、1、2、3を順にログするコードです。console.logではなく、api.logを用いていることに注目してください。

oneTwoThree.js
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のほうの実装は次のようになっています。もちろん、実際のコードとは違い、本記事に向けて改変しています。

reduxDispatch.js
const api = {
  getState: store.getState,
  dispatch: () => {}
}
const dispatch = dispatchFactory(api, middlewares, store.dispatch)
api.dispatch = dispatch
dispatch({})

Overall

これまでのをまとめると完成です!次のようなコードになります。これで色々な部分をmiddleware化することができるようになりました。ものすごく単純ですね。

middleware.ts
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)
}

使い方の例です。

oneTwoThree.js
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にすることもできます。便利ですね。

toExpressLike.js
let middlewares = [
  (action, api, next) => {...}
]
middlewares.map(middleware => {
  return api => next => action => {
    return middleware(action, api, next)
  }
})

最後に

最後まで読んでくれてありがとう。Middleware Patternはどうでしたか。自身の文章力の低さを噛み締めつつ、開発の助けになればなと思います。

おつ!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away