Reactive Way

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

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

В это главе мы познакомимся с реактивным программированием. С парадигмой, которая позволит более простым и естественным способом думать о асинхронном коде. Я покажу вам, как потоки событий, которые мы называем Observables — являются прекрасным способом обработки событий. Затем мы с вами создадим Observable и посмотрим, как "реактивное мышление" и RxJS в значительной степени улучшают текущие техники. Это сделает ваш фейс более радостным, ведь боль уйдет, а так же это поможет вам стать более продуктивным разработчиком. Юху!

А "реактивный" - это вообще что?

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

  • Программа должна объединять данные из двух разных источников, которые возвращают разные структуры в JSON.
  • Финальный результат обработки не должен содержать каких-либо дубликатов.
  • Чтобы пользователь не мог жать на кнопку как дурак по многу раз, тем самым делая кучу запросов, мы будет разрешать нажимать только один раз в секунду времени. Короче, блокировать мы ему будем кнопку.

Используя RxJS мы бы написали, что-то такое:

const button = document.getElementById('retrieveDataBtn')

const source1 = Rx.DOM.getJSON('/resource1').pluck('name')
const source2 = Rx.DOM.getJSON('/resource2').pluck('props', 'name')
const clicks = Rx.Observable.fromEvent(button, 'click')

function getResults(amount) {
    return source1
        .merge(source2)
        .pluck('names')
        .flatMap(array => Rx.Observable.from(array))
        .distinct()
        .take(amount)
}

clicks.debounce(1000)
    .flatMap(getResults(5))
    .subscribe(
        (value) => console.log('Received value', value),
        (err) => console.error(err),
        () => console.log('All values retrieved!')
    );

Не беспокойтесь о том, что вы ни черта не понимаете, что тут творится. Ну во-первых, если бы мы использовали обычные техники обработки всего этого добра, то у нас была бы портянка в разы больше. А тут мы используем Observables, что позволяет это сделать более компактно. Круто! Неправда ли? (прим. от переводчика: автор книги не предоставил противопоставляющего примера, поэтому как бы так себе утверждение)

По сути, Observable, представляет собой так называемый "поток данных". Программы могут быть выражены как потоки данных. В предыдущем примере оба удаленных источника - Observables, также как и щелчки мыши у пользователя. На самом деле, наша программа по существу представляет собой единый объект Observable, созданный на основе события нажатия кнопки, которое мы трансформируем для получения желаемых результатов.

Реактивное программирование придает коду выразительность. Возьмем, к примеру, "тормозилку" щелчков мышью в нашем примере. Представьте себе, насколько сложно было бы сделать это, используя callbacks или Promises: нам нужно было бы сбрасывать таймер каждую секунду и сохранять состояние о том, прошла ли секунда с момента последнего нажатия пользователем кнопки. Это очень сложная задача для столь маленьких функций, и код для нее даже не связан с фактической функциональностью вашей программы. В больших приложениях эти небольшие сложности складываются очень быстро, чтобы создать запутанную базу кода.

При реактивном подходе мы используем метод debounce для торможения потока кликов. Это гарантирует, что между каждыми щелчками есть хотя бы одна секунда, и игнорирует любые клики между ними. Нам все равно, как это происходит внутри RxJS. Мы просто выражаем то, что хотим сделать, а не то, как это сделать.

Ну что, интереснее становиться? Далее вы увидите, как реактивное программирование помогает нам сделать наши программы более эффективными и выразительными.

Таблицы типа Excel реактины!

Начнем с рассмотрения наиболее типичного примера реактивной системы: таблицы. Мы все использовали их, но мы редко останавливаемся и думаем, насколько они потрясающе интуитивны. Допустим, у нас есть значение в ячейке A1 таблицы. Затем мы можем ссылаться на него в других ячейках электронной таблицы, и всякий раз, когда мы меняем A1, каждая ячейка, зависящая от A1, автоматически обновляет свое значение.

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

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

События мыши тоже могут быть потоком

Чтобы понять, как события могут быть потоками, давайте вернемся к нашей микро-программе с начала этой главы. Там мы использовали щелчки мыши как бесконечную последовательность событий, генерируемых в режиме реального времени при нажатии пользователем. Эрик Мейер, изобретатель RxJS, предложил в своей статье такую мысль: «Твоя мышь - база данных». [1]

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

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

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

