Как стать автором
Обновить

Publish-Subscribe на TypeScript — уменьшаем связанность

Время на прочтение5 мин
Количество просмотров5.8K

Известно, что одним из признаков хорошего архитектурного дизайна является слабая связанность между отдельными модулями приложения. Достичь этого можно разными способами: Dependency Injection, с помощью паттернов проектирования Mediator, Publish-Subscribe и некоторыми другими, многие из которых так или иначе реализуют принцип инверсии зависимостей, ответственных за уменьшение связанности. Об одном из таких паттернов, а именно о Publish-Subscribe (далее PubSub) мы сегодня и поговорим. А заодно, предлагаю рассмотреть мою собственную реализацию на TypeScript, построенную на декораторах - люблю я декларативный подход, ничего тут не сделаешь.

Что такое PubSub и как он работает

Для тех, кто возможно не сильно знаком с данной темой, рассмотрим вкратце что из себя представляет паттерн PubSub, и как именно он помогает уменьшить связанность между модулями приложения. Если говорить просто - PubSub предоставляет некий централизованный канал для коммуникации между модулями, каждый из которых может:

  • Опубликовать (Publish) сообщение, состоящее из идентификатора типа этого сообщения и неких полезных данных (Payload)

  • Подписаться (Subscribe) на получение и обработку сообщений интересующего типа

Модуль, публикующий сообщения, принято называть Publisher, а подписывающийся и обрабатывающий - Subscriber. Все довольно просто, можно изобразить в виде следующей картинки:

Ключевой момент здесь в том, что отправители сообщения ничего не знают о подписчиках, а подписчики - ничего не знают об отправителях. И те и другие зависят только от типа и формата сообщений, с которыми они работают. Последние два предложения вам ничего не напоминают? Примерно так и звучит принцип инверсии зависимостей, да и по направлению стрелочек на картинке можно догадаться, что в данном случае реализован именно он. Поскольку модули больше не зависят напрямую друг от друга, их можно создавать раздельно, безболезненно заменять на другие модули, например - в целях тестирования.

Пакет type-pubsub

Предлагаю рассмотреть мою реализацию данного паттерна в виде готового npm-пакета type-pubsub. При реализации я руководствовался следующими соображениями:

  • Подписка при помощи декораторов: использование @Subscribe() на методе класса позволяет автоматически зарегистрировать его в качестве обработчика интересующего нас сообщения

  • Возможность подписчикам указывать канал явно (в случаях, если каналов несколько или используется не канал по-умолчанию) или не указывать (будет использован канал по-умолчанию) - тоже в параметрах декоратора

  • Возможность отписываться от сообщений, на обработку которых класс был подписан автоматически с использованием декораторов

  • Один и тот же метод может быть зарегистрирован в качестве обработчика нескольких типов сообщений

  • Возможность подставить кастомную реализацию PubSub-канала (например, адаптер для другой библиотеки или что-то принципиально иное) - все что требуется, реализовать простой интерфейс с методами publish, subscribe и unsubscribe

  • Класс-подписчик - не обязательно синглтон, несколько экземпляров одного и того же класса подписчика, существующие одновременно, должны работать корректно. В случаях, когда нам нужен именно синглтон - возможность создать его экземпляр неявно и автоматически

В простейшем случае, использование выглядит следующим образом:

import { PubSub, Subscribe, Subscriber, Unsubscribe } from 'type-pubsub';

@Subscriber()
class SubscriberExample {
  @Subscribe('TEST_MESSAGE')
  foo(payload: string, message: string): void {
    console.log(payload);
  }

  // Calling this the method marked with @Unsibscribe() will unregister 
  // all the subscriptions. No implementation is needed
  @Unsubscribe()
  dispose(): void {}
}

var subscriber = new SubscriberExample();
PubSub.publish('TEST_MESSAGE', 'This message will be displayed');
subscriber.dispose(); // Unsubscribe
PubSub.publish('TEST_MESSAGE', "This message won't be displayed");

