なぜパブリッシュ/サブスクライブパターン(JS / jQuery)を使用するのですか?


103

そのため、同僚からパブリッシュ/サブスクライブパターン(JS / jQuery)が紹介されましたが、「通常の」JavaScript / jQueryよりもこのパターンを使用する理由を理解するのに苦労しています。

たとえば、以前は次のコードがありました...

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    var orders = $(this).parents('form:first').find('div.order');
    if (orders.length > 2) {
        orders.last().remove();
    }
});

そして、代わりにこれを行うことのメリットを見ることができました...

removeOrder = function(orders) {
    if (orders.length > 2) {
        orders.last().remove();
    }
}

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    removeOrder($(this).parents('form:first').find('div.order'));
});

removeOrderさまざまなイベントなどで機能を再利用する機能が導入されているためです。

しかし、同じことをするのに、なぜパブリッシュ/サブスクライブパターンを実装して次の長さに進むことにしたのですか?(参考までに、jQuery tiny pub / subを使用しました)

removeOrder = function(e, orders) {
    if (orders.length > 2) {
        orders.last().remove();
    }
}

$.subscribe('iquery/action/remove-order', removeOrder);

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    $.publish('iquery/action/remove-order', $(this).parents('form:first').find('div.order'));
});

パターンについては確かに読みましたが、なぜこれが必要になるのか、想像もできません。説明したチュートリアルを見たこのパターンを実装方法を同じ基本的な例のみを対象としています。

pub / subの有用性は、より複雑なアプリケーションで明らかになると思いますが、想像できません。ポイントを完全に逃していると思います。でもあるなら要点を知りたい!

なぜ、どのような状況でこのパターンが有利であるかを簡潔に説明できますか?上記の例のようなコードスニペットにpub / subパターンを使用する価値はありますか?

回答:


222

これは、疎結合と単一の責任に関するものであり、JavaScriptのMV *(MVC / MVP / MVVM)パターンと密接に関係しており、過去数年間で非常に近代的です。

疎結合はオブジェクト指向の原則であり、システムの各コンポーネントはその責任を認識しており、他のコンポーネントについては気にしません(または、少なくともそれらについてはできるだけ気にしないようにします)。さまざまなモジュールを簡単に再利用できるため、疎結合は良いことです。他のモジュールのインターフェースとは連動していません。パブリッシュ/サブスクライブを使用すると、大したことではないパブリッシュ/サブスクライブインターフェースのみが結合されます。そのため、別のプロジェクトでモジュールを再利用する場合は、コピーして貼り付けるだけで機能するか、少なくとも機能させるためにそれほど労力を必要としません。

疎結合について話すとき、懸念分離について言及する必要があります。MV *アーキテクチャパターンを使用してアプリケーションを構築している場合は、常にモデルとビューがあります。モデルはアプリケーションのビジネス部分です。さまざまなアプリケーションで再利用できるため、通常、さまざまなアプリケーションではさまざまなビューがあるため、それを表示したい単一のアプリケーションのビューと組み合わせることはお勧めできません。したがって、Model-View通信にはパブリッシュ/サブスクライブを使用することをお勧めします。モデルが変更されると、イベントがパブリッシュされると、ビューはそれをキャッチして自身を更新します。パブリッシュ/サブスクライブによるオーバーヘッドがないため、分離に役立ちます。同様に、たとえばアプリケーションロジックをコントローラー(MVVM、MVPはコントローラーではありません)に保持し、ビューをできるだけシンプルに保つことができます。ビューが変更されると(またはユーザーが何かをクリックするなど)、新しいイベントが発行されるだけで、コントローラーはそれをキャッチして、何をするかを決定します。あなたがに精通している場合MVCパターン、またはMicrosoftテクノロジー(WPF / Silverlight)のMVVMでは、パブリッシュ/サブスクライブをObserverパターンのように考えることができます。このアプローチは、Backbone.js、Knockout.js(MVVM)などのフレームワークで使用されます。

次に例を示します。

//Model
function Book(name, isbn) {
    this.name = name;
    this.isbn = isbn;
}

function BookCollection(books) {
    this.books = books;
}

BookCollection.prototype.addBook = function (book) {
    this.books.push(book);
    $.publish('book-added', book);
    return book;
}

