Чому можна використовувати шаблон публікації / підписки (в 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 )

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 для фрагментів коду, як мої приклади вище?

Відповіді:


222

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

Вільне з'єднання - це об'єктно-орієнтований принцип, в якому кожен компонент системи знає свою відповідальність і не піклується про інші компоненти (або, принаймні, намагається не піклуватися про них якомога більше). Вільна муфта - це гарна річ, тому що ви можете легко використовувати різні модулі. Ви не поєднані з інтерфейсами інших модулів. Використовуючи опублікувати / підписатися, ви поєднані лише з інтерфейсом опублікувати / підписатися, що не є великою справою - лише два способи. Отже, якщо ви вирішили повторно використовувати модуль в іншому проекті, ви можете просто скопіювати і вставити його, і він, ймовірно, спрацює або, принаймні, вам не потрібно буде багато зусиль, щоб він працював.

Говорячи про вільну зв'язок, ми повинні згадати про відокремлення проблем. Якщо ви будуєте додаток, використовуючи архітектурний зразок MV *, у вас завжди є Моделі (и) та Вид (и). Модель є діловою частиною програми. Ви можете використовувати його повторно в різних додатках, тому не годиться поєднувати його з представленням однієї програми, де ви хочете її показати, оскільки зазвичай в різних додатках ви різні види. Тож корисно використовувати публікацію / підписку для зв’язку Model-View. Коли ваша Модель змінюється, вона публікує подію, 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 *, ви можете скористатися чимось дещо іншим (є перетин між тим, що я опишу наступним, та останнім згаданим). Просто структуруйте свою програму в різних модулях. Наприклад, подивіться у 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);
           });
       }
   };
}());

Про такий підхід чудова розмова Миколи Закаса . Для підходу MV * найкращі статті, які мені відомі, опубліковані Адді Османі .

Недоліки: Ви повинні бути обережними щодо надмірного використання публікації / підписки. Якщо у вас сотні подій, керувати ними всім може стати дуже заплутано. У вас можуть виникнути зіткнення, якщо ви не використовуєте простір імен (або не використовуєте його правильно). Розширену реалізацію Mediator, яка схожа на публікацію / підписку, можна знайти тут https://github.com/ajacksified/Mediator.js . У ньому є простір імен та такі функції, як "барботаж" подій, які, звичайно, можуть бути перервані. Ще одним недоліком публікації / підписки є тестування жорстких модулів, можливо, важко виділити різні функції в модулях і перевірити їх самостійно.


3
Дякую, це має сенс. Мені знайомий шаблон MVC, оскільки я його весь час використовую з PHP, але я не думав про це з точки зору програмування, керованого подіями. :)
Маккат

2
Дякую за цей опис Дійсно допомогла мені обернути голову навколо концепції.
flybear

1
Це відмінна відповідь. Не міг не зупинити себе на цьому голосуванні :)
Naveed Butt

1
Прекрасне пояснення, кілька прикладів, подальше читання пропозицій. A ++.
Карсон

16

Основна мета - зменшити зв’язок між кодом. Це дещо заснований на подіях спосіб мислення, але "події" не прив'язані до конкретного об'єкта.

Нижче я викладу великий приклад у псевдокоді, який трохи схожий на 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);
        }

    }

}

Ми могли б зробити це ще краще, створивши інтерфейс, як-от "SignalListener", так що нам потрібен лише один список у радіо-класі, і завжди можна викликати ту саму функцію на будь-якому об’єкті, який ми хочемо слухати сигнал. Але це все ще створює зв'язок між тим, яким інтерфейсом / базовим класом / тощо ми вирішимо, і радіокласом. В основному, коли ви змінюєте один із класів радіо, сигналу чи ретрансляції, вам потрібно задуматися про те, як це могло вплинути на два інші класи.

Тепер спробуємо щось інше. Створимо четвертий клас під назвою 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 class, будучи видавцем, знає про RadioMast, який він публікує сигнали. Це сенс використання такої системи передачі повідомлень, як публікація / підписка.


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

