# Publish/subscribe в Javascript

poster
В этом видео мы разберем такой популярный и часто используемый паттерн, как pubSub или publish/subscribe.
Понравилось? Поделитесь с друзьями!
Понравилось?
Поделитесь с друзьями!
Комментарии
Текст видео

В этом видео мы разберем такой популярный и часто используемый паттерн, как pubSub или publish/subscribe.

Какую проблему решает pubsub?

Представим, что у нас есть онлайн магазин, который при новом заказе посылает емейл юзеру. Давайте напишем простую реализацию.

Мы хотим создать класс Order, чтобы мы могли создавать заказ и сохранять его.

const order = new Order({userEmail: 'john@gmail.com'})
order.save()

Давайте добавим класс Order

class Order {
  constructor (params) {
    this.params = params
  }

  save () {
    console.log('Order saved')
    this.sendEmail(this.params)
  }

  sendEmail () {
    const mailer = new Mailer()
    mailer.sendPurchaseEmail(this.params)
  }
}

Мы сохраняем параметры в this и при вызове save отправляем емейл. Емейл мы отправляем с помощью другого класса Mailer.

Давайте добавим его сейчас.

class Mailer {
  sendPurchaseEmail (params) {
    console.log(`Email send to ${params.userEmail}`)
  }
}

Если мы посмотрим в браузер, то мы получаем сообщение Order saved и Email send.

Какая же у нас есть здесь проблема? Классы Order и Mailer очень тесно связаны. Это обычно значит, что когда мы заходим обновить один класс, то нам прийдется обновлять и другой. Например, если мы захотим поменять имя sendPurchaseEmail, или его параметры, то нам прийдется менять класс Order тоже.

Поэтому всегда нужно стараться, чтобы компоненты не были тесно связаны.

Вторая проблема заключается в том, что если мы будем покрывать класс Order тестами, нам прийдется много чего мокать.

Такие проблемы решаются с помощью паттерна publish/subscribe. То есть, в приложении мы паблишим события без привязки к какому-то конкретному классу. И мы можем создавать подписчиков (subscribers), которые будут слушать события, которые им интересны.

Это позволяет не делать зависимости между компонентами системы.

Давайте сейчас создадим нашу pubsub систему и улучшим наши классы Order и Mailer.

Мы хотим реализовать вот такое использование нашего PubSub.

EventBus.subscribe('foo', () => console.log('foo fired'))
EventBus.publish('foo', 'Hello world')

То есть, сначала, с помощью subscribe мы подписались на ивент foo, и когда он выстрелит, то наш коллбек, который мы передали вторым параметром, выполнится. Потом мы выстрелили publish с ивентом foo и передали в параметрах текст Hello world.

Давайте теперь опишем EventBus.

const EventBus = {
  channels: {},
  subscribe (channelName, listener) {
    if (!this.channels[channelName]) {
      this.channels[channelName] = []
    }
    this.channels[channelName].push(listener)
  },

  publish (channelName, data) {
    const channel = this.channels[channelName]
    if (!channel || !channel.length) {
      return
    }

    channel.forEach(listener => listener(data))
  }
}
  1. Мы создали объект channels, в котором мы будем хранить наши каналы. Например, foo у нас будет каналом.
  2. В методе subscribe мы проверяем - есть ли канал и если нет - создаем. Потом, пушим в созданный канал новый listener.
  3. В publish мы проходимся по слушателям канала и вызываем их. Если канала или слушателей нет, то ничего не делаем.

Если мы посмотрим в браузер, то все работает.

Это самый простой вариант реализации pubsub. Если вы хотите посмотреть на готовые и более сложные реализации pubsub, то советую библиотеку PubSubJS.

Теперь давайте обновим наш пример с Order и Mailer.

class Order {
  constructor (params) {
    this.params = params
  }

  save () {
    console.log('Order saved')
    EventBus.publish('order/new', {
      userEmail: this.params.userEmail
    })
  }
}

class Mailer {
  constructor () {
    EventBus.subscribe('order/new', this.sendPurchaseEmail)
  }

  sendPurchaseEmail (params) {
    console.log(`Email send to ${params.userEmail}`)
  }
}

const mailer = new Mailer()
const order = new Order({userEmail: 'john@gmail.com'})
order.save()

Теперь мы создаем Mailer отдельно. При инициализации он подписывается на канал order/new и когда кто-то запаблишит событие, вызовет sendPurchaseEmail.

В Order мы полностью убрали зависимость от Mailer и просто паблишим событие order/new, передавая в него нужные данные.

Если мы посмотрим в браузер, то все работает, как и раньше, но теперь Mailer и Order никак не связаны, мы можем менять любой функционал внутри и ничего не поломать в другом классе.

Какие же плюсы у этого паттерна?

  1. Мы можем легко разбивать приложение на независимые части
  2. Мы можем легко реализовывать слабую связность

Какие же есть минусы у данного паттерна?

  1. Мы никогда не знаем, а подписан ли на нас кто-то. Поэтому, если в subscribe что-то отломалось, то паблиш никогда этого не узнает.
  2. Также, достаточно часто, в больших проектах количество паблишей и сабскрайбов разрастается и они становятся крайне запутанными. Начиная от того, что вам нужно пройти по цепочке из нескольких паблишей, чтобы добраться до реальной функции, которая что-то делает, и заканчивая циклической зависимостью, когда компоненты подписаны друг на друга и вы, проходя по ивентам, возвращаетесь в начальный класс.

