JavaScript

Шаблоны проектирования JavaScript, часть 2: Шаблон Publisher/Subscriber

Spread the love

В первой части этой серии мы рассмотрели фабричный шаблон, который мы описали как порождающий паттерн, целью которого было позволить нам легко создавать объекты, абстрагируясь от деталей их реализации. В этой статье мы рассмотрим первый из поведенческих паттернов, которые мы рассмотрим в этой серии, который называется паттерном Publisher/Subscriber (Издатель/Подписчик).

Оригинальная статья: Babs CraigJavaScript Design Patterns Part 2: The Publisher/Subscriber Pattern

Как всегда, код для этой серии можно найти на Github.

Шаблон Publisher/Subscriber, или сокращенно «PubSub», — это шаблон, который позволяет нам создавать модули, которые могут взаимодействовать друг с другом без прямой зависимости друг от друга. Это отличный шаблон для разделение на части нашего приложения, и он довольно распространен в JavaScript.

Предостережение — используйте этот шаблон с умом, так как c ним легко переусердствовать. Обычно достаточно создать небольшое количество связей для того, чтобы получить читаемый и обслуживаемый код, но при увеличение абстрагированности ваш код может легко разрастись и усложниться до такой степени, что его станет трудно обслуживать.

Шаблон PubSub в действии

В папку javascript-design-patterns, которую мы создали в прошлый раз, добавьте новую папку с именем pubsub.

Начнем с нашего модуля pubsub. Создайте файл с именем pubsub.js и добавьте следующий код:

let subscribers = {};

module.exports = {
    publish() {
        // method to publish an update
    },
    subscribe() {
        // method to subscribe to an update
    }
};

Прежде всего, мы создаем объект, который мы назовем subscribers (подписчиками), чтобы отслеживать обратные вызовы зарегистрированных подписчиков. Внутри этого объекта мы в конечном итоге будем хранить пары ключ/значение событий. Каждое событие будет иметь ключ, соответствующий имени события, и значение, установленное для массива. В этом массиве мы будем регистрировать/хранить обратные вызовы подписчиков. Эти обратные вызовы вызываются всякий раз, когда произойдет наблюдаемое событие, и мы можем запустить несколько таких обратных вызовов для любого данного события.

Затем модуль pubsub экспортирует две функции. Одну publish для публикации обновления, а другую для subscribe подписки на обновления.

Это базовая основа нашего модуля издателя/подписчика.

Давайте сначала сосредоточимся на методе subscribe, поскольку этот метод будет использоваться другими модулями для регистрации обратного вызова подписчика.

Наш метод subscribe будет принимать два аргумента. Первый — это имя события, на которое подписывается, а второй — обратный вызов, который вызывается при публикации события.

subscribe(event, callback) {
    if (!subscribers[event]) {
        subscribers[event] = [];
    }    
    subscribers[event].push(callback);
}

В приведенном выше фрагменте кода мы сначала проверяем, было ли указанное событие зарегистрировано в объекте subscribers. Если событие не существует в объекте subscribers, мы знаем, что оно не было зарегистрировано, и поэтому мы регистрируем его, используя имя события в качестве ключа, и инициализируем значение пустым массивом. Наконец, мы вставим обратный вызов подписчика в массив событий.

Далее метод publish:

publish(event, data) {
    if (!subscribers[event]) return;
    subscribers[event].forEach(subscriberCallback =>
        subscriberCallback(data));
}

Сначала мы проверяем, есть ли подписчики, которые зарегистрировались. Если нет, мы уходим из метода publish, так как у нас нет подписчиков с обратными вызовами для вызова. Если есть подписчики, мы перебираем массив событий и вызываем каждый обратный вызов подписчика, который был помещен в массив событий. Мы также передаем любые данные, которые могли быть предоставлены для каждого из обратных вызовов.

Обратите внимание, что параметр данных является необязательным.

Теперь мы можем создавать наши модули, которые будут использовать объект pubsub. Добавим два новых файла с именами moduleA.js и moduleB.js соответственно.

В этом примере moduleA будет издателем, а moduleB будет подписчиком. Как мы увидим, оба модуля не будут знать друг друга и будут общаться только через модуль pubsub.

moduleA.js:

const pubSub = require("./pubsub");

module.exports = {
    publishEvent() {
        const data = {
            msg: "TOP SECRET DATA"
        };
        
        pubSub.publish("anEvent", data);
    }
};

Здесь нам требуется модуль pubSub, и мы экспортируем объект с помощью метода publishEvent. Метод указывает некоторые данные, а затем вызывает метод pubSub.publish, передавая имя публикуемого события и созданный нами объект данных.

moduleB.js:

const pubSub = require("./pubsub");

pubSub.subscribe("anEvent", data => {
    console.log(
        `"anEvent", was published with this data: "${data.msg}"`
    );
});

