Организация потоков данных

Данные приходят в программу, а вот как с ними работать дальше нужно придумывать.

Время чтения: 5 мин

Программы — это преобразователи данных. Мы даём программе задачу: вводим начальные данные и ожидаем получить какой-то результат после их преобразования.

Сложные программы делят исходную задачу на задачи поменьше. За разные подзадачи отвечают разные части программы — функции, классы, модули.

Чтобы программа выполнила исходную задачу целиком, её части должны работать сообща. Они общаются друг с другом с помощью промежуточных результатов работы — тоже данных.

То, как одна часть программы получает и передаёт данные другим, называется потоком данных (data flow) и может определить архитектурное устройство всей системы.

Виды потоков данных

Скопировано

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

  • однонаправленный (one-way).
  • двунаправленный (two-way).

Однонаправленный поток

Скопировано

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

Однонаправленный поток можно схематично представить, как водопровод, а модуль — как часть трубы:

У данных всегда есть направление и оно не меняется

В таком потоке данные «текут» от одного модуля к другому, а выходные данные предыдущего становятся входными следующего:

Данные переходят от одной сущности к другой

Flux / Redux

Скопировано

Самый известный пример архитектуры с однонаправленным потоком данных — это Flux и, как его реализация, Redux.

Во Flux приложение состоит из 3 главных компонентов:

  • хранилище данных или стор, store;
  • диспетчер, dispatcher;
  • представление или вью, view.

Задача стора похожа на задачу модели из MVC — он хранит в себе данные. Изменение данных в сторе влечёт за собой изменение представления, то есть перерисовку пользовательского интерфейса.

Задача представления — показать данные в понятном для пользователя виде, нарисовать пользовательский интерфейс.

Когда пользователи совершают какие-то действия, например, нажимают на кнопки, представление вызывает экшен (action) — объект-команду, который говорит, что произошло.

Экшен попадает в диспетчер, он распространяет этот экшен всем модулями, которые знают, как обработать его. (В Redux такие модули называются редьюсеры, reducers.) Эти модули преобразовывают данные в хранилище. Обновление данных влечёт перерисовку представления, и цикл замыкается.

Диаграмма потока данных во Flux

Такой поток данных похож на классический MVC.

Двунаправленный поток

Скопировано

Данные в двунаправленном потоке могут передаваться между частями программы в обе стороны.

Модуль может получить и передать данные предыдущему модулю

Чаще всего это используется для связывания модели и представления, чтобы обновление, например, текста в поле ввода сразу обновило данные в модели — это называется двунаправленным связыванием данных (two-way data binding).

Обновления модели влияют на отображение, отображение влияет на модель

У такого «среза углов» есть и плюсы, и минусы. Из плюсов:

  • Меньше кода, потому что не надо писать экшен и обработчик для него.
  • Работает как магия, если фреймворк делает всё автоматически за разработчиков.

Из минусов:

  • Труднее отлаживать, когда двойное связывание используется для чего-то сложнее, чем обновление текста в поле ввода.
  • Это работает как магия, если фреймворк делает всё автоматически :–)

Реактивность

Скопировано

Фреймворки, которые используют двунаправленное связывание, часто реактивные — то есть применяют изменения мгновенно не только к UI, но и к вычисляемым данным.

Представьте, что мы используем Excel. Запишем в двух ячейках численные значения, а в следующей — функцию подсчёта их суммы:

Записываем в ячейках 2 и 3

В третьей ячейке будет посчитанное значение:

В третьей ячейке — сумма, 5

Если мы теперь изменим значение первой ячейки, значение суммы пересчитается автоматически.

После изменения значения на 22 сумма стала равна 25

Это автоматическое изменение значения в третьей ячейке — и есть реактивность. Кажется, что это ничем не отличается от простого двойного связывания, но разница есть.

Представим, что мы пишем код, который должен делать то же самое:

        
          
          function sum(a, b) {  return a + b}let a1 = 2let a2 = 3let a3 = sum(a1, a2)// 5
          function sum(a, b) {
  return a + b
}

let a1 = 2
let a2 = 3
let a3 = sum(a1, a2)
// 5

        
        
          
        
      

Если мы изменим значение a1, значение a3 не поменяется:

        
          
          a1 = 22console.log(a3)// 5
          a1 = 22
console.log(a3)
// 5

        
        
          
        
      

Чтобы значение реактивно отреагировало (to re-act) на изменение, нам нужно перезапустить его подсчёт:

        
          
          a3 = sum(a1, a2)// 25
          a3 = sum(a1, a2)
// 25

        
        
          
        
      

Фреймворки типа Vue берут на себя заботу о реактивности значений.

Что выбрать

Скопировано

Зависит от задачи :–)

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

Например, в больших или сложных приложениях или больших командах хочется меньше «магии», чтобы чётко понимать, что и при каких условиях происходит — однонаправленный поток может это обеспечить.

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