大きなPubSub、小さなPubSub。
JavaScriptにおいてもそのほかの言語においても疎結合というのは結構大きなテーマの一つだと思います。
そんな疎結合を促す実装としてPubSubがにわかに脚光をあびてますね。
にわかというより定期的に盛り上がってる気がしますが。
僕はあまりデザインパターン厨ではないのであまり語れるようなことはないのですが疎結合なコードというのはコードの再利用性が高まり幸せ度がかなり高いものだと思います。
そんなPubSubを強力に後押しするライブラリは世の中星の数ほどあるわけですが、ほんの少しだけ融通が利かないなーって思うこともあって半年くらい前に自作しました。 そう、95%くらい車輪の再発明です。
その名もPubsubJS(https://github.com/nazomikan/PubsubJS)。 どこかですごく似た名前を聞いたことはあるわけですがまぁそのへんはおいといて何がいっぱいあるPubSubライブラリで不満だったのか、そしてそれをどんなアプローチで解消したのか。
たくさんのコードを書いてて非常に気になったのはSubscriberがPublisherになり得るという事実です。
少し迂回しますが、世の中ではjQueryのおかげかCommonJSの仕様のおかげか、はたまたあの翻訳記事のおかげか、Promiseがはやりましたね。
Promiseは何を解決したのか。
Promiseがもたらしたパラダイムシフトは非同期な(イベント)処理をあたかも同期的に書いてるかのように書けるというところだと思います。
非同期な処理をあたかも同期的に書いてるかのように書けることがなぜ幸せかというとその処理のトリガーを引いたスコープで続きの処理が書けるからです。 (ほかにもいろいろあるとは思いますが。)
例えばよくある商品検索サイトでお気に入りに何かを追加したりするような処理について考えてみましょう。
すごい適当にかいてますが☆がお気に入り追加ボタンだったとしましょう。
ajax成功時に☆を★に変えたりクラスを少し変えたりしてますね。
こういうのってthenコールバックが処理トリガーをひいたそのスコープでかけるからevt.currentTarget(クリックされたお気に入りアイコンの要素)とかにアクセスできて実現できるんですよね。
これがもし`Favorite.prototype.add`の$.ajaxのプロパティの中でsuccessプロパティを用意してそこにコールバックをかかなければならなかったとしたらevent.currentTargetにアクセスする方法はありませんね。
※こういう場合
Favorite.prototype.add = function (itemId) { return $.ajax({ type: "POST", url: "/echo/json/", data: { json: itemId, delay: 1 }, success: function () { // ここじゃ呼出し元のスコープのevent.currentTargetにアクセスできない } }); };
かといってこのスコープでevent.currentTargetにアクセスするためにその要素を引き回すというのも、かなり依存性を強めるナンセンスな手段でしょう。
とまぁこんな悩みを解決してくれるわけです。
これは非常に有益で常にこの恩恵をうけたいものです。
では話を戻してPubSubへ。
今のようなアプリケーションをPubSubを使って実装するとこんな感じになるでしょう。
お気に入りアイコンがクリックされたことをpublishして、お気に入りがそれをsubscribeしてるので通知を受けて動くという感じです。
お互いがお互いのことを意識していない非常に疎結合な実装ですがさきほどPromiseが解決していた点がまた問題としてあがってきます。
そう、Subscriber側では当然元のスコープの変数にアクセスできません。
あらかじめいっておくとこういう問題はそもそもPub/Subモデルが解決しようとしている問題ではないので、これが単純なPub/Subで解決できないのは非常に当たり前のことです。
Promiseは元のスコープに対してpromiseオブジェクトを返却することでその問題を解決してましたが、これは呼出し元と呼び出される側が返り値をやりとりするというとても疎結合とは呼べない処理の結びつきを生んでしまっています。
Pub/Subはこの結びつきをなくすかわりにPromiseのような柔軟な非同期処理の記述方法を失っています。
これはトレードオフ以外の何者でもなくどちらを責めるわけにもいかないでしょう。
ただ僕にとって、この二つを同時に必要と思うことがあるのもまた一つの真実だったのです。
この問題を整理して考えると、本質的には”SubscriberもPublisherに転じることがある”という風にまとめられると思います。
つまりどういうことかというと先ほどのアプリケーションでいえば、PublisherはItemList、SubscriberはFavoriteという関係でしたがお気に入り追加処理を終えたときにSubscriberであるはずのFavoriteがこんどはPublisherだったItemListに対して完了したことを通知したいという事情が発生してるわけです。
今回作ったPubsubJSはこの辺をうまく解決してくれるあれです。
で、どういう風に解決したかというと
大きなPubSub、小さなPubSubを作ることで解決しました。
大きなPubSub?
小さなPubSub?
よくわかりませんね。
pubsubというはイベントの中央管理者です。
いくつものオブジェクト間でイベントをやりとりするためにそれぞれのイベントを一重に管理している管理者です。
それとは別に小さなpubsub(Pubsub#Context)を作りました。
これはコードをみる方が早いでしょう。
先ほどのアプリケーションをPubsubJSの小さなpubsubで作るとこんな感じで表現できます。
注目すべき点はこことそこです。
ここ
ItemList.prototype.addFavorite = function (evt) { var target = $(evt.currentTarget), itemId = target.data('itemId'), localContext = pubsub.Context.create(); localContext.subscribe('favorite.added', function () { alert('favorite added'); target.text('★'); target.removeClass('selectable'); }); pubsub.publish('favorite.icon.clicked', localContext, itemId); };
そこ
Favorite.prototype.add = function (context, itemId) { $.ajax({ type: "POST", url: "/echo/json/", data: { json: itemId, delay: 1 } }).then(function () { context.publish('favorite.added'); }); };
問題が解決しましたね。
全体的なイベントを管理するpubsubとPublisher-Subscriber間での小さなイベント管理者pubsub#Contextという二人の管理者をつくることで元のスコープ内でSubscriberがPublisherに転じたときに通知されるsubscriberを書くことができて問題が解決します。
この小さな管理者もまた、pubsubの実装に従っているため、このイベントが引かれることを絶対としていない疎結合な関係にあります。
とまぁこんな感じで僕の場合は幸せな日々がおくれるようになりました。
以上ステマでした。
蛇足(補足)。
知人からコールバックじゃあかんのん?っていわれたので回答。
SubscriberがPublisherに転じたときに起こすイベントの数だけコールバック渡すのあまり現実的じゃなかったりsubscribeしてる全てのメソッドにも自分の処理と関係ないコールバック渡されるの嫌じゃないですか。
たとえばコールバックだとこんなの
function p1() { var handler1 = function..., handler2 = function ...; pubsub.publish('p1.executed', handler1, handler2); } pubsub.subscribe('p1.executed', function a(handler1, handler2) { //...こっちでは特定の何かが終わった時にhandler1を実行する }); pubsub.subscribe('p1.executed', function b(handler1, handler2) { //...こっちでは特定の何かが終わった時にhandler2を実行する });
こういうのってaが自分と関係ないhandlerを、bが自分と関係ないhandler2を渡されるの嫌じゃないですか。
さらにそれぞれ別個に渡せるようにするってなるとそれって依存性すごいじゃないですか
というので小さなイベントの管理者を渡すのがよいなとおもって実装した所存です。
あと、大きなpubsubに名前ルールきめてlocalContext作ってるところもpubsubにpublishしたらええやんともいわれたのでその辺もフォロー
いいたいことは多分こんな感じだと思います。
function p1() { pubsub.subscribe('local.a.executed', function () { //... }); pubsub.publish('p1.executed', handler1, handler2); } pubsub.subscribe('p1.executed', function a() { // something to do pubsub.publish('local.a.executed'); });
こうしちゃうとリスナがずっとpubsubに残っちゃってリソース勿体ない(pubsub#Contextで小さなpubsubを作る場合、その小さなpubsubがGCに回収されるときリスナも解放される)であったり、p1が実行されるたびにlocal.a.executedのsubscriberが一個ずつふえていってバグりますね。
結構致命的だと思います。
そもそもの話、大きなサイト作ってりゃ名前ルールで縛るのも限界がきてそんな細かいイベントまで管理していると実装が破綻するっていう話もあると思います。
なので私は小さry