Увидеть свою программу как последовательность данных - вот ключ к пониманию программирования на RxJS! Это требует практики, но это не сложно. На самом деле, большинство данных, которые мы используем в любом приложении, может быть выражено в виде последовательности. Мы рассмотрим последовательности более подробно в Главе 2, Глубоко в Последовательности

Запрос последовательности

Давайте реализуем простую версию этого потока с помощью традиционных обработчиков событий в JavaScript. Чтобы записать координаты X и Y для мыши, мы могли бы написать примерно так:

// ch1/thinking_sequences1.js

document.body.addEventListener('mousemove', (e) => {
    console.log(e.clientX, e.clientY)
})

Этот код будет выводить в консоль X- и Y-координаты каждого перемещения мыши. Вывод выглядит следующим образом:

// типа это консоль

252 183
211 232
153 323
...

Похоже на последовательность, не так ли? Проблема, конечно же, в том, что манипулировать событиями не так просто, как манипулировать массивами. Например, если мы хотим изменить предыдущий код, чтобы он регистрировал только первые 10 кликов в правой части экрана (вряд ли вы встретите эту задачу в реальной разработке), мы напишем что-то такое:

// ПРИМЕЧАНИЕ: это не оригинальный код из книги. Дело в том, что код из книги на 10 клик
// не снимает обработки. Необходимо произвести отдельный 11 клик, который только и сделает,
// что снимет обработчик. Поэтому переводчик посчитал уместным заменить код на более корректный пример.
let clicks = 0

document.addEventListener('click', function registerClicks (e) {
  if (e.clientX > window.innerWidth / 2) {
    clicks += 1

    if (clicks === 10) {
      document.removeEventListener('click', registerClicks)
    }
  }
})

Раз мы будем считать количество кликов, то нам нужно где-то хранить состояние. В данном случаи мы ввели глобальную переменную, то есть внешнее состояние. Также нам нужно проверить два разных условия и использовать вложенные условные блоки. И когда мы совершаем 10 клик, нам нужно снять обработчик, чтобы он в памяти не весел.

Побочные эффекты (side effects) и внешнее состояние (external state)

Если выполнение кода начинает влиять на внешнию среду мы называем это побочным эффектом. Изменение переменных, внешних по отношению к нашей функции, вывод в консоль или обновление значений в базе данных, все это являются примерами побочных эффектов.

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

Не смотря на то, что для создания любой интересной программы неизбежны побочные эффекты, мы должны стремиться к тому, чтобы в нашем коде их было как можно меньше. Это особенно важно в реактивных программах, где у нас есть много движущихся частей, которые меняются с течением времени. В этой книге мы будем использовать подход, который позволяет избежать внешнего состояния и побочных эффектов. Фактически, в Главе 3, «Создание параллельных программ», на странице 39 мы построим целую видеоигру без побочных эффектов.

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

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

SELECT x, y FROM clicks LIMIT 10

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

Давай те взгляним как тоже самое можно сделать с помощью RxJS и его тип данных Observable:

Rx.Observable.fromEvent(document, 'click')
  .filter(c => c.clientX > window.innerWidth / 2)
  .take(10)
  .subscribe(
    (c) => console.log(c.clientX, c.clientY)
  )

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

Создай объект Observable, который будет следить за DOM-событиями клика мыши по document и отфильтруй только клики которые происходят в правой части экрана, затем выведи координаты в консоль только первых 10 кликов по мере их появления.

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

Итого, Observable предоставляет нам последовательность или поток событий, которой(ым) мы можем манипулировать как хотим, вместо одного изолированного события каждый раз. Работа с последовательностями дает нам огромную силу, можно сказать богатырскую. Мы можем легко объединить, трансформировать или фильтровать Observable. Мы превратили наши события, в некую материальную структуру данных, которая так же проста в использовании, как и массив, но гораздо более гибкая.

В следующем разделе мы увидим принципы, которые делают Observables таким очень приятным инструментом.

Господа знакомьтесь: Observers и Iterators

И так, чтобы понять, что за хрень такая этот Observable, нам нужно посмотреть на его фундаментальную составляющую, а именно паттерны проектирования Observer и Iterator. В этом разделе мы кратко рассмотрим их, а затем мы увидим, как Observables сочетают понятия и того, и другого простым, но мощным способом.

Паттерн Observer

