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

avatar
Maccath
22 ноября 2012 в 12:39
70281
7
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 для фрагментов кода, подобных моим примерам выше?

Источник

Ответы (7)

avatar
Minko Gechev
22 ноября 2012 в 13:38
226

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

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

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

Если вы посмотрите на интерфейс, у вас просто разные поля. Вы можете рассматривать каждую коробку как отдельный модуль. Например, вы можете опубликовать твит. Это действие требует обновления нескольких модулей. Во-первых, он должен обновить данные вашего профиля (верхнее левое поле), но он также должен обновить вашу временную шкалу. Конечно, вы можете сохранять ссылки на оба модуля и обновлять их отдельно, используя их общедоступный интерфейс, но проще (и лучше) просто опубликовать событие. Это упростит модификацию вашего приложения из-за более слабой связи. Если вы разрабатываете новый модуль, который зависит от новых твитов, вы можете просто подписаться на событие «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 *, то лучшие статьи и книги, которые я знаю, опубликованы Адди Османи.

Недостатки: вы должны быть осторожны с чрезмерным использованием публикации / подписки. Если у вас есть сотни мероприятий, управлять всеми ими может быть очень сложно. У вас также могут быть конфликты, если вы не используете пространство имен (или используете его неправильно). Расширенную реализацию Mediator, которая очень похожа на публикацию / подписку, можно найти здесь https://github.com/ajacksified/Mediator.js. У него есть пространство имен и такие функции, как «всплытие» событий, которые, конечно, можно прервать. Еще один недостаток публикации / подписки - жесткое модульное тестирование, может стать трудным изолировать различные функции в модулях и тестировать их независимо.

Maccath
22 ноября 2012 в 13:44
3

Спасибо, в этом есть смысл. Я знаком с шаблоном MVC, так как все время использую его с PHP, но я не думал об этом с точки зрения программирования, управляемого событиями. :)

kyler
4 февраля 2015 в 15:39
2

Спасибо за это описание. Действительно помогло мне осмыслить эту концепцию.

Naveed Butt
14 мая 2015 в 04:19
1

Это отличный ответ. Не мог удержаться и проголосовал за это :)

Carson
16 декабря 2015 в 20:15
1

Отличное объяснение, несколько примеров, предложения по дальнейшему чтению. А ++.

avatar
Simon Miller
24 июля 2018 в 07:27
0

Простой ответ Исходный вопрос искал простой ответ. Вот моя попытка.

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

Теперь мы видим потребность в шаблоне pub / sub, тогда вы бы предпочли обрабатывать события DOM иначе, чем то, как вы обрабатываете события pub / sub? Для уменьшения сложности и других концепций, таких как разделение ответственности (SoC), вы можете увидеть преимущества единообразия всего.

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

Надеюсь, кто-то сочтет это обсуждение достаточно хорошим, не вдаваясь в подробности.

avatar
Peheje
30 марта 2018 в 19:09
1

Статья «Многоликая публикация / подписка» - хорошее прочтение, и они подчеркивают одну вещь, которую они подчеркивают, - разделение в трех «измерениях». Вот мое грубое резюме, но, пожалуйста, также обратитесь к статье.

  1. Разделение пространства. Взаимодействующим сторонам не нужно знать друг друга. Издатель не знает, кто слушает, сколько слушает и что они делают с событием. Абоненты не знают, кто продюсирует эти события, сколько продюсеров и т. Д.
  2. Развязка по времени. Взаимодействующие стороны не должны быть активными одновременно во время взаимодействия. Например, подписчик может быть отключен, пока издатель публикует некоторые события, но он может реагировать на него, когда он подключается к сети.
  3. Разделение синхронизации. Издатели не блокируются при создании событий, и подписчики могут асинхронно получать уведомления через обратные вызовы всякий раз, когда наступает событие, на которое они подписались.
avatar
user2756335
23 апреля 2017 в 17:41
1

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

  1. Существует реализация, похожая на портлет, в которой несколько портлетов обмениваются данными с помощью шины событий. Это помогает создавать в архитектуре aync.
  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.
avatar
Trevedhek
24 сентября 2015 в 21:06
5

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

Представьте, что мы подписались на экономический бюллетень. В бюллетене публикуется заголовок: « Понизить 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.

Bryan P
21 января 2016 в 11:16
0

если бы ваши последние 2 функции (remindUserToFloss и increaseProgrammerBrowniePoints) были расположены в отдельных модулях, вы бы опубликовали два события одно за другим прямо здесь, в handleRemoveOrderRequest, или вы бы flossModule опубликовали событие в browniePoints модуль, когда remindUserToFloss() выполнен?

avatar
Anders Arpi
22 ноября 2012 в 13:29
16

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

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

Maccath
22 ноября 2012 в 13:40
0

Действительно здорово иметь конкретный пример, который показывает, как реализация шаблона pub / sub может быть лучше, чем использование «обычных» методов! Спасибо!

Anders Arpi
22 ноября 2012 в 13:46
1

Пожалуйста! Лично я часто замечаю, что мой мозг не «щелкает», когда дело доходит до новых шаблонов / методологий, пока я не осознаю реальную проблему, которую он решает за меня. Шаблон sub / pub отлично подходит для архитектур, которые концептуально тесно связаны, но мы все же хотим, чтобы они были разделены как можно больше. Представьте себе игру, в которой у вас есть сотни объектов, которые должны реагировать, например, на происходящее вокруг них, и эти объекты могут быть чем угодно: игроком, пулей, деревом, геометрией, графическим интерфейсом и т. Д. И т. Д.

Rob W
22 ноября 2012 в 14:17
3

В JavaScript нет ключевого слова class. Пожалуйста, подчеркните этот факт, например. классифицируя ваш код как псевдокод.

Minko Gechev
22 ноября 2012 в 14:22
0

На самом деле в ES6 есть ключевое слово class.

avatar
Esailija
22 ноября 2012 в 12:50
4

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

Вот некоторые недостатки связывания, упомянутые в википедии

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

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

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

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

Maccath
22 ноября 2012 в 12:55
0

Я не понимаю здесь концепции «зависимости»; где зависимость в моем втором примере, а где она отсутствует в моем третьем? Я не вижу никакой практической разницы между моими вторым и третьим фрагментами - кажется, что он просто добавляет новый «слой» между функцией и событием без реальной причины. Я, наверное, слепой, но думаю, мне нужно больше указателей. :(

Jeffrey Sweeney
22 ноября 2012 в 12:56
1

Не могли бы вы представить пример использования, в котором публикация / подписка была бы более подходящей, чем просто создание функции, выполняющей то же самое?

Esailija
22 ноября 2012 в 13:13
0

@Maccath Проще говоря: в третьем примере вы не знаете или не должны знать, что removeOrder вообще существует, поэтому вы не можете зависеть от него. Во втором примере вы должны знать.

Jeffrey Sweeney
22 ноября 2012 в 13:16
0

Хотя я все еще чувствую, что есть более эффективные способы реализовать то, что вы здесь описали, я, по крайней мере, убежден, что эта методология имеет цель, особенно в средах с большим количеством других разработчиков. +1

Esailija
22 ноября 2012 в 13:19
0

Такая передача сообщений @JeffreySweeney обеспечивает минимально возможное количество связей, при этом модули все еще взаимодействуют друг с другом. Какие лучшие способы вы думаете?

Maccath
22 ноября 2012 в 13:24
1

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

Esailija
22 ноября 2012 в 13:29
0

@Maccath, подписчику все равно, откуда идет публикация и идет ли она вообще. И издателю все равно, есть ли подписчики. Так что да, ничего бы не случилось. И да, по вашему второму пункту.