同僚がパブリッシュ/サブスクライブパターン(JS/jQuery)を紹介してくれましたが、whyでこのパターンを使用することに慣れるのに苦労しています「通常の」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
機能を再利用する機能が導入されているためです。
しかし、パブリッシュ/サブスクライブパターンを実装し、同じことを行う場合、次の長さにすることに決めたのはなぜですか? (FYI、私は 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'));
});
私は確かにパターンについて読んだことがありますが、なぜこれが必要になるのか想像できません。このパターンを実装するhowを説明するチュートリアルでは、基本的な例だけを取り上げています。
Pub/subの有用性は、より複雑なアプリケーションで明らかになると思いますが、それを想像することはできません。私はその点を完全に見逃しているのではないかと心配しています。しかし、私はポイントがあればそれを知りたいです!
簡潔に説明していただけますか?なぜ、どのような状況でこのパターンが有利ですか?上記の例のようなコードスニペットにpub/subパターンを使用する価値はありますか?
それはすべて、疎結合と単一の責任に関するものであり、JavaScriptのMV *(MVC/MVP/MVVM)パターンと密接に関連しています。
疎結合 はオブジェクト指向の原則であり、システムの各コンポーネントはその責任を知っており、他のコンポーネントを気にしない(または少なくとも試行する可能な限りそれらを気にしないように)。さまざまなモジュールを簡単に再利用できるため、疎結合は良いことです。他のモジュールのインターフェースと連動していません。パブリッシュ/サブスクライブを使用すると、大したことではないパブリッシュ/サブスクライブインターフェイスとのみ結合されます。2つの方法だけです。そのため、別のプロジェクトでモジュールを再利用することにした場合は、コピーして貼り付けるだけで機能する可能性があります。少なくとも、機能させるのにそれほど労力は必要ありません。
疎結合について話すときは、 懸念の分離 に言及する必要があります。 MV *アーキテクチャパターンを使用してアプリケーションを構築する場合、常にモデルとビューがあります。モデルは、アプリケーションのビジネス部分です。さまざまなアプリケーションで再利用できるため、通常はさまざまなアプリケーションにさまざまなビューがあるため、それを表示したい単一のアプリケーションのビューと組み合わせることはお勧めできません。そのため、Model-View通信にはパブリッシュ/サブスクライブを使用することをお勧めします。モデルが変更されると、イベントが発行され、ビューがそれをキャッチして更新します。パブリッシュ/サブスクライブによるオーバーヘッドはありません。デカップリングに役立ちます。同じ方法で、たとえばコントローラーでアプリケーションロジックを保持し(MVVM、MVPはコントローラーではありません)、ビューをできるだけシンプルに保つことができます。ビューが変更される(またはユーザーが何かをクリックするなど)と、ビューは新しいイベントを発行するだけで、コントローラーはそれをキャッチして、何をすべきかを決定します。 MVC パターンまたは MVVM Microsoftテクノロジー(WPF/Silverlight)では、パブリッシュ/サブスクライブは 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を見てください。
インターフェイスを見ると、単純に異なるボックスがあります。各ボックスを異なるモジュールと考えることができます。たとえば、ツイートを投稿できます。このアクションには、いくつかのモジュールの更新が必要です。まず、プロファイルデータ(左上のボックス)を更新する必要がありますが、タイムラインも更新する必要があります。もちろん、両方のモジュールへの参照を保持し、それらのパブリックインターフェイスを使用して個別に更新することもできますが、イベントを公開する方が簡単です(より良い)。これにより、疎結合によりアプリケーションの変更が容易になります。新しいツイートに依存する新しいモジュールを開発する場合は、「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 にあります。名前空間と、もちろん中断できるイベント「バブル」のような機能があります。パブリッシュ/サブスクライブの別の欠点は、ハードユニットテストです。モジュール内のさまざまな機能を分離し、それらを個別にテストすることが難しくなる場合があります。
主な目標は、コード間の結合を減らすことです。これはややイベントベースの考え方ですが、「イベント」は特定のオブジェクトに関連付けられていません。
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クラスが受信する信号の一部、つまりSpeakersを別のコンポーネントに含めることを考えてみましょう。
(類推が一流ではない場合は申し訳ありません...)
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);
}
}
}
これで、パターンが認識されました。次の条件を満たしていれば、任意の数とタイプのクラスに使用できます。
そこで、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を認識していることを除いて、何も知りません。これが、パブリッシュ/サブスクライブなどのメッセージパッシングシステムを使用するポイントです。
他の答えは、パターンがどのように機能するかを示すのに素晴らしい仕事をしました。暗黙の質問「古い方法の何が問題になっていますか?」に取り組んでいます。最近のパターン、そして私はそれが私の思考の変化を伴うことがわかります。
経済速報を購読していると想像してください。このセキュリティ情報では、「ダウ・ジョーンズを200ポイント下げる」という見出しが掲載されています。それは奇妙でやや無責任な送信メッセージです。ただし、「Enronが今朝、第11章の銀行破産の保護を申請した」場合、これはより有用なメッセージです。メッセージはcauseダウ・ジョーンズが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つの異なる部分間の依存関係(または結合、任意の用語)が削減されます。
wikipedia で言及されているカップリングのいくつかの欠点を以下に示します。
密結合システムは、次のような開発上の特徴を示す傾向がありますが、これは多くの場合、欠点と見なされます。
- 通常、1つのモジュールの変更は、他のモジュールの変更の波及効果を強制します。
- モジュールの依存関係が増加するため、モジュールのアセンブリにはより多くの労力や時間を必要とする場合があります。
- 特定のモジュールは、依存モジュールを含める必要があるため、再利用やテストが困難になる場合があります。
ビジネスデータをカプセル化するオブジェクトのようなものを考えてください。年齢が設定されるたびにページを更新するハードコードされたメソッド呼び出しがあります:
var person = {
name: "John",
age: 23,
setAge: function( age ) {
this.age = age;
showAge( age );
}
};
//Different module
function showAge( age ) {
$("#age").text( age );
}
showAge
関数も含めずにpersonオブジェクトをテストすることはできなくなりました。また、他のいくつかのGUIモジュールでも年齢を表示する必要がある場合は、そのメソッド呼び出しを.setAge
でハードコードする必要があり、personオブジェクトには2つの無関係なモジュールの依存関係があります。また、これらの呼び出しが行われているのを見ると、同じファイル内にない場合でも、維持するのは困難です。
同じモジュール内では、もちろん直接メソッドを呼び出すことができます。ただし、ビジネスデータと表面的なGUIの動作は、合理的な基準によって同じモジュールに存在するべきではありません。
論文 "パブリッシュ/サブスクライブの多くの顔" は良い読み物であり、彼らが強調することの1つは、3つの「次元」の縮約です。これは私の大まかな要約ですが、代わりに論文をご覧ください。
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("A"); //executes the methods.
簡単な答え元の質問は簡単な答えを探していました。これが私の試みです。
Javascriptは、コードオブジェクトが独自のイベントを作成するメカニズムを提供しません。そのため、ある種のイベントメカニズムが必要です。パブリッシュ/サブスクライブパターンがこのニーズに応えます。独自のニーズに最適なメカニズムを選択するのはユーザー次第です。
これで、pub/subパターンの必要性がわかりました。pub/ subイベントの処理方法とは異なる方法でDOMイベントを処理する必要がありますか?複雑さを軽減するため、および関心の分離(SoC)などの他の概念を使用すると、すべてが均一であるという利点が得られる場合があります。
逆説的に言えば、より多くのコードが懸念のより良い分離を生み出し、それは非常に複雑なウェブページにまで拡大します。
詳細を説明しなくても、誰かがこれで十分な議論になると思います。