Код подписчика более лаконичен. Здесь мы импортируем модуль pubSub и вызываем метод subscribe, чтобы подписаться на событие. Мы передаем имя события, на которое мы хотим подписаться, и обратный вызов подписчика. Это обратный вызов, который сохраняется в массиве событий и будет вызываться при публикации события. Как мы видели ранее, он также предоставит нам необязательный объект данных, и в этом случае мы просто регистрируем свойство data.msg на консоли.

Теперь мы можем настроить наш основной файл. В эту же папку создайте файл index.js.

const moduleA = require("./moduleA");
const moduleB = require("./moduleB");

// We use moduleA's publishEvent() method

moduleA.publishEvent();
moduleA.publishEvent();

Здесь мы импортируем оба модуля, и затем вызываем метод publishEvent из модуля moduleA.

Теперь в нашем терминале перейдите в папку javascript-design-patterns и введите:

$ node ./pubsub/index.js

Вы должны увидеть следующие записи в консоли:

"anEvent", was published with this data: "TOP SECRET DATA"
"anEvent", was published with this data: "TOP SECRET DATA"

В файле moduleB — наш подписчик, метод pubSub.subscribe создал событие под названием «anEvent» и добавил обратный вызов, который мы предоставили, в список обратных вызовов для этого события.

Когда мы в конечном итоге вызываем метод publishEvent для moduleA — нашего издатель, вызывает обратный вызов. Поскольку мы вызываем метод publishEvent дважды, мы видим, что свойство data.msg регистрируется в консоли дважды.

На этом этапе в полной реализации шаблона PubSub отсутствует еще один метод, он должен позволять подписчикам отказаться от подписки на обновления.

Отличный способ сделать это — вернуть объект с методом unsubscribe из вызова функции, который подписывает модуль на событие.

Нам нужно знать индекс обратного вызова подписчика в массиве событий, чтобы мы могли его удалить. В файле pubsub.js давайте проведем рефакторинг метода subscribe.

subscribe(event, callback) {
    let index;    
    if (!subscribers[event]) {
        subscribers[event] = [];
    }    
    index = subscribers[event].push(callback) - 1;
    
    return {
        unsubscribe() {
            subscribers[event].splice(index, 1);
        }
    };
}

Мы добавили новую переменную с именем index. К счастью для нас, метод push() Javascript возвращает новую длину массива после добавления элемента. Мы можем взять эту длину и вычесть 1, чтобы получить индекс вновь добавленного обратного вызова подписчика, который мы затем сохраним в переменной index.

После этого мы возвращаем объект, который содержит метод unsubscribe. Этот метод берет массив subscribers[event] и удаляет обратный вызов в указанном месте индекса, используя метод splice.

Теперь мы можем использовать этот метод unsubscribe в нашем подписчике.

moduleB.js:

const pubSub = require("./pubsub");

let subscription;
subscription = pubSub.subscribe("anEvent", data => {
    console.log(
        `"anEvent", was published with this data: "${data.msg}"`
    );
    subscription.unsubscribe();
});

Сначала мы добавляем переменную subscription. Затем мы сохраняем возвращаемое значение из метода pubSub.subcribe() в переменной подписки (которую мы знаем из модуля pubsub как объект с методом unsubscribe).

Затем мы вызываем метод unsubscribe в обратном вызове после регистрации события в консоли.

Если вы снова запустите программу, на этот раз вы обнаружите, что в консоль записано только одно сообщение. Это связано с тем, что наш подписчик отписался от обновлений к моменту запуска второго метода publishEvent() с использованием метода subscription.unsubscribe().

И это все что я хотел рассказать о шаблоне Publisher/Subscriber!

Подводя итог, мы создали модуль pubsub, отвечающий за предоставление средств подписки и публикации обновлений с помощью методов subscribe и publish соответственно. Связь происходит по именованному каналу, который мы назвали «events», и каждое событие отслеживает обратные вызовы абонента, которые вызываются, когда событие инициируется/публикуется. Мы также создали метод unsubscribe(), который позволяет очищать код после себя, когда дальнейшие обновления больше не требуются.

Как мы уже видели, шаблон Publisher/Subscriber позволяет легко разъединять наши модули и устранять жесткие зависимости между ними. Однако, как отмечалось ранее, мы должны быть осторожны, чтобы не злоупотреблять этим шаблоном, так как это может привести к неясному, сложному для сопровождения коду.

Далее в этой серии мы расскажем о Strategy. Шаблон для выбора различных поведений (стратегий) во время выполнения в зависимости от предоставленного ввода. Как всегда, я хотел бы услышать ваши мысли в комментариях ниже! (на странице автора статьи)

Была ли вам полезна эта статья?
[6 / 4.3]

Spread the love
Editorial Team

Recent Posts

Vue 3.4 Новая механика v-model компонента

Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование​ v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…

11 месяцев ago

Анонс Vue 3.4

Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…

11 месяцев ago

Как принудительно пере-отобразить (re-render) компонент Vue

Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…

2 года ago

Проблемы с установкой сертификата на nginix

Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…

2 года ago

Введение в JavaScript Temporal API

Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…

2 года ago

Когда и как выбирать между медиа запросами и контейнерными запросами

Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…

2 года ago