Вы встречали использование 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 извне объекта-эмиттера.

Если остались вопросы, то пишите их в комментариях.