Достаточно очевидно, что Observables имеет прямую связь с паттерном Observer, если вы конечно знает о таком шаблоне проектирования. Этот паттерн определяет зависимость типа «один ко многим» между объектами таким образом, что при изменении состояния одного объекта все следящии за ним оповещаются об этом изменение.

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

Следующий код реализует упрощенную версию паттерна:

// ch1/observer_pattern.js
function Publisher () {
  this.subscribers = []
}

Publisher.prototype.add = function (subscriber) {
  this.subscribers.push(subscriber)
}

Publisher.prototype.remove = function (subscriber) {
  const index = this.subscribers.indexOf(subscriber)
  this.subscribers.splice(index, 1)
}

Publisher.prototype.notify = function (message) {
  this.subscribers.forEach((subscriber) => {
    subscriber.update(message)
  })
}

Объект Publisher хранит динамический список Subscribers в свойстве subscribers, которое является массивом. Так же объект Publisher содериж метод add с помощью которого будут добавляться подписчики. Собственно эти подписчики и будут оповещены при вызове метода notify.

В следующем коде мы создаем два объекта, которые подписываются на оповещение экземпляра объекта Publisher:

// ch1/observer_pattern.js

// любой объек имещий метод update будет работать
const subscriber1 = {
  update(message) {
    console.log('Subscriber 1 received:', message)
  }
}

const subscriber2 = {
  update(message) {
    console.log('Subscriber 2 received:', message)
  }
}

const notifier = new Publisher
notifier.add(subscriber1)
notifier.add(subscriber2)

notifier.notify('Hello there!')

После запуска программы мы видем в консоле:

Subscriber 1 received: Hello there!
Subscriber 2 received: Hello there!

Объекты subscriber1 и subscriber2 будут оповещены о изменение состояния объекта notifier всякий раз как будет вызываться метод notify. Заметьте, объектам subscriber1 и subscriber2, не нужно ни чего делать!

Паттерн Iterator

Другая фундаментальная состовляющая Observable - это паттерн Iterator. Итератор - это объект, который предоставляет простой способ прохода его содержимого, скрывая реализацию.

Интерфейс Iterator очень прост. Он требует реализации двух методов: next, чтобы получить следующий элемент последовательности, и hasNext, чтобы проверить, остались ли элементы в последовательности.

Вот как мы могли бы написать итератор, который работает с массивом чисел и возвращает только те элементы, которые кратны параметру divisor:

// ch1/iterator.js
function IterateOnMultiples (arr, divisor) {
  this.cursor = 0
  this.array = arr
  this.divisor = divisor || 1
}

IterateOnMultiples.prototype.next = function () {
  while (this.cursor < this.array.length) {
    let value = this.array[this.cursor++]
    if (value % this.divisor === 0) {
      return value
    }
  }
}

IterateOnMultiples.prototype.hasNext = function () {
  let cur = this.cursor
  while (cur < this.array.length) {
    if (this.array[cur++] % this.divisor === 0) {
      return true
    }
  }

  return false
}

Вот так мы можем использовать итератор:

// ch1/iterator.js
const consumer = new IterateOnMultiples([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 3)

console.log(consumer.next(), consumer.hasNext()) // 3 true
console.log(consumer.next(), consumer.hasNext()) // 6 true
console.log(consumer.next(), consumer.hasNext()) // 9 false

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

Паттерн Rx и объект Observales

Несмотря на то, что паттерны Observer и Iterator сильны сами по себе, их комбинация добавляет еще больше мощи. Мы называем эту комбинацию, как паттерн Rx, названным в честь библиотек Reactive Extensions.[2] Далее, мы будем использовать этот шаблон для всей остальной части книги.

Observable является главным элементом Rx. Observable транслирует свои значения по порядке подобному итератору, но вместо того, чтобы потребители запрашивали следующее значение, Observable посылает значения потребителям по мере их появления. Он имеет аналогичную роль Publisher'а в паттерне Observer: транслирует значения и подталкивает их своим подписчикам.

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

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

Используя Observables, мы можем объявить, как реагировать на последовательность значений, которые они транслирую, вместо того, чтобы реагировать на отдельные значения. Мы можем эффективно копировать, преобразовывать и запрашивать последовательность, и эти операции будут применяться ко всем значениям последовательности.

results matching ""

    No results matching ""