为什么要使用发布/订阅模式(在JS / jQuery中)?

时间:2012-11-22 12:39:30

标签: javascript jquery design-patterns publish-subscribe

所以,一位同事向我介绍了发布/订阅模式(在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模式来代码片段?

7 个答案:

答案 0 :(得分:216)

这是关于松耦合和单一责任的全部内容,它与JavaScript中的MV *(MVC / MVP / MVVM)模式密切相关,这些模式在过去几年非常现代。

Loose coupling是面向对象的原则,系统的每个组件都知道它的责任,并不关心其他组件(或者至少尽量不关心它们)。松散耦合是一件好事,因为您可以轻松地重用不同的模块。您没有与其他模块的接口耦合。使用发布/订阅你只需要发布/订阅接口,这不是一个大问题 - 只有两种方法。因此,如果您决定在不同的项目中重复使用模块,您可以复制并粘贴它,它可能会起作用,或者至少您不需要太多努力就可以使它运行。

在谈到松耦合时,我们应该提到separation of concerns。如果您使用MV *体系结构模式构建应用程序,则始终具有模型和视图。模型是应用程序的业务部分。您可以在不同的应用程序中重复使用它,因此将它与要显示它的单个应用程序的视图结合起来并不是一个好主意,因为通常在不同的应用程序中您有不同的视图。因此,使用发布/订阅进行模型 - 视图通信是个好主意。当您的模型更改它发布事件时,View会捕获它并自行更新。您没有发布/订阅的任何开销,它可以帮助您进行解耦。以同样的方式,您可以将应用程序逻辑保留在Controller中(例如MVVM,MVP,它不是一个控制器),并使View尽可能简单。当您的视图发生更改(或者用户点击某些内容)时,它只会发布一个新事件,Controller会捕获它并决定要执行的操作。如果您熟悉MVC模式或Microsoft技术中的MVVM(WPF / Silverlight),您可以将发布/订阅视为Observer pattern。这种方法用于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 Modules

如果您查看界面,您只需使用不同的框。您可以将每个框视为不同的模块。例如,您可以发布推文。此操作需要更新几个模块。首先,它必须更新您的个人资料数据(左上方框),但它还必须更新您的时间表。当然,您可以保留对这两个模块的引用并使用它们的公共接口单独更新它们,但只是发布一个事件更容易(也更好)。这将使您的应用程序的修改更容易,因为更松散的耦合。如果您开发依赖于新推文的新模块,您只需订阅“发布 - 推文”事件并处理它。这种方法非常有用,可以使您的应用程序非常分离。您可以非常轻松地重用模块。

这是最后一种方法的基本示例(这不是原始的推特代码,它只是我的样本):

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/ajacksified/Mediator.js找到Mediator的高级实现,它看起来很像发布/订阅。它具有命名空间和功能,如事件“冒泡”,当然,可以中断。发布/订阅的另一个缺点是硬单元测试,可能难以隔离模块中的不同功能并独立测试它们。

答案 1 :(得分:15)

主要目标是减少代码之间的耦合。这是一种基于事件的思维方式,但“事件”与特定对象无关。

我将在下面的一些伪代码中写出一个很大的例子,看起来有点像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类中只需要一个列表,并且总是可以在我们想要监听信号的任何对象上调用相同的函数。但是,这仍然会在我们决定的任何接口/基类/等与Radio类之间产生耦合。基本上每当你改变一个Radio,Signal或Relay类时,你必须考虑它可能如何影响其他两个类。

现在让我们尝试不同的东西。让我们创建一个名为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);
        }
    }
}

现在我们知道了一个模式,我们可以将它用于任何数量和类型的类,只要它们:

  • 知道RadioMast(处理所有消息传递的类)
  • 知道发送/接收消息的方法签名

因此我们将Radio类更改为最终的简单形式:

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

我们将扬声器和继电器添加到RadioMast的接收器列表中以获取此类信号:

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

现在,扬声器和中继类对任何事物都没有任何了解,除了它们有一个可以接收信号的方法,而作为发布者的Radio类知道它发布信号的RadioMast。这是使用发布/订阅等消息传递系统的重点。

答案 2 :(得分:5)

其他答案在展示模式如何运作方面做得很好。我想解决隐含的问题“ 旧方式出了什么问题? ”,因为我最近一直在使用这种模式,我发现它涉及到一个转变我的想法。

想象一下,我们订阅了一份经济公报。该公告发布了一个标题:“将道琼斯指数下调200点”。发送这将是一个奇怪的,有点不负责任的消息。然而,如果它发表:“安然公司今天早上申请破产保护”,那么这是一个更有用的信息。请注意,该消息可能导致道琼斯指数下跌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的有用区别。

答案 3 :(得分:4)

因此,您不必对方法/函数调用进行硬编码,只需发布​​事件而无需关心谁倾听。这使得发布者独立于订阅者,减少了应用程序的两个不同部分之间的依赖性(或耦合,无论您喜欢哪个术语)。

wikipedia

提到的耦合的一些缺点
  

紧密耦合的系统倾向于表现出以下发展   特征,通常被视为缺点:

     
      
  1. 一个模块的更改通常会对其他模块的更改产生连锁反应。
  2.   
  3. 由于模块间的依赖性增加,模块的组装可能需要更多的努力和/或时间。
  4.   
  5. 特定模块可能更难重用和/或测试,因为必须包含相关模块。
  6.   

考虑类似于封装业务数据的对象。它有硬编码方法 每当设置年龄时调用以更新页面:

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行为不应该以任何合理的标准存在于同一模块中。

答案 4 :(得分:0)

PubSub实现常见于 -

  1. 有一个类似portlet的实现,其中有多个portlet在事件总线的帮助下进行通信。这有助于在异步架构中创建。
  2. 在一个受紧耦合破坏的系统中,pubsub是一种有助于各种模块之间通信的机制。
  3. 示例代码 -

    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.
    

答案 5 :(得分:0)

论文"The Many Faces of Publish/Subscribe"是一本很好的读物,他们强调的一件事就是在三个维度中贬值#34;这是我粗略的总结,但请参阅论文。

  1. 空间加倍。互动方不需要彼此了解。出版商不知道谁在听,有多少或者他们在做什么。订阅者不知道谁在制作这些活动,或者有多少生产者等。
  2. 时间延迟。互动方不需要在互动期间同时处于活动状态。例如。当发布者发布某些事件时,订阅者可能会断开连接,当它成为在线时,它可以对其作出反应。
  3. 同步解耦。生成事件时不会阻止发布者,只要订阅的事件到达,就可以通过回调异步通知订阅者。

答案 6 :(得分:0)

简单答案 最初的问题是寻找一个简单的答案。这是我的尝试。

JavaScript不提供任何机制来使代码对象创建自己的事件。因此,您需要一种事件机制。 “发布/订阅”模式将满足此需求,您可以选择最适合自己需求的机制。

现在我们可以看到需要使用pub / sub模式,那么,您宁愿以与处理pub / sub事件不同的方式来处理DOM事件吗?为了降低复杂性,以及减少关注点分离(SoC)之类的其他概念,您可能会看到统一的好处。

矛盾的是,更多的代码创建了更好的关注点分离,可以很好地扩展到非常复杂的网页。

我希望有人能在不进行详细讨论的情况下找到足够好的讨论。