BookCollection.prototype.removeBook = function (book) {
   var removed;
   if (typeof book === 'number') {
       removed = this.books.splice(book, 1);
   }
   for (var i = 0; i < this.books.length; i += 1) {
      if (this.books[i] === book) {
          removed = this.books.splice(i, 1);
      }
   }
   $.publish('book-removed', removed);
   return removed;
}

//View
var BookListView = (function () {

   function removeBook(book) {
      $('#' + book.isbn).remove();
   }

   function addBook(book) {
      $('#bookList').append('<div id="' + book.isbn + '">' + book.name + '</div>');
   }

   return {
      init: function () {
         $.subscribe('book-removed', removeBook);
         $.subscribe('book-aded', addBook);
      }
   }
}());

もう一つの例。MV *アプローチが気に入らない場合は、少し異なるものを使用できます(次に説明するものと最後に説明したものの間には共通点があります)。異なるモジュールでアプリケーションを構築するだけです。たとえば、Twitterを見てください。

Twitterモジュール

インターフェースを見ると、単に異なるボックスがあります。各ボックスは異なるモジュールと考えることができます。たとえば、ツイートを投稿できます。このアクションには、いくつかのモジュールの更新が必要です。まず、プロファイルデータ(左上のボックス)を更新する必要がありますが、タイムラインも更新する必要があります。もちろん、両方のモジュールへの参照を保持し、それらのパブリックインターフェイスを使用して個別に更新できますが、イベントを公開するだけの方が簡単(かつ優れています)です。これにより、結合が緩やかになるため、アプリケーションの変更が容易になります。新しいツイートに依存する新しいモジュールを開発する場合は、「publish-tweet」イベントをサブスクライブして処理できます。このアプローチは非常に便利で、アプリケーションを非常に切り離すことができます。モジュールは非常に簡単に再利用できます。

これが最後のアプローチの基本的な例です(これはオリジナルのTwitterコードではなく、私がサンプルにしただけです):

var Twitter.Timeline = (function () {
   var tweets = [];
   function publishTweet(tweet) {
      tweets.push(tweet);
      //publishing the tweet
   };
   return {
      init: function () {
         $.subscribe('tweet-posted', function (data) {
             publishTweet(data);
         });
      }
   };
}());


var Twitter.TweetPoster = (function () {
   return {
       init: function () {
           $('#postTweet').bind('click', function () {
               var tweet = $('#tweetInput').val();
               $.publish('tweet-posted', tweet);
           });
       }
   };
}());

このアプローチについては、ニコラス・ザカスによるすばらしい講演があります。MV *アプローチの場合、私が知っている最高の記事と本はAddy Osmaniから出版されています。

欠点:パブリッシュ/サブスクライブの過度の使用に注意する必要があります。何百ものイベントがある場合、それらすべてを管理するのは非常に混乱する可能性があります。ネームスペースを使用していない(または正しい方法で使用していない)場合も、衝突が発生する可能性があります。パブリッシュ/サブスクライブのように見えるMediatorの高度な実装は、https://github.com/ajacksified/Mediator.jsにあります。名前空間とイベント「バブリング」などの機能があり、もちろん中断できます。パブリッシュ/サブスクライブのもう1つの欠点は、ハードユニットテストであり、モジュール内のさまざまな機能を分離して個別にテストすることが困難になる場合があります。


3
ありがとうございます。PHPでMVCパターンをいつも使用しているので、MVCパターンに精通していますが、イベント駆動型プログラミングの観点からは考えていませんでした。:)
Maccath

2
この説明をありがとう。本当に頭を抱えるのに役立ちました。
flybear、2015

1
それは素晴らしい答えです。私はこれに賛成投票することを止めることができませんでした:)
Naveed Butt

1
素晴らしい説明、複数の例、さらなる読書の提案。A ++。
Carson

16

主な目標は、コード間の結合を減らすことです。これはややイベントベースの考え方ですが、「イベント」は特定のオブジェクトに関連付けられていません。

JavaScriptのように見えるいくつかの疑似コードで、以下の大きな例を書きます。

クラスRadioとクラスRelayがあるとします。

class Relay {
    function RelaySignal(signal) {
        //do something we don't care about right now
    }
}

class Radio {
    function ReceiveSignal(signal) {
        //how do I send this signal to other relays?
    }
}

ラジオが信号を受信するたびに、いくつかのリレーが何らかの方法でメッセージをリレーするようにします。リレーの数とタイプは異なる場合があります。次のようにすることができます。

class Radio {
    var relayList = [];

    function AddRelay(relay) {
        relayList.add(relay);
    }

    function ReceiveSignal(signal) {
        for(relay in relayList) {
            relay.Relay(signal);
        }
    }

}

これは正常に動作します。しかし、今度は、Radioクラスが受信する信号の一部、つまりスピーカーにも別のコンポーネントを使用することを想像してください。

(アナロジーが一流でない場合は申し訳ありません...)

class Speakers {
    function PlaySignal(signal) {
        //do something with the signal to create sounds
    }
}

パターンをもう一度繰り返すことができます。

class Radio {
    var relayList = [];
    var speakerList = [];

    function AddRelay(relay) {
        relayList.add(relay);
    }

    function AddSpeaker(speaker) {
        speakerList.add(speaker)
    }

    function ReceiveSignal(signal) {

        for(relay in relayList) {
            relay.Relay(signal);
        }

        for(speaker in speakerList) {
            speaker.PlaySignal(signal);
        }

    }

}

"SignalListener"のようなインターフェイスを作成することで、これをさらに改善できます。これにより、Radioクラスに1つのリストのみが必要になり、信号をリッスンしたい任意のオブジェクトで常に同じ関数を呼び出すことができます。しかし、それでも、私たちが決定するインターフェイス/ベースクラスなどとRadioクラスの間に結合が作成されます。基本的に、Radio、Signal、Relayのいずれかのクラスを変更する場合は、それが他の2つのクラスにどのように影響するかを考える必要があります。

では、別のことを試してみましょう。RadioMastという名前の4番目のクラスを作成してみましょう。

class RadioMast {

    var receivers = [];

    //this is the "subscribe"
    function RegisterReceivers(signaltype, receiverMethod) {
        //if no list for this type of signal exits, create it
        if(receivers[signaltype] == null) {
            receivers[signaltype] = [];
        }
        //add a subscriber to this signal type
        receivers[signaltype].add(receiverMethod);
    }

    //this is the "publish"
    function Broadcast(signaltype, signal) {
        //loop through all receivers for this type of signal
        //and call them with the signal
        for(receiverMethod in receivers[signaltype]) {
            receiverMethod(signal);
        }
    }
}

これで、私たちが認識しているパターンがあり、次の条件を満たす限り、任意の数およびタイプのクラスに使用できます。

  • RadioMast(すべてのメッセージパッシングを処理するクラス)を認識している
  • メッセージを送受信するためのメソッドシグネチャを認識している

そのため、Radioクラスを最終的な単純な形式に変更します。

class Radio {
    function ReceiveSignal(signal) {
        RadioMast.Broadcast("specialradiosignal", signal);
    }
}

そして、スピーカーとリレーをこのタイプの信号のRadioMastのレシーバーリストに追加します。

RadioMast.RegisterReceivers("specialradiosignal", speakers.PlaySignal);
RadioMast.RegisterReceivers("specialradiosignal", relay.RelaySignal);

現在、Speakers and Relayクラスは、信号を受信できるメソッドがあることを除いて何も認識しておらず、パブリッシャーであるRadioクラスは、信号をパブリッシュするRadioMastを認識しています。これが、パブリッシュ/サブスクライブなどのメッセージパッシングシステムを使用するポイントです。


pub / subパターンの実装が「通常の」メソッドを使用するよりも優れていることを示す具体的な例があるのは本当に素晴らしいことです。ありがとうございました!
Maccath

1
どういたしまして!個人的には、私が解決する実際の問題に気づくまで、新しいパターンや方法論については、私の脳が「クリック」しないことに気が付きます。sub / pubパターンは、概念的に密結合されているアーキテクチャに最適ですが、可能な限りそれらを分離しておく必要があります。GUIなどなどプレーヤー、弾丸、木、幾何学、:すべてが、例えばその周囲起こって物事に対処する必要があり、これらのオブジェクトはすべてできることをあなたがオブジェクトの数百を持っているゲームを想像
アンダース・アルピ

3
JavaScriptにはclassキーワードがありません。このことを強調してください。コードを疑似コードとして分類する。
Rob W

実際、ES6にはclassキーワードがあります。
Minko Gechev

5