Если у вас возникли какие-то вопросы или комментарии, пишите их прямо под этим видео.

Только зарегистрированные пользователи могут оставлять комментарии.  Войдите, пожалуйста.
Alex Panchuk
3 месяцев назад
Спасибо за видео)) Это видео не находится по запросу "паттерн". Может стоит видео про паттерны объеденить в серию?
monsterlessons
3 месяцев назад
Ого я думал они у меня давно в серию объединены. Спасибо, что написали.
Moe Green
4 месяцев назад
классно )
monsterlessons
4 месяцев назад
Спасибо
fil
5 месяцев назад
Как можно внутри функции подождать выполнения всех publish(если их там много), т.е. чтобы все subscribers отработали и потом что-то выполнить? Почему так происходит что publish все выполняются как будто асинхронно?
monsterlessons
5 месяцев назад
Потому что они вполне могут выполнятся асинхронно и это нормально. Чтобы подождать выполнения всех коллбеков, по нормальному, лучше использовать паттерн Aggregator. Вот картинка, чтобы было понятно, что он делает. http://www.enterpriseintegrationpatterns.com/patterns/messaging/Aggregator.html А по тупому можно как то так: let event1Happened = false let event2Happended = false EventBus.subscribe('event1', () => { event1Happened = true if (event1Happened && event2Happened) { console.log('all passed') } }) EventBus.subscribe('event2', () => { event2Happened = true if (event1Happened && event2Happened) { console.log('all passed') } })
fil
5 месяцев назад
А как понять будет выполняться код асинхронно или синхронно?
monsterlessons
5 месяцев назад
Весь код, написанный внутри синхронных функций будет выполнятся мгновенно, внутри промисов, http запросов и setTimeoutов будет асинхронно.
fil
5 месяцев назад
есть так: for (a of b) { publish... publish... publish... } console.log(...) Последний console.log выполняется быстрее чем в console.log в subscribe-ах. никакого асинхронного кода нет...
monsterlessons
5 месяцев назад
Вот пример. У меня сначала выводится 10 консоль логов, а потом лог после for. https://jsfiddle.net/wncc5nm2/
fil
5 месяцев назад
если используется loop foreach вызов колбаков не гарантированно последовательно. Т.е. есть 10й колбак может закончится быстрее первого? значит асинхронность появляется не только внутри промисов и таймаутов...?
monsterlessons
5 месяцев назад
Вы можете сделать пример на jsfiddle? У меня с foreach все работает. https://jsfiddle.net/wncc5nm2/1/
fil
5 месяцев назад
работает потому что нагрузка не та... У меня 20 подписчиков и цикл на 10000. - обработка лога из Asterisk. И наблюдается такая ситуация. А вот еще - пользуюсь библиотекой pubsub-js. может в ней что-то не так.
fil
5 месяцев назад
И вопрос про асинхронность остался. если указан последовательный вызов функций то гарантирован порядок их выполнения или нет? заметил что если указывать так: a = f1(parameters); b = f2(...); .... то всегда последовательно выполняются. если так: (не pure): f3(); f4(); ... то порядок может нарушаться.... Так ли это?
monsterlessons
5 месяцев назад
Если бы вы уточнили библиотеку сразу же, то было бы намного проще понять вашу проблему. https://github.com/mroderick/PubSubJS/blob/master/src/pubsub.js#L124 Вот исходный код pubsubjs. Там если не указан параметр sync, заворачивается в setTimeout.
Sergey Illarionov
9 месяцев назад
Почему EventBus реализован объектом, а не классом, скажем?
monsterlessons
9 месяцев назад
Потому что из него не нужно создавать экземпляры класса. У нас всегда только один обьект с которым мы работает. Но вы, конечно, могли бы сделать его классом и просто написать eventBus = new EventBus() но он у вас все равно один на все приложение. Как вариант можно было сделать его синглтоном.
Sergey Illarionov
9 месяцев назад
Про синглтон отдельно ждем видео.
monsterlessons
9 месяцев назад
Есть же уже https://monsterlessons.com/project/lessons/singleton-pattiern-v-javascript
Sergey Illarionov
9 месяцев назад
Идеально, спасибо.
Galeups
9 месяцев назад
Спасибо большое. Как раз начал интересоваться паттернами JS.
monsterlessons
9 месяцев назад
На здоровье
Sergey Illarionov
9 месяцев назад
Спасибо за видео. И очень надеюсь, что предполагается серия видео о о иных дизайн-паттернах в JS, таких как: Decorator, Factory и т.д.
monsterlessons
9 месяцев назад
На здоровье. Да, будет снята вся серия.
Sergey Illarionov
9 месяцев назад
Заранее ликую и жду выпусков!
winlx
9 месяцев назад
Серия по паттернам, как раз то что искал.
monsterlessons
9 месяцев назад
Все будет по обычному графику вт и суббота, за исключением отпуска.