Реактивное программирование: написание слабосвязанного программного обеспечения в сети

В разработке программного обеспечения при проектировании поддерживаемой архитектуры используются две ключевые концепции: связь и согласованность. Мы всегда должны стремиться к слабосвязанному и высокосвязному программному обеспечению, и хотя это возможно достичь на бумаге, выразить это в реализации иногда может быть сложно - если не невозможно - если мы не выберем правильные инструменты и решения, доступные для язык / среда, которые мы используем.

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

Почему реактивное программирование?

Во-первых, почему реактивное программирование хорошо подходит для Интернета? Взгляните на следующие утверждения:

  • «Когда пользователь успешно аутентифицирован, получите данные учетной записи»
  • «Когда пользователь покупает продукт, отправляйте данные в инструмент аналитики»
  • «После получения информации об учетной записи пользователя и доступности настроек местоположения перенаправьте пользователя в предпочтительный город»

Если вы работали над относительно сложными веб-проектами, вы должны быть знакомы с этими утверждениями. Это связано с тем, что бизнес-логика веб-сайта управляется событиями, а с событиями приходит оркестровка: «когда происходит событие A, выполняется логика B».

Реактивное программирование - это парадигма декларативного программирования, использующая потоки данных для связи и распространения изменений. Другими словами, существует поток данных, который получает релевантную информацию - например, Событие А произошло - и есть группа подписчиков, которые слушают эту информацию и реагируют соответствующим образом - например, Выполнить логику B. Звучит знакомо?

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

  1. Декларативный код: генерация событий + подписки - просты для понимания и выполнения, не только упрощают общее обслуживание, но и позволяют без труда обнаруживать запахи кода, например генерирование события «перенаправление в предпочтительный город» в логике хранилища информации об учетной записи пользователя - не очень ли связно, не так ли?
  2. Развязанный код: минимальные зависимости между различными программными компонентами, поскольку им нужно только знать доступные события, чтобы они могли подписаться на них. Нет необходимости знать детали реализации, например, при каких обстоятельствах они генерируются или как именно

Как делать реактивное программирование в сети (правильно)

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

# 1 Найдите технически надежный глобальный поток данных, на который вы можете подписаться

JavaScript предоставляет множество способов сделать это, среди которых можно упомянуть:

  1. VanillaJS с использованием addEventListener, dispatchEvent и CustomEvent
  2. Библиотека RxJS
  3. Angular + поток действий Магазин NgRx
  4. Vue + поток действий Vuex
  5. Стрим действий React + Redux

# 2 Рассматривайте всю логику как потенциальное событие для стрима

Поскольку JS управляется событиями, вся логика выполняется внутри обработчика существующего события - даже код верхнего уровня выполняется на window.onload (или когда файл загружается и выполняется).

Это означает, что любая логика может быть либо частью текущего обработчика, либо представлять новое событие со своим собственным обработчиком. Например, следующий вариант использования:

Когда пользователь успешно аутентифицирован, получите данные учетной записи.

Все может быть реализовано в одном обработчике:

async onAuthenticationFormSubmit(credentials) {
  const session = await authenticateUser(credentials)
  storeSession(session)
  
  const accountDetails = await fetchAccountDetails(session.userId)
  storeAccountDetails(accountDetails)
}

Или в отдельных обработчиках:

async onAuthenticationFormSubmit(credentials) {
  const session = await authenticateUser(credentials)
  storeSession(session)
  dispatchEvent(
    new CustomEvent('onUserAuthenticated', { details: session })
  )  
}
async onUserAuthenticated({ details }) {
  const { userId } = details.session
  const accountDetails = await fetchAccountDetails(userId)
  storeAccountDetails(accountDetails)
}

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

  1. Чтобы изменить свое мышление от синхронного императивного программирования к асинхронному реактивному программированию и
  2. Чтобы заполнить поток данных соответствующими событиями, сделав его надежным

# 3 Никогда не отправляйте события, которые не связаны с текущим обработчиком

Следуя тому же примеру, это сильный запах кода:

async onAuthenticationFormSubmit(credentials) {
  const session = await authenticateUser(credentials)
  storeSession(session)
  dispatchEvent(new CustomEvent('onFetchAccountDetails', {
    details: session.userId
  })
  dispatchEvent(new CustomEvent('onSendAuthAnalyticsData', {
   details: session
  })
  dispatchEvent(new CustomEvent('onUpdateUserLocation', {
    details: session.userId
  })
}

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

Это рекомендуемый подход:

async onAuthenticationFormSubmit(credentials) {
  const session = await authenticateUser(credentials)
  storeSession(session)
  
  dispatchEvent(new CustomEvent('onUserAuthenticated', {
    details: session
  })  
}
window.onload = () => {
  addEventListener('onUserAuthenticated', fetchAccountDetails)
  addEventListener('onUserAuthenticated', sendAuthAnalytics)
  addEventListener('onUserAuthenticated', updateUserLocation)
}
async fetchAccountDetails(event) { ... }
async sendAuthAnalytics(event) { ... }
async updateUserLocation(event) { ... }

# 4 Предоставление событий потребителям в виде контракта

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

Кроме того, полезно иметь «официальный» источник событий, такой же простой, как список строковых констант:

export const ON_USER_AUTHENTICATED = 'ON_USER_AUTHENTICATED'
export const ON_USER_SIGNED_OUT = 'ON_USER_SIGNED_OUT'
...

Резюме

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

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