他の答えは、パターンがどのように機能するかを示すのに素晴らしい仕事をしました。私はこのパターンを最近使用しているため、暗黙の質問「古い方法の何が問題になっていますか?」に対処したいと思いました。

経済速報を購読したとしましょう。この速報には、「ダウジョーンズを200ポイント下げる」という見出しが付けられています。それは、奇妙でやや無責任なメッセージです。しかし、「エンロンが今朝、第11章破産保護を申請した」と発表した場合、これはより有用なメッセージです。このメッセージによってダウジョーンズが200ポイント下がる可能性がありますが、それは別の問題です。

コマンドを送信することと、発生したことを通知することには違いがあります。これを念頭に置いて、今のところハンドラを無視して、元のバージョンのpub / subパターンを取得します。

$.subscribe('iquery/action/remove-order', removeOrder);

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    $.publish('iquery/action/remove-order', $(this).parents('form:first').find('div.order'));
});

ここには、ユーザーのアクション(クリック)とシステムの応答(削除される注文)の間に、暗黙の強い結合があります。あなたの例では効果的に、アクションはコマンドを与えることです。このバージョンを検討してください:

$.subscribe('iquery/action/remove-order-requested', handleRemoveOrderRequest);

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    $.publish('iquery/action/remove-order-requested', $(this).parents('form:first').find('div.order'));
});

これで、ハンドラーは発生した関心のある何かに応答していますが、注文を削除する義務はありません。実際、ハンドラーは、注文の削除に直接関係しないあらゆる種類のことを実行できますが、呼び出し側のアクションに関連している可能性があります。例えば:

handleRemoveOrderRequest = function(e, orders) {
    logAction(e, "remove order requested");
    if( !isUserLoggedIn()) {
        adviseUser("You need to be logged in to remove orders");
    } else if (isOkToRemoveOrders(orders)) {
        orders.last().remove();
        adviseUser("Your last order has been removed");
        logAction(e, "order removed OK");
    } else {
        adviseUser("Your order was not removed");
        logAction(e, "order not removed");
    }
    remindUserToFloss();
    increaseProgrammerBrowniePoints();
    //etc...
}

コマンドと通知の違いは、このパターンであるIMOを使用して区別すると便利です。


あなたの最後の2つの機能(場合remindUserToFlossincreaseProgrammerBrowniePoints)は、別々のモジュールに位置していた、あなたはお互いに右後2つのイベント1つの権利を発行するだろうそこにhandleRemoveOrderRequestまたはあなたがいるだろうflossModuleにイベントを発行するbrowniePointsモジュールremindUserToFloss()で行われていますか?
ブライアンP

4

メソッド/関数呼び出しをハードコードする必要がないように、誰が聞くかを気にせずにイベントを公開するだけです。これにより、パブリッシャーはサブスクライバーから独立し、アプリケーションの2つの異なる部分の間の依存関係(またはカップリング、任意の用語)が減少します。

ウィキペディアで言及されているように、結合のいくつかの欠点があります

密結合システムは、以下の発達上の特徴を示す傾向があり、これらはしばしば欠点と見なされます。

  1. 1つのモジュールでの変更は通常、他のモジュールでの変更の波及効果を強制します。
  2. モジュール間の依存性の増加により、モジュールの組み立てには、より多くの労力や時間が必要になる場合があります。
  3. 特定のモジュールは、依存するモジュールを含める必要があるため、再利用やテストが難しい場合があります。

ビジネスデータをカプセル化するオブジェクトのようなものを考えます。年齢が設定されるたびにページを更新するためのハードコードされたメソッド呼び出しがあります。

var person = {
    name: "John",
    age: 23,

    setAge: function( age ) {
        this.age = age;
        showAge( age );
    }
};

//Different module

function showAge( age ) {
    $("#age").text( age );
}

showAge関数を含めないと、人物オブジェクトをテストできなくなります。また、他のGUIモジュールでも年齢を表示する必要がある場合は.setAge、そのメソッド呼び出しをにハードコーディングする必要があり ます。これで、personオブジェクトに2つの無関係なモジュールの依存関係があります。また、これらの呼び出しが行われていて、それらが同じファイル内にない場合は、維持するのが困難です。

同じモジュール内で、直接メソッドを呼び出すことはもちろん可能です。ただし、ビジネスデータと表面的なGUIの動作は、合理的な基準で同じモジュールに常駐させないでください。


