Рабочая демонстрация
Код

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

  1. Посмотрите, насколько хорошо RxJS работает с React: я рассматривал redux-observable как промежуточное ПО для обработки асинхронности и побочных эффектов. Прежде чем опробовать библиотеку, я хотел увидеть, как я сам подхожу к использованию RxJS с React.
  2. Посмотрите, насколько «легко обосновать» код RxJS: у меня создалось впечатление, что потоковые библиотеки (и функциональное программирование в целом) поначалу довольно сложно изучить, но становятся интуитивно понятными, как только они усваиваются. .

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

Redux застрял в моей голове

Даже после того, как я намеренно исключил Redux из своего проекта, я, естественно, попытался имитировать его. Я использовал веб-API CustomEvent (удачно разделенный именами как «действие») для отправки событий,

this.goToNextStep = () => {
  const action = {
    origin: ‘USER’,
    request: ‘GO_TO_NEXT_STEP’
  }
  document.dispatchEvent(new CustomEvent(‘action’, {detail: action}))
}

и создать наблюдаемое событие «действия» в компоненте React <StreamProvider /> с подпиской, которая будет запускать обновления состояния, которые передаются дочернему компоненту <SortVisualizer />.

this.sortHistory$ = Observable
  .fromEvent(document, 'action').map(e => e.detail)
  .mergeMap(...) // deal with actions
this.sortHistory$
  .subscribe(x => {
    this.setState({
      sortState: x[x.length - 1]
    })
  })

Стримы классные

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

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

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

Реализовать «Отменить» легко с RxJS

Создание кнопки «вернуться на один шаг» с использованием RxJS было легким делом. Во-первых, я отслеживал текущее состояние процесса сортировки как опору in<SortVisualizer /> следующим образом:

const currentSortState = {
  bars: [{bar}, ...] // data that we're sorting
  nextStep: {targetIndex: 0, type: 'COMPARE'} // describes what the next sorting step is
}

Это свойство взято из наблюдаемого в <StreamVisualizer />. Метод .scan() RxJS значительно упростил задачу:

this.sortHistory$ = this.actions$
  // other stuff
  .scan((acc, curr) => {
    if (curr.request === 'GO_TO_PREV_STEP') { // if want to go back
      return acc.length <= 1 ? acc : acc.slice(0, acc.length -1)
      // just remove latest step 
    } else {
      // other stuff
    }
  })

Декларативная обработка асинхронного режима с помощью RxJS

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

Например, я хотел иметь функцию «автовоспроизведение», которая будет отключаться всякий раз, когда пользователь пытается сделать что-то еще, например перетащить полосу, чтобы не отпустить ее. Вместо того, чтобы императивно говорить: «Если произойдет событие А, выключите автовоспроизведение. Если произойдет событие B, выключите автовоспроизведение. Если событие C… »Я мог легко сказать« автоматическое воспроизведение, пока не произойдет какое-либо другое событие ».

const handleAutoPlay = (action) => {
  if (!this.state.playing && action.request === 'TOGGLE_PLAY') {
    // to turn on autoplay
    return Observable.interval(500) // every 500ms
      .startWith(1)
      .takeUntil(this.actions$) // until we get some other action
      .takeWhile(x => !this.state.sortState.sortCompleted)
      // or until sorting is completed
      .map(x => {return {request: 'GO_TO_NEXT_STEP'}})
      // fire 'GO_TO_NEXT_STEP'
      .do(x => {
        if (!this.state.playing) {this.setState({playing: true})}
      })
      .finally(() => {this.setState({playing: false})})
  } else {
    return Observable.of(action)
  }
}
this.sortHistory$ = this.actions$
    .mergeMap(handleAutoPlay)
    // other stuff

В заключение

Кодовая база определенно может потребовать больше работы (например, события отправки разбросаны по <StreamProvider /> и <SortVisualizer />, yuk), но на этом я пока завершу свой эксперимент.

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

Спасибо за чтение.