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