Ряд практик, которые я использую каждый день в своем коде

RxJS - самый популярный фреймворк для функционального реактивного программирования (FRP) на JavaScript. Это означает, что многие люди ежедневно используют RxJS в своих проектах. Большинство разработчиков знают об общих практиках чистого кода, но как насчет лучших практик RxJS? Вы знаете, что можно и чего нельзя делать, когда дело касается FRP? Вы применяете их в своем коде?

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

Мы рассмотрим эти техники:

1. Избегайте логики внутри функции подписки.

2. Используйте темы для принудительного завершения

3. Избегайте дублирования логики.

4. Избегайте вложенности - используйте цепочку.

5. Поделитесь, чтобы избежать дублирования потока

6. Не раскрывайте темы

7. Используйте мраморные диаграммы для тестирования.

Без лишних слов, приступим.

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

1. Избегайте логики внутри функции подписки

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

Наш pokemon$ Observable испускает Pokemon объектов, и очень нереактивным способом мы подписываемся на него, чтобы получить доступ к этим объектам и выполнить некоторые действия, например, вернуться раньше, если тип Pokemon - Water, вызов функции getStats(), регистрация статистики, возвращаемой этой функцией, и, наконец, сохранение данных в Pokedex. Вся наша логика находится внутри функции subscribe.

Однако разве этот код не выглядит точно так же, как в традиционной парадигме императивного программирования? Поскольку RxJS - это функциональная библиотека реактивного программирования, мы должны попрощаться с нашим традиционным образом мышления и начать думать функционально реактивно (Streams! Чистые функции!).

Так как же сделать наш код функциональным и реактивным? Используя конвейерные операторы, которые предоставляет нам RxJS:

Et voilá, наш код превратился из императивного в функционально реактивный с несколькими простыми изменениями. Он даже выглядит чище, не так ли?

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

Операторы, которые мы использовали, довольно просты: filter и map работают точно так же, как операторы массива, с которыми они имеют общее имя, а tap используется для выполнения побочных эффектов (определенных ниже).

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

2. Использование субъектов для принудительного завершения

Утечки памяти представляют реальную опасность, когда дело доходит до использования Observables. Почему? Потому что, как только мы подписываемся на Observable, он будет передавать значения неограниченное время , пока не будет выполнено одно из следующих двух условий:

  1. Мы вручную отписываемся от Observable
  2. Это завершает

Кажется достаточно простым, правда? Давайте посмотрим, как отказаться от подписки на Observable:

Как вы можете видеть в приведенном выше примере, мы должны сохранить подписку нашего pokemon$ Observable в переменной, а затем вручную вызвать unsubscribe для этой сохраненной подписки. Пока не кажется слишком сложным.

Но что произойдет, если у нас будет больше Observables, на которые нам нужно подписаться?

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

Мы можем использовать Subject вместе с оператором takeUntil(), чтобы заставить наши Observables завершиться. Как? Вот пример:

Давайте разберемся, что происходит выше. Мы создали stop$ Subject и передали наши три Observable оператору takeUntil. Этот оператор используется, чтобы сообщить Observable прекратить выдачу значений, как только появится другой Observable-уведомитель. Это означает, что наши три Observable перестанут испускать значения, когда будет испускаться stop$ Subject.

Итак, как нам сделать так, чтобы наша stop$ Subject испускалась? Вызывая на нем функцию next(), что мы и делаем внутри нашей функции stopObservables(). Следовательно, всякий раз, когда мы вызываем нашу stopObservables() функцию, наша stop$ Subject будет выдавать, и все наши Observables будут автоматически завершены. Звучит круто, не правда ли?

Больше не нужно хранить какие-либо подписки и отказываться от подписки? Приветствую оператора takeUntil!

3. Избегайте дублирования логики

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

Как видите, у нас есть number$ Observable, на который мы подписываемся дважды: один раз, чтобы вычислить общее значение с помощью reduce() , и один раз, чтобы избавиться от всех нечетных чисел с помощью filter().

