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


103

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

Ответы:


222

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

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

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

Модули Twitter

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


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

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

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

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

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


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

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

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

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

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.


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

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

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

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


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

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

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

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

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

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.

1

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

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

0

Простой ответ Первоначальный вопрос требовал простого ответа. Вот моя попытка.

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

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

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.