ここで「依存」の概念を理解していません。2番目の例の依存関係はどこにあり、3番目の例にはどこにないのですか?2番目と3番目のスニペットの間に実際的な違いはありません。実際の理由なしに、関数とイベントの間に新しい「レイヤー」が追加されているようです。私はおそらく盲目ですが、もっと指針が必要だと思います。:(
Maccath

1
同じことを実行する関数を作成するだけではなく、パブリッシュ/サブスクライブが適切なサンプルユースケースを提供できますか?
ジェフリースウィーニー

@Maccath簡単に言うと、3番目の例では、removeOrder存在することさえ知らない、または知って いる必要がないため、依存することはできません。2番目の例では、知っておく必要があります。
エサイリヤ、

ここで説明したことを実行するためのより良い方法があるように私はまだ感じていますが、特に他の多くの開発者がいる環境では、この方法論には目的があると確信しています。+1
ジェフリー・スウィーニー

1
@Esailija-ありがとう、私は少しよく理解できたと思います。ですから...サブスクライバーを完全に削除しても、エラーなどは発生せず、何も実行されません。これは、アクションを実行したいが、発行時に最も関連性のある機能が必ずしもわかっているわけではないが、サブスクライバーが他の要因に応じて変化する可能性がある場合に役立ちますか?
Maccath

1

PubSubの実装は、通常、次の場所に見られます-

  1. イベントバスの助けを借りて通信する複数のポートレットがある実装のようなポートレットがあります。これはayncアーキテクチャでの作成に役立ちます。
  2. 密結合により損傷したシステムでは、pubsubはさまざまなモジュール間の通信を支援するメカニズムです。

コード例-

var pubSub = {};
(function(q) {

  var messages = [];

  q.subscribe = function(message, fn) {
    if (!messages[message]) {
      messages[message] = [];
    }
    messages[message].push(fn);
  }

  q.publish = function(message) {
    /* fetch all the subscribers and execute*/
    if (!messages[message]) {
      return false;
    } else {
      for (var message in messages) {
        for (var idx = 0; idx < messages[message].length; idx++) {
          if (messages[message][idx])
            messages[message][idx]();
        }
      }
    }
  }
})(pubSub);

pubSub.subscribe("event-A", function() {
  console.log('this is A');
});

pubSub.subscribe("event-A", function() {
  console.log('booyeah A');
});

pubSub.publish("event-A"); //executes the methods.

1

「パブリッシュ/サブスクライブの多くの側面」という論文は良い読み物であり、彼らが強調することの1つは、3つの「次元」に分離することです。これが私の大まかな要約ですが、論文も参照してください。

  1. スペースの分離。対話する当事者はお互いを知る必要はありません。パブリッシャーは、誰が聞いているか、何人が聞いているか、イベントで何をしているのかを知りません。サブスクライバーは、誰がこれらのイベントをプロデュースしているのか、プロデューサーがいくついるのかなどわかりません。
  2. 時間の分離。対話する当事者は、対話中に同時にアクティブである必要はありません。たとえば、パブリッシャーがいくつかのイベントをパブリッシュしているときにサブスクライバーが切断されることがありますが、オンラインになるとサブスクライバーはそれに対応できます。
  3. 同期デカップリング。パブリッシャーはイベントの生成中にブロックされず、サブスクライバーは、サブスクライブしたイベントが到着するたびにコールバックを通じて非同期に通知されます。

0

単純な答え 元の質問は単純な答えを探していました。これが私の試みです。

JavaScriptは、コードオブジェクトが独自のイベントを作成するためのメカニズムを提供しません。したがって、一種のイベントメカニズムが必要です。パブリッシュ/サブスクライブパターンはこのニーズに答えるものであり、自分のニーズに最適なメカニズムを選択するのはあなた次第です。

これでpub / subパターンの必要性を確認できました。次に、pub / subイベントの処理方法とは異なる方法でDOMイベントを処理する必要がありますか?複雑さを軽減するため、および懸念の分離(SoC)などの他の概念のために、すべてが均一であることの利点がわかる場合があります。

逆説的に言えば、より多くのコードを使用することで、懸念事項の分離が改善され、非常に複雑なWebページに十分に拡張できます。

私は誰かがこれについて詳細に説明することなく十分に良い議論だと思うことを望みます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.