1
Ласкаво просимо! Особисто я часто виявляю, що мій мозок не «клацає», коли йдеться про нові зразки / методології, поки я не усвідомив актуальну проблему, яку він вирішує для мене. Модель суб / паб чудово підходить для архітектурних конструкцій, які тісно поєднані концептуально, але ми все ще хочемо тримати їх якнайбільше відокремлених. Уявіть гру, де у вас є сотні об'єктів, на які всі повинні реагувати, наприклад, на речі, що відбуваються навколо них, і з цих об'єктів може бути все: гравець, куля, дерево, геометрія, gui тощо тощо
Anders Arpi

3
У JavaScript немає classключового слова. Будь ласка, підкресліть цей факт, напр. класифікувавши свій код як псевдо-код.
Роб Ш

Насправді в ES6 є ключове слово.
Мінько Гечев

5

Інші відповіді зробили чудову роботу, показуючи, як працює візерунок. Я хотів би вирішити питання, що мається на увазі, " що старого не так? ", Оскільки я працював з цією схемою останнім часом, і я вважаю, що це змінює міркування.

Уявіть, що ми підписалися на економічний бюлетень. Бюлетень публікує заголовок: " Опустіть Доу Джонса на 200 балів ". Це було б дивне і дещо безвідповідальне повідомлення. Якщо він опублікував: " Енрон подав заяву про захист банкрутства глави 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()це буде виконано?
Брайан П

4

Так що вам не доведеться жорстко кодувати виклики методу / функції, ви просто публікуєте подію, не піклуючись про те, хто слухає. Це робить видавця незалежним від передплатників, зменшуючи залежність (або з'єднання, який би термін ви не хочете) між двома різними частинами програми.

Ось деякі недоліки з'єднання, про які згадує wikipedia

Щільно пов'язані системи мають тенденцію проявляти такі характеристики розвитку, які часто розглядаються як недоліки:

  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функцію. Крім того, якщо мені потрібно показати вік і в іншому модулі GUI, мені потрібно ввести жорсткий код цього методу .setAge, і тепер в об'єкті людини є залежності для 2-х непов'язаних модулів. Це також важко підтримувати, коли ви бачите ці дзвінки, і вони навіть не в одному файлі.

Зауважте, що всередині одного модуля ви, звичайно, можете мати прямі виклики методів. Але ділові дані та поверхнева поведінка гі не повинні міститись в одному модулі за будь-якими розумними стандартами.


Я не розумію тут поняття "залежність"; де залежність у моєму другому прикладі, а де вона відсутня в моєму третьому? Я не бачу жодної практичної різниці між моїм другим та третім фрагментами - це, здається, додає новий "шар" між функцією та подією без справжньої причини. Я, мабуть, сліпий, але думаю, що мені потрібно більше покажчиків. :(
Маккат

1
Чи можете ви надати зразок використання зразка, коли публікація / підписка була б більш доречною, ніж просто виготовлення функції, яка виконує те саме?
Jeffrey Sweeney

@Maccath Простіше кажучи: у третьому прикладі ви не знаєте і не повинні знати, що воно removeOrderнавіть існує, тому ви не можете бути залежними від нього. У другому прикладі ви повинні знати.
Есаїлія

Хоча я все ще відчуваю, що існують кращі способи піти над тим, що ви описали тут, я, принаймні, переконаний, що ця методологія має призначення, особливо в середовищі з багатьма іншими розробниками. +1
Jeffrey Sweeney

1
@Esailija - Дякую, я думаю, я розумію трохи краще. Отже ... якби я повністю видалив абонента, він не помилився б і нічого, він просто нічого не зробить? І чи можете ви сказати, що це може бути корисним у випадку, коли ви хочете виконати дію, але не обов'язково знаєте, яка функція є найбільш актуальною на момент публікації, але підписник може змінитися залежно від інших факторів?
Маккат

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 не забезпечує жодного механізму для об'єктів коду для створення власних подій. Тож вам потрібен своєрідний механізм подій. Шаблон публікації / підписки відповість на цю потребу, і ви вирішите вибрати механізм, який найкраще відповідає вашим власним потребам.

Тепер ми можемо побачити потребу в паб / під-шаблоні. Тоді вам доведеться обробляти події DOM по-іншому, ніж обробляти паб / підподії? Для зменшення складності та інших понять, таких як розділення проблем (SoC), ви можете побачити користь у тому, що все є рівномірним.

Парадоксально, але більше коду створює кращий поділ проблем, який масштабується до дуже складних веб-сторінок.

Я сподіваюся, що хтось вважає це досить гарним обговоренням, не вдаючись до деталей.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.