Node.jsのProxyでdirty checkとmethod missingを実現してみる

LINEで送る
Pocket

過去にphpのマジックメソッドを使ってRailsのfind_all_by_*メソッドを実装してみる | WEB EGGという記事を書いたのですが、Node.jsでもProxyの登場により、似たようなことができるのでは?と思ったので試してみました。

今回の題材は、同じくRailsのActiveRecordから、ActiveModel::Dirtyモジュールです。

こんな感じに変更を検知するためのマジックメソッド、ユーティリティが加わるモジュールだそうです。

昔であればBackbone.jsのモデルが似たような仕組みを提供していました。
ですが、あれば独自のセッタを提供しており、それを利用しているから変更が検知できるという仕組みです。 いわば白魔術です

今回は、 独自のセッタ を提供せず、普通にオブジェクト操作しているだけで変更検知ができちゃう機能の実装を目指します。
白魔術に対して言うなれば、黒魔術です。

ちなみに使用しているNode.jsのバージョンはv6.1.0です。

簡単な設計

モジュールは高階関数として作成し、使用する際はdecoratorとして利用できるようにします。
なので継承関係によらず、任意のクラスに対して適応可能です。
ざっくりしたイメージとしてはPHPでいうところのtrait、Rubyでいうところのinclude相当だと思ってもらえればいいと思います

コンストラクタの形式は問いません。thisに何かセットされていればそれを利用できるようにします。 こんな感じで利用できるDirtyCheckable関数を実装していきます

完成済みのコードはgistに上げてあります。

decoratorの挙動

decoratorはReactでHigh Order Componentsなんて言われて流行ってますが、要は昔からある関数型言語のアプローチのひとつ、高階関数です。

【エンジニア初心者向け】高階関数入門(Javascript) – Qiita

一応、先程のコードをjsのコードにするとこんな感じになります

DirtyCheckableの要件は、クラスを受け取りクラスを返す関数になります。 実装イメージとしては、以下のような感じになります。

継承の逆?と言えば伝わるんでしょうか。
渡されたクラスを親クラスにとる無名クラスを作成して返す感じです。

Proxyの挙動

Proxy自体の説明はMDNを見ればだいたいわかると思います。

“`js const obj = new Proxy({}, { set (instance, prop, value) { console.log(${prop}=${JSON.stringify(value)}) instance[prop] = value } })

obj.hoge = 1 “`

実行するとhoge=1と出力されたと思います。
こんな感じで、ただのオブジェクト操作をフックすることが可能になります。

setの中身を実装することで、dirty checkを実装できます。 同様にgetの中身を実装することで、method missingも実装できます。

クラスをProxyする

  • コンストラクタの形式を制限しないように可変長で受け取って可変長で渡す
  • Proxyのインスタンスを返す

コードは以下のような感じです。

上記のコードをベースに実装を続けます。

Node.jsでdirty check

早速実装します。DirtyCheckerはただのユーティリティなので実装はgistを御覧ください。
先述のコードのobserverのsetを実装します。
instanceは呼び出し元のインスタンスを指します。

なので、this.dirties = instance.dirtiesです。
ということで、DirtyChecker#setをコールするだけです。

instance[prop] = valueを忘れるとインスタンスに値が反映されないのでご注意下さい。

これで変更検知の仕組みは完成したので、後はユーティリティを実装します。

試してみます。

実行結果は

いい感じです。
各プロパティごとの*Was, *Changed, *Changeメソッドはmethod missingを利用して実装します。

Node.jsでmethod missing

今度はobserverのgetを実装していきます

  • もし定義済のプロパティならそれを返す
  • 未定義の値が来たらmethod missingのフォールバック処理へ以降
  • 余計なサフィックスを除去し、本来のプロパティ名をフォールバック処理へ渡す

という感じです。

完成です。ここまでのコードを纏めて実行してみると、

いい感じです。これでdirty checkとmethod missingの実装が完了しました。

パフォーマンス測定

最後に気になるパフォーマンスですが、こんなコードで比較してみます

100,000回同じ処理をしてみてどれくらいコスト差があるか比べてみました。

メソッド ProfileWithDirty Profile
new 244 ms 6 ms
set 29 ms 5 ms
get 35 ms 1 ms
methodCall 63 ms 3 ms

newが激遅いです。
他も優位な差が出ているものの、10万回で数十ms程度の差なら無視しても良いレベルではないでしょうか。

まとめ

かなり愚直な方法で実装しているので、もっと早い実装がたくさんあると思います。 使いみちが色々あって面白いので、ぜひProxy利用してみて下さい。 ただしよほど丁寧に書かないと黒魔術化は必至なので、用法用量をお守りのうえお楽しみ下さい。

LINEで送る
Pocket