# 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
8 месяцев назад
Спасибо за видео)) Это видео не находится по запросу "паттерн". Может стоит видео про паттерны объеденить в серию?
monsterlessons
8 месяцев назад
Ого я думал они у меня давно в серию объединены. Спасибо, что написали.
Максим Титяев
4 месяцев назад
Такой серии до сих пор нет((
Максим Титяев
4 месяцев назад
всего в сериях 12 плейлистов, если что..
monsterlessons
4 месяцев назад
К сожалению, я пока занимаюсь другим проектом, так что это проект я не обновляю, а только отвечаю на комментарии.
Максим Титяев
4 месяцев назад
Ок, я подумал, что может быть повторно что-то отвалилось. А другой проект тоже обучающий? У вас классные уроки!
monsterlessons
4 месяцев назад
Спасибо. Другой проект - учет финансов для Европы.
Максим Титяев
4 месяцев назад
Хотел бы узнать. Вы когда приложение какое нибудь разрабатываете, вы сначала пишете подробный план? Всегда ли? я сейчас пытаюсь на Vue.js кое-что сделать, но с наскока не вышло, остановился((. Думаю может мне сначала продумать всё, расписать подробно задачи и только потом приступать. Как это делается? Может знаете какое-то видео или книгу на русском. Или может есть пример проекта с планом действий?
monsterlessons
4 месяцев назад
Видео и книг точно нет. Все приходит с опытом. Если опыта не очень много, то можно просто кодить и потом рефакторить, когда что-то новое узнал/попробовал. Очень сильно помогает улучшать код, но реализацию готового проекта не приблизит, конечно. При разработке проекта: 1. Пишутся рекваерменты 2. MVP - список фич для прототипа 3. Выбираются подходящие технологии 4. Делаются эпики(пачка фич), бьются на спринты, эстимейтятся, заносятся в трекер 5. Делаем фичи по спринту 6. Релиз Как то так
Максим Титяев
4 месяцев назад
а каждая отдельная фича, начинается с псевдокода?из головы прям код не льётся? Глядя на различные видео уроки складывается впечатление что всё пишется без подготовки, легко, экспромтом. У меня методом проб и ошибок, идеи приходят после, приходится переделывать.
monsterlessons
4 месяцев назад
Это нормально. Пока все популярные задачи не решаешь по 50 раз, то это всегда пробы, ошибки и рефакторинг. P.S. 10минутное видео готовится от 2 часов и больше.
Максим Титяев
4 месяцев назад
Спасибо за поддержку!
Максим Титяев
4 месяцев назад
Ведь оценить себя можно сравнивая с другими, а так как я не в коллективе прогеров, то сравнивать остаётся с авторами видео уроков. А это часто больно.
monsterlessons
4 месяцев назад
Вот и не надо сравнивать. Ходите по собесам - понимайте, что компаниям нужно. Взяли на работу - фигачте код и все. P.S. хуже может быть только смотреть список новых технологий, которые появляются каждый день и думать "наверно это все тоже нужно учить, ведь люди уже на этом пишут".
Moe Green
9 месяцев назад
классно )
monsterlessons
9 месяцев назад
Спасибо
fil
10 месяцев назад
Как можно внутри функции подождать выполнения всех publish(если их там много), т.е. чтобы все subscribers отработали и потом что-то выполнить? Почему так происходит что publish все выполняются как будто асинхронно?
monsterlessons
10 месяцев назад
Потому что они вполне могут выполнятся асинхронно и это нормально. Чтобы подождать выполнения всех коллбеков, по нормальному, лучше использовать паттерн 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
10 месяцев назад
А как понять будет выполняться код асинхронно или синхронно?
monsterlessons
10 месяцев назад
Весь код, написанный внутри синхронных функций будет выполнятся мгновенно, внутри промисов, http запросов и setTimeoutов будет асинхронно.
fil
10 месяцев назад
есть так: for (a of b) { publish... publish... publish... } console.log(...) Последний console.log выполняется быстрее чем в console.log в subscribe-ах. никакого асинхронного кода нет...
monsterlessons
10 месяцев назад
Вот пример. У меня сначала выводится 10 консоль логов, а потом лог после for. https://jsfiddle.net/wncc5nm2/
fil
10 месяцев назад
если используется loop foreach вызов колбаков не гарантированно последовательно. Т.е. есть 10й колбак может закончится быстрее первого? значит асинхронность появляется не только внутри промисов и таймаутов...?
monsterlessons
10 месяцев назад
Вы можете сделать пример на jsfiddle? У меня с foreach все работает. https://jsfiddle.net/wncc5nm2/1/
fil
10 месяцев назад
работает потому что нагрузка не та... У меня 20 подписчиков и цикл на 10000. - обработка лога из Asterisk. И наблюдается такая ситуация. А вот еще - пользуюсь библиотекой pubsub-js. может в ней что-то не так.
fil
10 месяцев назад
И вопрос про асинхронность остался. если указан последовательный вызов функций то гарантирован порядок их выполнения или нет? заметил что если указывать так: a = f1(parameters); b = f2(...); .... то всегда последовательно выполняются. если так: (не pure): f3(); f4(); ... то порядок может нарушаться.... Так ли это?
monsterlessons
10 месяцев назад
Если бы вы уточнили библиотеку сразу же, то было бы намного проще понять вашу проблему. https://github.com/mroderick/PubSubJS/blob/master/src/pubsub.js#L124 Вот исходный код pubsubjs. Там если не указан параметр sync, заворачивается в setTimeout.
Sergey Illarionov
1год назад назад
Почему EventBus реализован объектом, а не классом, скажем?
monsterlessons
1год назад назад
Потому что из него не нужно создавать экземпляры класса. У нас всегда только один обьект с которым мы работает. Но вы, конечно, могли бы сделать его классом и просто написать eventBus = new EventBus() но он у вас все равно один на все приложение. Как вариант можно было сделать его синглтоном.
Sergey Illarionov
1год назад назад
Про синглтон отдельно ждем видео.
monsterlessons
1год назад назад
Есть же уже https://monsterlessons.com/project/lessons/singleton-pattiern-v-javascript
Sergey Illarionov
1год назад назад
Идеально, спасибо.
Galeups
1год назад назад
Спасибо большое. Как раз начал интересоваться паттернами JS.
monsterlessons
1год назад назад
На здоровье
Sergey Illarionov
1год назад назад
Спасибо за видео. И очень надеюсь, что предполагается серия видео о о иных дизайн-паттернах в JS, таких как: Decorator, Factory и т.д.
monsterlessons
1год назад назад
На здоровье. Да, будет снята вся серия.
Sergey Illarionov
1год назад назад
Заранее ликую и жду выпусков!
winlx
1год назад назад
Серия по паттернам, как раз то что искал.
monsterlessons
1год назад назад
Все будет по обычному графику вт и суббота, за исключением отпуска.