メインコンテンツへ移動

オブザーバーパターン_JavaScript デザインパターン 4

無料2015-07-16#JS#Design_Pattern#JavaScript观察者模式#Observer_Pattern#发布订阅模式#Publish/Subscribe

最も古典的なオブザーバーパターンの応用はイベントメカニズムです。本稿では JS でオブザーバーパターンとそれに類似した Pub/Sub パターンをどのように実装するかを詳細に紹介します

一.Observer(オブザーバー)パターン

オブザーバーパターンを利用すれば、オブジェクト間の「1 対多」の依存関係を簡単に構築できます; オブザーバーパターンのメカニズムを利用すれば、このような依存関係の動的維持を簡単に実現できます

黯羽轻扬:デザインパターン之オブザーバーパターン(Observer Pattern) より引用)

オブザーバーパターンは主題とオブザーバーをデカップリングし、主題はオブザーバーの詳細を知らず、すべてのオブザーバーがupdateインターフェースを持っていることだけを知っています;オブザーバーは主題についてさらに少なくしか知らず、主題の更新を受動的に待つだけです;各オブザーバー間はお互いを全く見ることができず、同じ主題を購読しているかどうかも知りません

疎結合は柔軟性(弾力性)を意味し、既定のインターフェースを実装している限り、主題、オブザーバーを独立して修正でき、二者の交互関係に影響を与えません

二.古典的オブザーバーパターン

オブザーバーパターンの概念は比較的単純で、古典的実装には 1 レベルの抽象化(Observ、Subject のインターフェース定義)を追加する必要があります。JS 実装の単純なオブザーバーパターンは以下の通りです:

/* 古典的オブザーバーパターン
 * Observer、Subject
 * 1 レベルの抽象化を追加することもできますが、JS の継承は複雑すぎるため、ここでは簡略化します
 * List を手動で維持します
 */

// Subject
function Subject(name) {
    this.name = name;
    this.list = [];
}
// 購読
Subject.prototype.regist = function(obj) {
    this.list.push(obj);
};
// 購読解除
Subject.prototype.unregist = function(obj) {
    var list = this.list;

    for(var i = 0, len = list.length; i < len; i++) {
        if (list[i] === obj) {
            list.splice(i, 1);
        }
    }
};
// すべてのファンに通知
Subject.prototype.notify = function() {
    var list = this.list;

    for(var i = 0, len = list.length; i < len; i++) {
        list[i].update({data: this.name});
    }
};

// Observer
function Observer(id) {
    this.id = id;
    // Subject が呼び出す更新メソッド
    this.update = function(dataObj) {
        // ...
        console.log(this.id + ' received ' + dataObj.data + '\'s update');
    };
}

// test
var subject = new Subject('My Topic');
var observer1 =  new Observer(1);
var observer2 =  new Observer(2);
var observer3 =  new Observer(3);

subject.regist(observer1);
subject.regist(observer2);
subject.notify();   // 主題更新、すべてのオブザーバーに通知

subject.regist(observer3);  // observer3 が新規加入
subject.notify();

// observer1 が参加しなくなる
subject.unregist(observer1);
subject.notify();

実行結果は以下の通り:

[caption id="attachment_663" align="alignnone" width="239"]オブザーバーパターン オブザーバーパターン[/caption]

