105

それで、同僚がパブリッシュ/サブスクライブ パターン (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 パターンを使用する価値はありますか?

4

7 に答える 7

228

ここ数年で非常にモダンになった JavaScript の MV* (MVC/MVP/MVVM) パターンと密接に連携する、疎結合と単一の責任がすべてです。

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

疎結合について話すときは、関心の分離について言及する必要があります. 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* アプローチが気に入らない場合は、少し異なる方法を使用できます (次に説明する方法と最後に説明する方法の間には共通部分があります)。アプリケーションを異なるモジュールで構成するだけです。たとえば、ツイッターを見てください。

ツイッターモジュール

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

このアプローチについては、Nicholas Zakasによる優れた講演があります。MV* アプローチについては、私が知っている最高の記事や本は、Addy Osmaniによって出版されています。

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

于 2012-11-22T13:38:33.317 に答える
16

The main goal is to reduce coupling between the code. It's a somewhat event-based way of thinking, but the "events" aren't tied to a specific object.

I'll write out a big example below in some pseudo code that looks a bit like JavaScript.

Let's say we have a class Radio and a class 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?
    }
}

Whenever radio receives a signal, we want a number of relays to relay the message in some way. The number and types of relays can differ. We could do it like this:

class Radio {
    var relayList = [];

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

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

}

This works fine. But now imagine we want a different component to also take part of the signals that the Radio class receives, namely Speakers:

(sorry if the analogies aren't top notch...)

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

We could repeat the pattern again:

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);
        }

    }

}

We could make this even better by creating an interface, like "SignalListener", so that we only need one list in the Radio class, and always can call the same function on whatever object we have that wants to listen to the signal. But that still creates a coupling between whatever interface/base class/etc we decide on and the Radio class. Basically whenever you change one of the Radio, Signal or Relay class you have to think about how it could possibly affect the other two classes.

Now let's try something different. Let's create a fourth class named RadioMast:

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);
        }
    }
}

Now we have a pattern that we are aware of and we can use it for any number and types of classes as long as they:

  • are aware of the RadioMast (the class handling all the message passing)
  • are aware of the method signature for sending/receiving messages

So we change the Radio class to its final, simple form:

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

And we add the speakers and the relay to the RadioMast's receiver list for this type of signal:

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

Now the Speakers and Relay class has zero knowledge of anything except that they have a method that can receive a signal, and the Radio class, being the publisher, is aware of the RadioMast that it publishes signals to. This is the point of using a message-passing system like publish/subscribe.

于 2012-11-22T13:29:46.517 に答える
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 で行うのに便利な区別です。

于 2015-09-24T21:06:25.600 に答える
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 );
}

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

もちろん、同じモジュール内で直接メソッドを呼び出すことができることに注意してください。ただし、妥当な基準では、ビジネス データと表面的な GUI 動作を同じモジュールに配置することはできません。

于 2012-11-22T12:50:21.027 に答える
1

PubSub の実装は、ある場所で一般的に見られます -

  1. イベントバスの助けを借りて通信する複数のポートレットがあるポートレットのような実装があります。これは、async アーキテクチャの作成に役立ちます。
  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.
于 2017-04-23T17:41:20.403 に答える