问题描述
所以,一位同事向我介绍了发布/订阅模式(以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/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')); });
我肯定会读到有关模式的信息,但是我无法想象为什么这是必要的.我看到的教程解释了 如何实现此模式仅涵盖我自己的基本示例.
我想,酒吧/sub的实用性将使自己在更复杂的应用中显而易见,但我无法想象.恐怕我完全错过了这一点.但是我想知道是否有一个!
您能否简洁地解释 为什么以及在什么情况下这种模式有利?像我上面的示例一样,是否值得在代码段中使用酒吧/子图案?
推荐答案
这一切都是关于松散的耦合和单一责任,它可以与JavaScript中的MV*(MVC/MVP/MVVM)交手,这些模式在过去几年中非常现代.
松散的耦合是一个面向对象的原则,系统的每个组成部分都知道其责任并且不在乎关于其他组件(或至少试图不尽可能多地关心它们).松散的耦合是一件好事,因为您可以轻松地重复使用不同的模块.您没有与其他模块的接口相结合.使用Publish/订阅,您仅与发布/订阅界面相结合,这没什么大不了的 - 只有两种方法.因此,如果您决定在另一个项目中重复使用模块,则可以复制和粘贴它,它可能会起作用,或者至少您不需要太多的努力才能使其正常工作.
在谈论松散的耦合时,我们应该提及关注点的分离.如果您使用MV*架构模式构建应用程序,则始终具有模型和视图.该模型是应用程序的业务部分.您可以在不同的应用程序中重复使用它,因此将其与单个应用程序的视图(您要显示的位置)搭配不是一个好主意,因为通常在不同的应用程序中您具有不同的视图.因此,将发布/订阅用于模型视图通信是一个好主意.当您的模型更改时,它将发布事件,视图会捕获并自行更新.您没有发布/订阅的开销,它可以帮助您进行解耦.以相同的方式,您可以将应用程序逻辑保留在控制器中(MVVM,MVP它不完全是控制器),并保持视图尽可能简单.当您的视图更改(例如,用户单击某些内容)时,它只是发布了一个新事件时,控制器会抓住它并决定该怎么做.如果您熟悉 mvc 模式或 mvvm 在Microsoft Technologies(WPF/Silverlight)中,您可以想到出版/订阅,例如 observer模式.这种方法用于backbone.js,quotout.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代码,而是我的示例):
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 .
缺点:您必须谨慎使用发布/订阅.如果您有数百个活动,那么管理所有活动可能会变得非常困惑.如果您不使用命名领域(或以正确的方式使用),则可能会发生碰撞.调解器的高级实现,看起来很像出版/订阅,可以在此处找到 https://github.com/ajacksifice/ajacksifice/mediator.js .它具有命名间隔和事件"冒泡"之类的功能,当然可以中断.发布/订阅的另一个缺点是硬单元测试,可能很难隔离模块中的不同功能并独立测试.
其他推荐答案
主要目标是减少代码之间的耦合.这是一种基于事件的思维方式,但是"事件"并未与特定对象绑定.
我将在下面的一些伪代码中写出一个大示例,看起来有点像JavaScript.
假设我们有一个课程收音机和类中继:
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); } } }
这很好.但是现在想象一下,我们希望另一个组件也参与广播类收到的信号,即扬声器:
(对不起,如果类比不是一流的...)
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); } } }
我们可以通过创建一个界面(例如"信号",因此我们只需要一个在广播类中的列表,并且始终可以在我们想要收听信号的任何对象上调用相同的函数,从而使其变得更好.但这仍然可以在我们决定的任何接口/base类/等之间创建一个耦合.基本上,每当您更改收音机,信号或中继类时,您都必须考虑它可能如何影响其他两个类别.
现在让我们尝试一些不同的事情.让我们创建一个名为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); } } }
现在,我们有了我们知道的模式,只要它们都可以将其用于任何数字和类型的类型,只要它们:
- 知道放射线瘤(处理所有消息传递的类)
- 知道发送/接收消息的方法签名
因此,我们将无线电类更改为最终简单形式:
class Radio { function ReceiveSignal(signal) { RadioMast.Broadcast("specialradiosignal", signal); } }
,我们将扬声器和继电器添加到Radiomast的接收器列表中的此类信号:
RadioMast.RegisterReceivers("specialradiosignal", speakers.PlaySignal); RadioMast.RegisterReceivers("specialradiosignal", relay.RelaySignal);
现在,扬声器和继电器类对任何内容的知识为零,除了它们具有可以接收信号的方法,并且作为发布者的无线电类都知道它发布了信号的放射线仪.这是使用诸如发布/订阅的消息通话系统的重点.
其他推荐答案
其他答案在展示模式的工作原理方面做得很好.我想解决一个隐含的问题" 旧方法有什么问题?我的想法.
想象我们已经订阅了一个经济公告.该公告发布了一个标题:" 将道琼斯琼斯降低200点".这将是一个奇怪的,有些不负责任的消息.但是,如果它发表了:"今天早上为第11章资金保护申请安然,这是一个更有用的信息.请注意,该消息可能会导致道琼斯琼斯跌倒200点,但这是另一回事.
发送命令和建议刚刚发生的事情之间存在区别.考虑到这一点,请使用您的原始版本的酒吧/子图案,暂时忽略处理程序:
$.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... }
命令和通知之间的区别是使用此模式进行的有用区别.
问题描述
So, a colleague introduced me to the publish/subscribe pattern (in JS/jQuery), but I'm having a hard time getting to grips with why one would use this pattern over 'normal' JavaScript/jQuery.
For example, previously I had the following code...
$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(); } });
And I could see the merit of doing this instead, for example...
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')); });
Because it introduces the ability to re-use the removeOrder functionality for different events etc.
But why would you decide to implement the publish/subscribe pattern and go to the following lengths, if it does the same thing? (FYI, I used 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')); });
I've read about the pattern for sure, but I just can't imagine why this would ever be necessary. The tutorials I've seen that explain how to implement this pattern only cover just as basic examples as my own.
I imagine that the pub/sub's usefulness would make itself apparent in a more complex application, but I can't imagine one. I'm afraid that I am completely missing the point; but I'd like to know the point if there is one!
Could you explain succinctly why and in what situations this pattern is advantageous? Is it worth using the pub/sub pattern for code snippets like my examples above?
推荐答案
It’s all about loose coupling and single responsibility, which goes hand to hand with MV* (MVC/MVP/MVVM) patterns in JavaScript which are very modern in the last few years.
Loose coupling is an Object-oriented principle in which each component of the system knows its responsibility and doesn’t care about the other components (or at least tries to not care about them as much as possible). Loose coupling is a good thing because you can easily reuse the different modules. You’re not coupled with the interfaces of other modules. Using publish/subscribe you’re only coupled with the publish/subscribe interface which is not a big deal – just two methods. So if you decide to reuse a module in a different project you can just copy and paste it and it’ll probably work or at least you won’t need much effort to make it work.
When talking about loose coupling we should mention the separation of concerns. If you’re building an application using an MV* architectural pattern you always have a Model(s) and a View(s). The Model is the business part of the application. You can reuse it in different applications, so it’s not a good idea to couple it with the View of a single application, where you want to show it, because usually in the different applications you have different views. So it’s a good idea to use publish/subscribe for the Model-View communication. When your Model changes it publishes an event, the View catches it and updates itself. You don’t have any overhead from the publish/subscribe, it helps you for the decoupling. In the same manner you can keep your application logic in the Controller for example (MVVM, MVP it’s not exactly a Controller) and keep the View as simple as possible. When your View changes (or the user clicks on something, for example) it just publishes a new event, the Controller catches it and decides what to do. If you are familiar with the MVC pattern or with MVVM in Microsoft technologies (WPF/Silverlight) you can think of the publish/subscribe like the Observer pattern. This approach is used in frameworks like Backbone.js, Knockout.js (MVVM).
Here is an example:
//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); } } }());
Another example. If you don’t like the MV* approach you can use something a little different (there’s an intersection between the one I’ll describe next and the last mentioned). Just structure your application in different modules. For example look at Twitter.
If you look at the interface you simply have different boxes. You can think of each box as a different module. For example you can post a tweet. This action requires the update of a few modules. Firstly it has to update your profile data (upper left box) but it also has to update your timeline. Of course, you can keep references to both modules and update them separately using their public interface but it’s easier (and better) to just publish an event. This will make the modification of your application easier because of looser coupling. If you develop new module which depends on new tweets you can just subscribe to the “publish-tweet” event and handle it. This approach is very useful and can make your application very decoupled. You can reuse your modules very easily.
Here is a basic example of the last approach (this is not original twitter code it’s just a sample by me):
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); }); } }; }());
For this approach there's an excellent talk by Nicholas Zakas. For the MV* approach the best articles and books I know of are published by Addy Osmani.
Drawbacks: You have to be careful about the excessive use of publish/subscribe. If you’ve got hundreds of events it can become very confusing to manage all of them. You may also have collisions if you’re not using namespacing (or not using it in the right way). An advanced implementation of Mediator which looks much like an publish/subscribe can be found here https://github.com/ajacksified/Mediator.js. It has namespacing and features like event “bubbling” which, of course, can be interrupted. Another drawback of publish/subscribe is the hard unit testing, it may become difficult to isolate the different functions in the modules and test them independently.
其他推荐答案
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.
其他推荐答案
The other answers have done a great job in showing how the pattern works. I wanted to address the implied question "what is wrong with the old way?" as I've been working with this pattern recently, and I find it involves a shift in my thinking.
Imagine we have subscribed to an economic bulletin. The bulletin publishes a headline: "Lower the Dow Jones by 200 points". That would be an odd and somewhat irresponsible message to send. If however, it published: "Enron filed for chapter 11 bankrupcy protection this morning", then this is a more useful message. Note that the message may cause the Dow Jones to fall 200 points, but that is another matter.
There is a difference between sending a command, and advising of something that has just happened. With this in mind, take your original version of the pub/sub pattern, ignoring the handler for now:
$.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')); });
There is already an implied strong coupling here, between the user-action (a click) and the system-response (an order being removed). Effeectively in your example, the action is giving a command. Consider this version:
$.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')); });
Now the handler is responding to something of interest that has happened, but is under no obligation to remove an order. In fact, the handler can do all sorts of things not directly related to removing an order, but still maybe relevant to the calling action. For example:
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... }
The distinction between a command and a notification is a useful distinction to make with this pattern, IMO.