課題
knockout.jsのコンポーネント間のやり取りで、互いのViewModelを参照していたりと密結合になっていてリファクタリングがしづらい状況になっていた。
たとえば、コンポーネントAの処理で、コンボーネントBの値が変わってほしい場合に、コンポーネントAのViewModelにコンポーネントBのViewModelのインスタンスを取得するメソッドを作ったりしていた。
こんな感じ。
class ViewModelA
clickButton: ->
@getViewModelB().name("new_value")
getViewModelB: ->
window.view_models.view_model_b
class ViewModelB
constructor: ->
@name = ko.observable()
$ ->
window.view_models = {
view_model_a: new ViewModelA(),
view_model_b: new ViewModelB()
}
こうなると、Bのインスタンスがある前提で処理を書かなければならず、Bがない画面ときはエラーになるのでBの存在チェックを行う必要性が出たりなど、だんだん複雑化していった。
class ViewModelA
clickButton: ->
vm = @getViewModelB()
if vm? # Bがない場合を考慮
vm.name("new_value")
getViewModelB: ->
window.view_models.view_model_b
ViewModel同士が少なければこれでも耐えられたのだけれど、だんだん苦しくなってきた。
調査
AndroidのIntentのように、とりあえず投げるからそれを拾って勝手に処理してくれる仕組みがほしいなぁと考えるようになった。とはいえ、こういう課題はよくあることだろうと思ったので、ググってみると、ko.subscribable
を使えという記事があった。
毎度毎度忘れてしまうのだが、subscribeは”購読”である。
subscribe
メソッドで購読する。通知が届いた際の処理を関数で記述。
notifySubscribers
メソッドで購読者に通知を送る。
シンプルな例だと、こういう感じ。
# 購読通知管理用のインスタンスを生成
shouter = new ko.subscribable()
class ViewModelA
clickButton: ->
# 通知ID"subscriber_id"に対して"new_value"を送る通知
shouter.notifySubscribers("new_value", "subscriber_id")
class ViewModelB
constructor: ->
@name = ko.observable()
# 通知ID"subscriber_id"を購読。届いた値をnameに入れる
# ViewModelAからの通知では、"new_value"が入る
shouter.subscribe (value) ->
this.name(value)
, this, "subscriber_id"
素晴らしい点として、AはBに依存しなくなった。Aはただ通知を送っただけ。
その先に、Bが処理を実行するかどうはAは知る由もない。
Bは、通知が届いたらnameを更新するというようにしているのみ。
それがAからの通知であるとかも意識しなくていい。ただ、通知IDが一致していればいいのである。
Pub/Sub
そして、こういうモデルのことをPub/Subメッセージングモデルということを知った。
Pub=Publish(発行)
Sub=Subscribe(購読)
まさに、Aが発行、Bが購読をしている。
ko.subscribableの課題
ko.subscribable
のインスタンスはいろいろなところで使おうと思うとグローバル変数として管理しなくてはいけなくなるので、あまり使いたくない…。また、ちょっとコードが読みにくくなるなぁという印象があった。
ライブラリknockout-postboxを使うと、そのあたりが綺麗に書けるという話だったので使ってみた。
knockout-postboxを使ってみる
導入
knockout-postboxのreleasesから、バージョン0.5.2をダウンロードして利用した。
利用
knockout-postboxを使うと、notifySubscribers
がpublishになり、わかりやすい。
class ViewModelA
clickButton: ->
# 通知ID"subscriber_id"に対して"new_value"を送る通知
ko.postbox.publish("subscriber_id", "new_value")
class ViewModelB
constructor: ->
@name = ko.observable()
# 通知ID"subscriber_id"を購読。届いた値をnameに入れる
# ViewModelAからの通知では、"new_value"が入る
self = this
ko.postbox.subscribe "subscriber_id", (value) ->
self.name(value)
また、observableな変数自体に、Pub/Subを設定することもできる。
class ViewModelA
clickButton: ->
# 通知ID"change_name"に対して"new_value"を送る通知
ko.postbox.publish("change_name", "new_value")
class ViewModelB
constructor: ->
# 通知ID"change_name"を購読する
@name = ko.observable().subscribeTo("change_name")
# @ageの値が変更されたら、通知ID"change_age"に通知する
@age = ko.observable().publishOn("change_age")
# 通知ID"sync_email"を購読する他のobservableと同期を取る
@email = ko.observable().syncWith("sync_email")
まとめ
Pub/Subを採用することで、コンポーネント同士が疎結合になり、かつ、コンポーネントを管理するための親ViewModelみたいなものも必要なくなり、コードが非常にすっきりする。変更にも強くなるので、今のところ悪い点が見当たらない。
コンポーネント連携は全てこの方式に変えていこうと思う。