P.S.1 レベルの抽象化に興味がある場合は、[黯羽轻扬:JS の 6 種の継承方式を再理解する](http://www.ayqy.net/blog/重新理解 JS の 6 種継承方式/) を参照して、1 レベルの抽象化を自分で実装してみてください

三.Publish/Subscribe(发布/購読)��ターン

オブザーバーパターンは主題とオブザーバーを分離しますが、交互関係は二者の内部に埋め込まれています(既定のインターフェースを実装する必要があります)。もし交互関係をさらにデカップリングするなら、それは Pub/Sub パターンと呼ばれます

Pub/Sub パターンは主題とオブザーバーの間にイベントメカニズムという層を新たに追加し、イベントメカニズムが双方にインターフェースを提供し、交互関係を維持します。この層を追加する利点は、イベントキュー、イベントバブリング(DOM だけでなく存在します哦~)など、より多くの制御を導入できることです

ここまではデカップリングを極限まで行ったことになります。巨大な柔軟性をもたらす一方で、いくつかの避けられない欠点も存在します。例えば、あるオブザーバーがクラッシュした場合、デカップリングの関係により、このメッセージは外部に伝達されず、主題も知らず、周囲のオブザーバーも知らず、このオブザーバーに依存する他のモジュールも知りません。(理論的にはイベント管理メカニズムを強化することでこの状況を制御できますが、これによりイベント層がますます複雑になり、構造もますます膨張します)

四.カスタムイベントメカニズムの実装による Pub/Sub パターンの実現

注意「イベントメカニズム」であり、「イベント」ではありません。カスタムイベントは DOM API であり、DOM 要素にのみバインドできるためです。詳細はDocument.createEvent() - Web API Interfaces | MDN を参照してください

したがって、カスタム「イベントメカニズム」を実装する必要があります。実装の詳細は[黯羽轻扬:JS 学習ノート 11_ 高級テクニック](http://www.ayqy.net/blog/JS 学習ノート 11_ 高級テクニック/) の底部を参照してください

DOM3 レベル API はカスタムイベントの作成をサポートし、具体的には以下の通りです:

// カスタムイベントオブジェクトを作成
var event = document.createEvent('CustomEvent');
// イベントを初期化
event.initCustomEvent('myeve', true, true);
// initCustomEvent インターフェース定義は以下の通り
/*
void initCustomEvent(
    in DOMString type,
    in boolean canBubble,
    in boolean cancelable,
    in any detail
);
*/

document.createEventinitCustomEventはすでに時代遅れであり、現在はコンストラクタで直接カスタムイベントオブジェクトを作成できます(例はJavaScript CustomEvent を参照)。もちろん現在(2015-7-13)時代遅れのものの互換性は最も良いです。詳細はDocument.createEvent() - Web API Interfaces | MDN を参照してください

[IE8-] との互換性が必要な場合、いくつかの別の手段を講じる必要があります。ここでは展開しませんが、JavaScript カスタムイベント を参照してください

P.S.プロジェクトに主流の JS ライブラリ(フレームワーク)が導入されている場合、一般的にカスタムイベントをサポートしていますが、それらも DOM 要素にバインドされています。非 DOM 要素オブジェクトのイベントメカニズムは自分で実装する必要があります。非常にミニマルな Pub/Sub 実装があり、参考になりますaddyosmani/pubsubz。コードはとても精巧で、一見の価値があります

五.オブザーバーパターンの具体的応用

オブザーバーパターンの重要な応用シナリオの 1 つは Ajax リクエストのコールバックロジックです。主題は Ajax リクエストの戻りデータであり、オブザーバーはコールバック関数内の各ロジックブロックです(list は実行順序を保証できます)。コールバック関数ですべてのオブザーバーに notify するだけで済みます。サンプルコードは以下の通り:

// Pub/Sub パターンの実装は以下から:https://github.com/addyosmani/pubsubz
(function(window) {
    var topics = {},
        subUid = -1,
        pubsubz = {};

    pubsubz.publish = function(topic, args) {
        if (!topics[topic]) {
            return false;
        }

        var subscribers = topics[topic],
            len = subscribers ? subscribers.length : 0;
        while (len--) {
            subscribers[len].func(topic, args);
        }

        return true;
    };

    pubsubz.subscribe = function(topic, func) {
        if (!topics[topic]) {
            topics[topic] = [];
        }

        var token = (++subUid).toString();
        topics[topic].push({
            token: token,
            func: func
        });

        return token;
    };

    pubsubz.unsubscribe = function(token) {
        for (var m in topics) {
            if (topics[m]) {
                for (var i = 0, j = topics[m].length; i < j; i++) {
                    if (topics[m][i].token === token) {
                        topics[m].splice(i, 1);
                        return token;
                    }
                }
            }
        }
        return false;
    };

    window.pubsubz = pubsubz;
}(this));


// 具体的応用
var psz = pubsubz;
var eventName = 'DataUpdate';

var obs1 = psz.subscribe(eventName, function(data) {
    console.log('obs1 received ' + data);
    console.log('無効データをクリア');
});
var obs2 = psz.subscribe(eventName, function(data) {
    console.log('obs2 received ' + data);
    console.log('新データを表示');
});
var obs3 = psz.subscribe(eventName, function(data) {
    console.log('obs3 received ' + data);
    console.log('アイコンリストを更新');
});

// ajax をシミュレート
function ajax(url, callback) {
    // 50ms 後にコールバックを実行(データを取得したと仮定)
    setTimeout(function(data) {
        data = 'data';  // データをシミュレート

        callback(data);
    }, 50);
}

ajax('emptyUrl', function(data) {
    psz.publish(eventName, data);
});

// 実行結果は以下の通り:
// obs3 received DataUpdate
// アイコンリストを更新
// obs2 received DataUpdate
// 新データを表示
// obs1 received DataUpdate
// 無効データをクリア
// (逆順なのは pubsubz.publish が減算イテレーションを採用しているため)

もちろん、これにより一定の問題ももたらされます。例えば、ロジックが各オブザーバーに分散しているため、コーディングの難易度が上がります(イベント駆動の欠点に類似)。使用する際は具体的なシナリオに応じて权衡する必要があります

参考資料

  • 『JavaScript デザインパターン』

  • 関連ブログ記事若干

コメント

コメントはまだありません

コメントを書く