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