Зачем использовать шаблон публикации/подписки (в JS/jQuery)?

Итак, коллега познакомил меня с шаблоном публикации/подписки (в 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 для разных событий и т.д.

Но почему вы решили внедрить шаблон публикации/подписки и перейти к следующей длине, если он сделает то же самое? (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'));
});

Я читал о шаблоне точно, но я просто не могу представить, почему это было бы необходимо. Учебники, которые я видел, которые объясняют, как реализовать этот шаблон, охватывают как основные примеры, как мои собственные.

Я предполагаю, что полезность pub/sub станет очевидной в более сложном приложении, но я не могу представить ее. Боюсь, что я совершенно не понимаю смысла; но я бы хотел узнать, есть ли он!

Не могли бы вы объяснить лаконично, почему и в каких ситуациях этот шаблон выгоден? Стоит ли использовать шаблон pub/sub для фрагментов кода, подобных моим примерам выше?

Ответ 1

Его все о свободной связи и единой ответственности, которая идет рука об руку с шаблонами MV * (MVC/MVP/MVVM) в JavaScript, которые очень современны в последние несколько лет.

Свободная связь является объектно-ориентированным принципом, в котором каждый компонент системы знает свою ответственность и не заботится о других компонентах (или, по крайней мере, пытается не заботиться о них как можно больше). Свободная связь - это хорошо, потому что вы можете легко использовать различные модули. Вы не связаны с интерфейсами других модулей. Использование publish/subscribe youre только в сочетании с интерфейсом публикации/подписки, что не имеет большого значения - всего два метода. Поэтому, если вы решите повторно использовать модуль в другом проекте, вы можете просто скопировать и вставить его, и он, вероятно, будет работать или, по крайней мере, вам не потребуется много усилий, чтобы заставить его работать.

Говоря о свободной связи, мы должны упомянуть о разделении проблем . Если вы создаете приложение с использованием архитектурного шаблона MV *, у вас всегда есть модель и вид (ы). Модель является бизнес-частью приложения. Вы можете повторно использовать его в разных приложениях, поэтому не рекомендуется сочетать его с представлением о одном приложении, где вы хотите его показать, потому что обычно в разных приложениях у вас разные взгляды. Поэтому неплохо использовать публикацию/подписку на связь Model-View. Когда ваша модель изменяет, она публикует событие, представление ловит его и обновляет. У вас нет накладных расходов на публикацию/подписку, это поможет вам разобраться. Таким же образом вы можете хранить логику приложения в контроллере, например (MVVM, MVP - это не совсем контроллер) и держать представление как можно проще. Когда ваш просмотр изменяется (или пользователь нажимает на что-то, например), он просто публикует новое событие, контроллер его ловит и решает, что делать. Если вы знакомы с шаблоном MVC или MVVM в технологиях Microsoft (WPF/Silverlight), вы может думать о публикации/подписке, как шаблон наблюдателя. Этот подход используется в таких структурах, как 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 *, вы можете использовать что-то немного другое (theres пересечение между одним Ill описывает следующий и последний упомянутый). Просто структурируйте свое приложение в разных модулях. Например, посмотрите на Twitter.

Twitter Modules

Если вы посмотрите на интерфейс, у вас просто разные коробки. Вы можете думать о каждом ящике как о другом модуле. Например, вы можете опубликовать твит. Это действие требует обновления нескольких модулей. Во-первых, он должен обновлять данные вашего профиля (верхний левый флажок), но он также должен обновлять вашу временную шкалу. Конечно, вы можете хранить ссылки на оба модуля и обновлять их отдельно, используя свой публичный интерфейс, но проще (и лучше) просто публиковать событие. Это облегчит изменение вашего приложения из-за ослабления связи. Если вы разрабатываете новый модуль, который зависит от новых твитов, вы можете просто подписаться на мероприятие "publish-tweet" и обработать его. Этот подход очень полезен и может сделать ваше приложение очень развязанным. Вы можете легко использовать свои модули.

Вот базовый пример последнего подхода (это не оригинальный твиттер-код, а только образец):

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.

Недостатки: вы должны быть осторожны с чрезмерным использованием публикации/подписки. Если у вас есть сотни событий, может стать очень запутанным управлять всеми ими. У вас также могут быть конфликты, если вы не используете пространство имен (или не используете его правильно). Передовая реализация медиатора, которая выглядит как публикация/подписка, находится здесь https://github.com/ajacksified/Mediator.js. Он имеет пространство имен и функции, такие как событие "пузырь", которое, конечно же, можно прервать. Другим недостатком публикации/подписки является жесткое тестирование, может быть сложно выделить различные функции в модулях и проверить их самостоятельно.

Ответ 2

Основная цель - уменьшить связь между кодом. Это несколько основанный на событиях способ мышления, но "события" не привязаны к конкретному объекту.

Я напишу большой пример ниже в некотором псевдокоде, который немного похож на 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);

Теперь класс Speakers and Relay имеет нулевые знания о чем-либо, кроме того, что у них есть метод, который может получать сигнал, а класс Radio, являющийся издателем, знает RadioMast, что он публикует сигналы. Это точка использования системы передачи сообщений, такой как публикация/подписка.

Ответ 3

Другие ответы проделали большую работу, показывая, как работает шаблон. Я хотел обратиться к подразумеваемому вопросу ", что не так со старым способом?", поскольку я недавно работал с этим шаблоном, и я считаю, что это связано с изменением моего мышления.

Представьте, что мы подписались на экономический бюллетень. Бюллетень публикует заголовок: "Опустите Dow Jones на 200 пунктов". Это было бы странным и несколько безответственным посланием для отправки. Если, однако, он опубликовал: "Сегодня утром Enron подал заявку на защиту банкротства главы 11", то это более полезное сообщение. Обратите внимание, что сообщение может привести к тому, что Dow Jones упадет на 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.

Ответ 4

Чтобы вам не пришлось жестко кодировать вызовы методов/функций, вы просто публикуете это событие, не заботясь о том, кто слушает. Это делает издателя независимым от абонента, уменьшая зависимость (или связь, независимо от того, какой вы предпочитаете) между двумя различными частями приложения.

Вот некоторые недостатки сцепления, упомянутые wikipedia

Плотно связанные системы, как правило, демонстрируют следующее развитие характеристики, которые часто считаются недостатками:

  • Изменение одного модуля обычно вызывает эффект пульсации изменений в других модулях.
  • Сборка модулей может потребовать больше усилий и/или времени из-за увеличения межмодульной зависимости.
  • Определенный модуль может быть сложнее повторить и/или проверить, потому что должны быть включены зависимые модули.

Рассмотрим нечто вроде объекта, инкапсулирующего бизнес-данные. Он имеет жестко закодированный метод вызов обновления страницы при достижении возраста:

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

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

//Different module

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

Теперь я не могу проверить объект person, не включая функцию showAge. Также, если мне нужно показать возраст в каком-то другом графическом интерфейсе, мне нужно жестко запрограммировать вызов этого метода .setAge, и теперь существуют зависимости для 2 несвязанных модулей в объекте person. Это также просто трудно поддерживать, когда вы видите, что эти вызовы сделаны, и они даже не в одном файле.

Обратите внимание, что внутри одного и того же модуля вы можете, конечно, иметь прямые вызовы методов. Но бизнес-данные и поверхностные поведение gui не должно находиться в одном модуле по любым разумным стандартам.

Ответ 5

Реализация PubSub обычно встречается там, где есть -

  • Существует такой портлет, как реализация, где имеется несколько портлетов, которые обмениваются данными с помощью шины событий. Это помогает создавать в архитектуре aync.
  • В системе, омраченной жесткой связью, 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.