Вы встречали использование EventEmitter
в node.js или в других языках при котором некоторые сущности подписываются
на события объекта-эмиттера и сами же инициируют эмитинг событий через тот же объект-эмиттер? Или же класс
наследующий EventEmitter
вызывает this.emit('event', ...)
и сам подписывается на прослушивание
this.on('event', ...)
? Стоит ли так делать? Ниже вы можете прочесть мои мысли на эту тему, я попытаюсь
объяснить границы применимости этих подходов.
Когда я встретил некорректное использование в реальном коде, то первым делом посмотрел что говорит о подобном кейсе Фаулера, GoF, да и интернет в целом. Отсылка идет к паттерну Observer, а в описании паттерна нет ни слова об этой теме. Если я ошибаюсь, то оставьте в комментариях цитаты и ссылки, добавлю в статью.
Еще отмечу, что я публиковал ранее статью о некорректном использовании EventEmitter в node.js. Она более техническая и призвана помочь в выборе архитектуры и изежать утечек памяти.
Начнем с разбора названия паттерна: event emitter, emit, излучать, излучатель событий… Название уже подразумевает что происходит некоторое событие и происходит одностороннее “излучение” этого события. При этом это инициатива самого объекта-излучателя, он делает это независимо от наличия наблюдателей. Наблюдатели же (подписчики), которых может быть большое количество, следят за этими событиями.
Начнем мы с примера из реального мира, а затем приведем пример кода.
Пример с будильником: контрпример двунаправленного общения
Когда утром у человека звонит будильник, то есть два варианта его заткнуть:
- физически осуществить действие и выключить его
- при наличии голосового управления дать команду на выключение
В первом случае имеется действие, а не событие. Будильник не наблюдает за хозяином, не знает проснулся ли он, не следит за уровнем его гнева.
Во втором случае будильник становится наблюдателем за объектом “человек” и слушает команду. Это тот же паттерн event emitter, но не хозяин инициирует отправку события в будильник. Хозяин не делает ничего, только орет на будильник, а тот подписывается на “человека” и слушает его.
Будильников может быть несколько, но человек в первом примере осуществляет действие над каждым из них, а во втором случае дает одну команду и все “подписанные” будильники реагируют одновременно.
Данный пример является типичной зоной применения паттерна Observer.
Пример с кодом
Разбирать будем следующий пример:
const {EventEmitter} = require('events');
const SubProcess = require('somepath/SubProcess');
class Processor extends EventEmitter {
constructor() {
this.subProcess = new SubProcess();
this._processData = this._processData.bind(this);
}
start() {
this.subProcess.on('data', this._processData);
this.on('stop', () => {
this.subprocess.emit('stop');
});
this.subProcess.start();
}
_processData(data) {
// some actions that generates processedData
this.emit('processedData', processedData);
}
}
const taskProcessor = new Processor();
taskProcessor.on('processedData', (processedData) => {
console.log(processedData);
});
process.on('SIGTERM', () => {
taskProcessor.emit('stop');
});
В коде выше объект класса Processor
генерирурет события (метод _processData
) и подписывается на события
брошенные самому классу извне (this.on('stop', ...)
в методе start
). Это и есть двунаправленное общение.
process
в примере выше является излучателем уведомляющем о желании пользователя или системы повлиять на процесс, а
программа следит за этим и осуществляет действия над сущностями которые она же и породила. Нарушений логики еще нет.
Нарушение происходит в момент вызова taskProcessor.emit('stop')
. По бизнес логике приложения происходит инициирование
действия которое должно привести к остановке, но:
- эмиттинг события
stop
предполагает доставку уведомления только этому обьектуtaskProcessor
и больше никому - подобный подход провоцирует к реализации эмитинга еще одного события уведомляющего того кто заэмитил
stop
об успехе/неуспехе выполнения команды.
В обоих случаях нет того ради чего паттерн создавался: уведомления от одного ко многим. Теперь приходим к тому, что событие не равно действие. Код стоит переписать следующим образом:
class Processor extends EventEmitter {
// ...
start() {
this.subProcess.on('data', this._processData);
this.subProcess.start();
}
stop() {
this.subprocess.emit('stop'); // да-да, стоит отрефакторить и код в этом классе
}
// ...
}
process.on('SIGTERM', () => {
taskProcessor.stop();
});
В таком коде метод stop
может вернуть Promise
, может событие об остановке сгенерировать, а заинтересованные
сущности могут подписаться на уведомление об успешной остановке если их несколько.
Резюме
Из всего вышесказанного дам следующие рекомендации:
- Постоянно анализируйте требуется ли реализовать действие или уведомление.
- Никогда не реализовывайте действие посредством паттерна
Event Emitter
. - Никогда не вызывайте emit извне объекта-эмиттера.
Если остались вопросы, то пишите их в комментариях.