В примере выше мы реализовали класс SubscriberExample, пометили его декоратором @Subscriber(), что автоматически и реализует всю магию: методы, объявленные с помощью @Subscribe(...) будут автоматически зарегистрированы в качестве обработчиков интересующего нас сообщения (в данном примере - 'TEST_MESSAGE'), а метод, помеченный @Unsubscribe() будет обернут в оболочку, реализующую отписку данного экземпляра подписчика от всех ранее подписанных сообщений.

В предыдущем примере мы не указывали явно канал подписчику, что означает подписку на канал по-умолчанию, которым в данном пакете является PubSub. В случаях, если нам нужно указать канал явно, мы можем передать параметры в декоратор @Subscriber():

import { Channel, Subscribe, Subscriber, Unsubscribe } from 'type-pubsub';

const myChannel = new Channel<string>();

@Subscriber({ channel: myChannel })
class SubscriberExample {
  ...
}

myChannel.publish('TEST_MESSAGE', 'Some data');

Как уже было сказано ранее, один и тот же метод можно подписать одновременно на несколько сообщений, для этого нужно указать несколько декораторов @Subscribe():

@Subscriber()
class SubscriberExample {
  @Subscribe('TEST_MESSAGE')
  @Subscribe('OTHER_MESSAGE')
  foo(payload: string, message: string): void {
    console.log(`Message received: ${message}, Payload: ${payload}`);
  }
}

При срабатывании, тип сообщения придет во втором параметре, данные (Payload) - в первом. На самом деле при вызове обработчика в метод может быть передано четыре параметра: payload, тип сообщения, канал и ссылка на сам обработчик, что позволяет, в частности, отписаться прямо из обработчика:

@Subscriber()
class SubscriberExample {
  @Subscribe('TEST_MESSAGE')
  foo(
      payload: string, 
      message: string, 
      channel: PubSubService<string>, 
      handler: MessageHandler<string, string>
  ): void {
    ...
    channel.unsubscribe(message, handler);
  }
}

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

var subscriber = new SubscriberExample();

В некоторых случаях хочется просто реализовать и экспортировать класс, единственный экземпляр которого должен существовать на протяжении всего времени работы приложения (синглтон), и в случаях, когда у нас не используются специальные инструменты управления жизненным циклом, такие как DI-контейнеры, чтобы не создавать объект вручную, можно соответствующим образом сконфигурировать @Subscriber():

@Subscriber({ createInstance: true })
class SubscriberExample {
  ...
}

При регистрации подписок конструктор класса будет вызван автоматически. Если же вам в конструктор нужно передать параметры, их можно указать с помощью свойства constructorParameters:

@Subscriber({ createInstance: true, constructorParameters: ['Test'] })
class SubscriberExample {
  constructor(p: string) { }
  ...
}

Ну и наконец, если не хочется использовать классы и декораторы, подписаться можно и без них:

PubSub.subscribe('TEST_MESSAGE', (payload) => console.log(payload));

В заключение

Пакет может быть использован как для Node.js, так и для Web-браузера. Лично я сейчас использую его в своем React-приложении с Apollo-client в качестве GraphQL-клиента и в какой-то степени State Manager-а. В архитектуре приложения принято некоторое разделение ответственности: кастомные хуки, реализующие взаимодействие с сервером при помощи GraphQL, не являются ответственным за обновление стейта - это не их обязанность. И чтобы реализовать обновление состояния при успешном выполнении, скажем, мутаций, мы просто публикуем сообщение что такая-то такая-то сущность была добавлена/изменена/удалена. Те, кто ответственны за управление shared-стейтом, пусть сами предпримут по этому поводу необходимые действия. Зависимости логических модулей друг от друга пропадают, связанность ослабляется.

На момент публикации данной статьи я с большего удовлетворен реализацией и особых глобальных изменений в пакете не вижу, разве что небольшие косметические. Понимаю, что моя реализация далеко не единственная, однако принял решение реализовать самостоятельно т.к. не видел особой сложности и можно адаптировать именно под свои вкусы и предпочтения. Будет здорово, если кому-либо еще данная реализация может показаться полезной, так же как буду рад любым советам, замечаниям и просто вниманию и рассуждениям по данной теме.

Ссылка на GitHub-репозиторий: https://github.com/YMSpektor/type-pubsub

Теги:
Хабы:
+7
Комментарии3

Публикации

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн