発行/購読パターンとはなんぞや。
発行/購読パターン(パブリッシュ/サブスクライブ)とは、発行側(パブリッシャー)と購読側(サブスクライバー)という二つの実体の間に、チャンネル(トピック)と呼ばれる中継層を設け、メッセージ交換はチャンネルを介して行うパターンです。パブサブ(Pubsub)とも呼ばれます。
パブリッシャーは、特定のチャンネルにメッセージを公開(パブリッシュ)し、サブスクライバーは購読(サブスクライブ)しているチャンネルの新規メッセージ公開時にその通知を受け取ります。
発行/購読パターンを用いれば、各実体(メソッドなりオブジェクトなり)を分離し部品化することができます。これによりアーキテクチャが疎結合になり、拡張性・再利用性を高めます。
論よりコード
まずは発行/購読パターンを使わない場合の例を書いてみます。
ユーザーがアクセスすると、サーバーからAjaxでユーザー情報を取得し、プロフィールを表示するページを作ることになりました。
ひとまず、コードを書いてみます。名前空間hogeを定義し、
hoge.profileモジュールに描画処理を、hoge.userモジュールにユーザー情報取得と描画処理実行処理をそれぞれ実装しています。
// 名前空間hogeを定義
var hoge = hoge || {};
/* ユーザーモジュール */
hoge.user = (function($){
var find;
find = function(){
$.ajax({
/* ユーザー情報取得.. */
})
.done(function( user_data ){
// プロフィール描画処理
hoge.profile.draw( user_data );
});
};
return {
find : find
}
}(jQuery));
/* プロフィールモジュール */
hoge.profile = (function($){
var draw;
draw = function( user_data ){
/* プロフィール描画処理.. */
}
return {
draw : draw
}
}(jQuery));
ロードイベントなどにhoge.user.findを紐付ければプロフィールが表示されるようになりました。
後日「ユーザー情報を使ってリスト表示とグラフ表示を実装してほしい」という要望があがりました。
上記のモジュール群を書き換えることにします。
// 名前空間hogeを定義
var hoge = hoge || {};
/* ユーザーモジュール */
hoge.user = (function($){
var find;
find = function(){
$.ajax({
/* ユーザー情報取得.. */
})
// 取得に成功したら各描画処理を実行
.done(function( user_data ){
// プロフィール描画処理
hoge.profile.draw( user_data );
// グラフ描画処理
hoge.graph.draw( user_data );
// リスト描画処理
hoge.list.draw( user_data );
});
};
return {
find : find
}
}(jQuery));
hoge.profile = /* プロフィールモジュール */
hoge.graph = /* グラフ描画処理 */
hoge.list = /* リスト描画処理 */
グラフモジュールとリストモジュールを追加し、ユーザーモジュールからコールするように修正しました。
今のところまだまだ問題はなさそうですが、上記のコードはいくつかの「嫌な予感」を抱えています。
ひとつは、今後ユーザー情報を使った処理が増えるたびにhoge.user.find後の.done()が太っていく点です。
- 「タブ毎に線グラフ・棒グラフ・円グラフを表示してほしい」
- 「取得時刻を表示してほしい」
- 「通信中はプログレスバーを表示し、取得後は100%を表示してほしい」
- 「取得したユーザー情報によって○○を描画して欲しい」
etc... まだまだ要望はありえそうですが、これらを追加するたびに.done()からの呼び出しが必要になります。
ここに条件分岐などが入り混じってくると、なかなかに読みづらいコードになりそうな気配です。
もうひとつは、ユーザーモジュールが受け持っているユーザー情報取得処理を他のページで使いまわせないことです。
無論これについてはそもそも例が悪く、取得処理と各描画の実行処理が分離できていないのが原因なのですが、
その際にもページ毎に描画実行処理を受け持つモジュールが必要となりそうです。
発行/購読パターンを使ってみる
さきほどのコードを発行/購読パターンを使って書き換えてみましょう。
javascriptはイベント駆動型言語であり、このパターンと非常に相性が良いです。
javascriptでの発行/購読パターンはカスタムイベントを用いて実現します。しかし、コアで提供されているわけではないので自前で準備しましょう。
発行/購読パターンを提供するライブラリはいろいろありますが、ここでは、jQuery Tiny Pub/Subを使います。
このライブラリはその名の通り非常にコンパクトにまとまっており、コードはわずか21行しかありません。
GitHub - jQuery Tiny Pub/Sub
/* jQuery Tiny Pub/Sub - v0.7 - 10/27/2011
* http://benalman.com/
* Copyright (c) 2011 "Cowboy" Ben Alman; Licensed MIT, GPL */
(function($) {
var o = $({});
$.subscribe = function() {
o.on.apply(o, arguments);
};
$.unsubscribe = function() {
o.off.apply(o, arguments);
};
$.publish = function() {
o.trigger.apply(o, arguments);
};
}(jQuery));
jQuery Tiny Pub/Subは3つのユーティリティメソッドを提供します。
- $.subscribe - 購読を担当するメソッドです。第一引数にイベント名を指定し、第二引数にコールバック関数を指定します。
- $.publish - 発行を担当するメソッドです。第一引数にイベント名を指定し、第二引数に配列を渡します。この配列の持つ要素が、$.subscribeのコールバック関数の引数となります。
- $.unscribe - 第一引数で指定したイベントを停止します。
ここで作成したイベントが、本稿冒頭で説明のあったチャンネル(トピック)にあたります。
jQuery.fn.on,offを使用しているので名前空間も使えます。(参照:jQuery.fn.onに名前空間をつけることができる)
$.publishが実行されると、同じチャンネルを共有するすべての$.subscribeに通知され、コールバック関数が実行されます。
上記を使って、最初のコードを書き換えてみましょう。
// 名前空間hogeを定義
var hoge = hoge || {};
/* ユーザーモジュール */
hoge.user = (function($){
var find;
find = function(){
$.ajax({
/* ユーザー情報取得.. */
})
.done(function( user_data ){
// ユーザー情報取得完了後、チャンネルに通知
$.publish('hoge.user.loaded', [ user_data ]);
});
};
return {
find : find
}
}(jQuery));
/* プロフィールモジュール */
hoge.profile = (function($){
var draw;
draw = function( user_data){ /* プロフィール描画処理.. */ };
// ユーザー情報取得完了チャンネルを購読
$.subscribe('hoge.user.loaded', function( e, user_data ){
draw(user_data);
});
}(jQuery));
/* グラフモジュール */
hoge.graph = (function($){
var draw;
draw = function( user_data){ /* グラフ描画処理.. */ };
// ユーザー情報取得完了チャンネルを購読
$.subscribe('hoge.user.loaded', function( e, user_data ){
draw(user_data);
});
}(jQuery));
/* リストモジュール */
/* etc... */
hoge.user.findはサーバーからデータを取得した後、チャンネル'hoge.user.loaded'に通知するようになりました。
hoge.profile.drawおよびhoge.graph.drawは、通知が発行されると同時にuser_dataを受け取り、描画処理が走ります。
注目すべきは、発行側(hoge.user)と購読側(hoge.profile,hoge.graph)は、互いの実装について何も知らないということです。
発行側は、通知したデータがどのように利用されるか知る必要がなく、購読側はチャンネルの通知者が何者であるかを知る必要がありません。両者とも、そのチャンネルが何を通知するか/されるかだけを知っていればよくなります。
機能を追加したい場合はモジュール内部でチャンネル'hoge.user.loaded'を購読すればよく、その都度hoge.user.findを書き換える必要はありません。機能を削除したい場合は単に購読をやめれば済みます。また、hoge.user.findもユーザー処理後は単に通知をするのみなので再利用性が高まっています。新たに別のモジュールからユーザー情報を取得する必要が出ても、単にそのモジュールからチャンネルhoge.user.loadedに通知するだけでよいでしょう。
まとめ
以前のhoge.jsは、hoge.user.findメソッドが、下位モジュールのメソッドに直接依存していました。
発行/購読パターン適用後は、そういった『上位モジュール->下位モジュール』という関係から、『上位モジュール->チャンネル<-下位モジュール』という関係に変化していることがわかります。これは依存関係逆転の原則の実践例といえそうです。このことにより、各モジュール間の結合が疎に保たれ、拡張性、再利用性が高まりました。
反面、欠点もあります。たとえば、チャンネルに通知される引数を変更するには、すべての購読者について考慮したうえで、変更前との互換性を保たなければならなくなります。疎結合をもたらした「互いの実装について何も知らなくてよい」というメリットが、ここでは追跡性の困難さとなってあらわれるかもしれません。
とはいえ、他のデザインパターンと同じく万能解ではありませんが、使いどころを間違えなければ強力な手段となります。
アプリケーションの処理の流れがイベントという形で明確化され、コードの管理も容易になります。うまいこと使っていきましょう。