Однако наш number$ Observable, несмотря на свое название, содержит не только числа. Как видите, он также содержит некоторые опасные ложные значения, которые могут привести к сбою нашего кода. Вот почему нам нужно отфильтровать эти ложные значения, что мы и делаем с filter(Boolean).

Примечание: при использовании filter(Boolean) мы теряем вывод типа. Этого можно избежать, явно набрав filter, например: filter<number>(Boolean).

Кажется довольно простым, но ...

Обратите внимание, как мы продублировали filter(Boolean) логику в обоих Observables? Этого следует избегать, если это позволяет наш код. Как? Прикрепив эту логику к исходному Observable, например:

Меньше кода + отсутствие дублирования = более чистый код. Потрясающие!

4. Избегайте вложенности - вместо этого используйте цепочку

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

"Что такое вложенная подписка?" вы можете спросить. Это когда мы подписываемся на Observable в блоке подписки другого Observable. Давайте посмотрим на следующий код:

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

Ответ - использовать операторы отображения более высокого порядка. Некоторые из этих операторов - switchMap, mergeMap и т. Д.

Чтобы исправить наш пример, мы собираемся использовать оператор switchMap. Почему? Потому что switchMap отписывается от предыдущего Observable и переключается (легко запомнить, правда?) На внутренний Observable, что в нашем случае является идеальным решением. Однако обратите внимание, что в зависимости от того, какое поведение вам нужно, вам может потребоваться использовать другой оператор сопоставления более высокого порядка.

Вы только посмотрите, как красиво теперь выглядит наш код.

5. Совместное использование во избежание дублирования потока

Когда-нибудь ваш код Angular создавал дублированные HTTP-запросы, и вы задаетесь вопросом, почему? Читайте дальше, и вы узнаете причину этой широко распространенной ошибки.

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

Так что же такое производитель? По сути, это источник значений нашего Observable (например, событие DOM, HTTP-запрос, массив и т. Д.). Что это означает для нас, реактивных программистов? Что ж, если мы, например, дважды подписываемся на наблюдаемый объект, который делает HTTP-запрос, будут выполнены два HTTP-запроса.

Похоже на проблемы.

Следующий пример (заимствование HttpClient Angular) вызовет два разных HTTP-запроса, потому что pokemon$ - это холодный Observable, и мы подписываемся на него дважды:

Как вы понимаете, такое поведение может привести только к неприятным ошибкам, так как же нам этого избежать? Нет ли способа многократно подписаться на Observable, не вызывая дублирования логики, поскольку его источник создается снова и снова? Конечно, есть. Разрешите представить оператор share().

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

Да, это действительно все, что нам нужно сделать, и наша проблема волшебным образом решена. При добавлении оператора share() наш ранее холодный pokemon$ Observable теперь ведет себя так, как если бы он был горячим, и будет выполнен только один HTTP-запрос, даже если мы подписались на него дважды.

Предостережение: поскольку горячие Observables не реплицируют источник, если мы поздно подпишемся на поток, мы не сможем получить доступ к ранее выданным значениям. Решением для этого может быть оператор shareReplay() .

6. Не раскрывайте темы

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

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

"Разве это не одно и то же?" вам может быть интересно. Ответ - нет. Если мы раскрываем Subject, мы делаем доступными все его методы, включая функцию next(), которая используется для того, чтобы Subject выдавал новое значение. С другой стороны, если мы просто раскроем его данные, мы не сделаем доступными методы нашего Субъекта, а только значения, которые он испускает.

Итак, как мы можем раскрыть данные нашего Субъекта, но не его методы? Вызывая метод asObservable(), который в основном преобразует Subject в Observable. Поскольку у Observables нет функции next(), данные нашего субъекта будут защищены от подделки:

В приведенном выше коде происходит четыре разных вещи:

  • Обе наши pokemonLevel и stop$ Субъекты теперь являются частными и, следовательно, недоступны извне нашего DataService класса.
  • Теперь у нас есть pokemonLevel$ Observable, который был создан путем вызова метода asObservable() для нашего pokemonLevel Subject. Таким образом, мы можем получить доступ к pokemonLevel данным извне класса, сохраняя при этом Subject в безопасности от манипуляций.
  • Возможно, вы заметили, что для stop$ Subject мы не создавали Observable. Это потому, что нам не нужен доступ к данным stop$ извне.
  • Теперь у нас есть два общедоступных метода с именами increaseLevel() и stop(). Последнее достаточно просто понять. Это позволяет нам заставить частный stop$ Subject испускаться извне класса - таким образом, завершая все Observables, которые переданы takeUntil(stop$) по конвейеру.
  • increaseLevel() действует как фильтр и позволяет передавать в pokemonLevel() Subject только определенные значения.

Таким образом, никакие произвольные данные не смогут попасть в наши Субъекты, которые хорошо защищены внутри класса.

Примечание. Имейте в виду, что у Observables есть методы complete() и error(), которые по-прежнему можно использовать для искажения темы.

Помните, что инкапсуляция - это ключ к успеху.

Используйте мраморные диаграммы для тестирования

Как мы все (должны) знать, написание тестов так же важно, как написание самого кода. Однако, если мысль о написании тестов RxJS кажется вам немного пугающей… не бойтесь. Начиная с RxJS 6+, утилиты для тестирования мрамора RxJS значительно упростят нам жизнь. Вы знакомы с мраморными диаграммами? Если нет, то вот пример:

Даже если вы новичок в RxJS, вы должны более или менее разбираться в этих схемах. Они повсюду, они довольно интуитивно понятны и позволяют легко понять, как работают некоторые из более сложных операторов RxJS. Утилиты тестирования RxJS позволяют нам использовать эти мраморные диаграммы для написания простых, интуитивно понятных и визуальных тестов. Все, что вам нужно сделать, это импортировать TestScheduler из модуля rxjs/testing и начать писать тесты!

Давайте посмотрим, как это делается, протестировав наш number$ Observable:

Поскольку глубокое погружение в тестирование мрамора не является целью этого руководства, я лишь кратко рассмотрю ключевые концепции, которые появляются в приведенном выше коде, чтобы мы имели общее представление о том, что происходит:

  • TestScheduler: Используется для виртуализации времени. Он получает обратный вызов, который можно вызывать с помощью вспомогательных объектов (в нашем случае - помощников cold() и expectObservable()).
  • Run(): автоматически вызывает flush() при возврате обратного вызова.
  • - : каждый - представляет 1 миллисекунду виртуального времени.
  • Cold(): Создает холодный Observable, подписка которого запускается при запуске теста. В нашем случае мы создаем холодный Observable, который будет выдавать значение каждые 1 мс и завершаться.
  • |: представляет завершение наблюдаемого.
  • Следовательно, наш expectedMarbleDiagram ожидает, что a будет отправлено через 20 мс.
  • Переменная expectedValues содержит ожидаемые значения каждого элемента, генерируемого нашим Observable. В нашем случае a - единственное значение, которое будет выдано, и оно равно 10.
  • ExpectObservable(): назначает утверждение, которое будет выполняться при сбросе testScheduler. В нашем случае наше утверждение ожидает, что number$ Observable будет похож на expectedMarbleDiagram, со значениями, содержащимися в переменной expectedValues.

Вы можете найти больше информации о помощниках в официальных документах RxJS.

Преимущества использования утилит для тестирования мрамора RxJS:

  • Вы избегаете большого количества шаблонного кода. (Пользователи Jasmine Marbles смогут это оценить.)
  • Пользоваться им очень просто и интуитивно.
  • Это весело! Даже если вы не большой поклонник написания тестов, я могу гарантировать, что вам понравится тестирование мрамора.

Поскольку мне нравится делать все свои примеры кода на тему покемонов, я добавлю еще одну спецификацию, на этот раз с pokemon$ наблюдаемым тестом:

Заключение

Вот и все, ребята! Сегодня мы обсудили несколько лучших практик RxJS, которые я всегда стараюсь применять в своем коде. Надеюсь, вы нашли их полезными, если еще не знали о них.

Знаете ли вы еще какие-нибудь лучшие практики RxJS? Если да, дайте мне знать в комментариях ниже. Таким образом, мы все можем внести свой вклад в создание лучшего и чистого реактивного кода.