javascript

В чём сила Redux?

  • суббота, 22 июля 2017 г. в 03:12:47
https://habrahabr.ru/post/333848/
  • ReactJS
  • JavaScript


image


Это перевод статьи "What’s So Great About Redux?" (автор Justin Falcone), которая мне показалась весьма приятной и интересной для прочтения, enjoy!


Redux мастерски справляется со сложными взаимодействиями состояний, которые трудно передать с помощью состояния компонента React. По сути, это система передачи сообщений, которая встречается и в объектно-ориентированном программировании, но она не встроена непосредственно в язык, а реализована в виде библиотеки. Подобно ООП, Redux переводит контроль от вызывающего объекта к получателю — интерфейс не управляет состоянием напрямую, а передает ему сообщение для обработки.


В этом плане хранилище в Redux — это объект, редюсеры — это обработчики методов, а действия — это сообщения. Вызов store.dispatch({ type:"foo", payload:"bar" }) равносилен store.send(:foo, "bar") в Ruby. Middleware используется почти таким же образом, как в аспектно-ориентированном программировании (например, before_action в Rails), а с помощью connect в react-redux осуществляется внедрение зависимости.


Какие преимущества даёт такой подход?


  • Благодаря инверсии контроля, упомянутой выше, исчезает необходимость обновлять пользовательский интерфейс, как только меняется имплементация смены состояний. Добавлять такие сложные функции, как логирование, отмена действия или даже time-travel debugging, становится проще простого. Интеграционные тесты сводятся лишь к тому, чтобы проверить, отправляется ли правильное действие, а для всего остального достаточно юнит-тестов.


  • Состояние компонентов в React слишком неповоротливо для работы со сквозной функциональностью, которая затрагивает многие модули приложения, как, например, информация о пользователе или оповещения. Как раз для этого в Redux есть дерево состояний, независимое от пользовательского интерфейса. К тому же, при обработкe состояния вне интерфейса легче поддерживать постоянство, ведь сериализация в localStorage или URL проводится в единственном месте.


  • Редюсеры дают огромную свободу в работе с действиями, которые можно сочетать, отправлять одновременно и даже обрабатывать в стиле method_missing.

Но все эти случаи особые. Что же насчёт более простых сценариев?

Как раз здесь у нас проблемы.


  • Actions можно рассматривать как сложные переходы между состояниями, но в основном они лишь задают одно значение. В приложениях, сделанных на Redux, зачастую накапливается множество таких простых actions, и это явно напоминает Java с написанием функций сеттеров вручную.


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


  • Функции-редюсеры могли бы справиться с самыми замысловатыми задачами метапрограммирования, но обычно сводятся к примитивной диспетчеризации action согласно его типу. Это не проблема для таких языков, как Elm или Erlang, которые отличаются лаконичным и выразительным синтаксисом pattern matching, но в Javascript приходится иметь дело с громоздкими конструкциями switch.

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


Легко списать всё это на человеческий фактор: не осилил документацию, или "мастер глуп — нож туп" — но такие проблемы встречаются подозрительно часто. Не в ноже ли проблема, если он туп для большинства мастеров?


Получается, лучше сторониться Redux в обычных случаях и приберечь его для особенных?


Как раз такой совет вам даст команда Redux — и я говорю то же самое своим коллегам: не беритесь за него до тех пор, пока setState не начнёт совсем выходить из-под контроля. Но даже я сам не следую собственным правилам, потому что всегда можно найти повод использовать Redux. Может быть, у вас есть много actions вроде set_$foo, при этом с каждым присвоением значения обновляется URL или сбрасывается ещё какое-нибудь промежуточное значение. Может, у вас установлено чёткое однозначное соответствие между состоянием и пользовательским интерфейсом, но вам требуется логирование или отмена действий.


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


Что же делать в таком случае?


К счастью, Redux достаточно гибкий, чтобы подключить к нему сторонние библиотеки для работы с простыми вещами — тaкие, как Jumpstart (https://github.com/jumpsuit/jumpstate). Поясню: я не считаю, что Redux нельзя использовать для низкоуровневых задач. Просто если работа над ними отдаётся сторонним библиотекам, это усложняет понимание, и по закону тривиальности время тратится на мелочи, потому что каждому пользователю в итоге приходится строить собственный фреймворк по частям.


Некоторым такое по душе


И я в их числе! Но это относится далеко не ко всем. Я большой поклонник Redux и использую его почти во всех проектах, но мне также нравится пробовать новые конфигурации webpack. Я не типичный пример пользователя. Создание собственных абстракций на основе Redux даёт мне новые возможности, но что могут дать абстракции, написанные каким-нибудь Senior-инженером, который не оставил никакой документации и уволился полгода назад?


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


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


А как мы заботимся о мастерах? Почему мы утверждаем, что они всё делают не так, вместо того, чтобы поработать над удобством инструмента?


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


Ты серьёзно собираешься объяснять монады посреди поста?


Монады — это распространённая в Haskell абстракция, которая используется для самых разных вычислений, например, при работе со списками или обработке ошибок, состояний, времени или ввода/вывода. Синтаксический сахар в виде do-нотации позволяет представлять последовательности операций над монадами в форме, похожей на императивный код, примерно как генераторы в JavaScript, которые делают асинхронный код похожим на синхронный.


Первая проблема состоит в том, что не совсем правильно описывать монады с точки зрения их применения. Монады появились в Haskell для работы с побочными эффектами и последовательным вычислением, но в абстрактном смысле они не имеют с этим ничего общего: они просто представляют из себя набор правил взаимодействия двух функций, и в них не заложено никакого особого смысла. Похожим образом ассоциативность применима к арифметике, операциям над множествами, объединению списков и null propagation, но существует независимо от них.


Вторая проблема — это многословность, а значит, как минимум, визуальная сложность монад по сравнению с императивным подходом. Четко определенные опциональные типы вроде Maybe более безопасны, чем поиск подводных камней в виде null, но код с ними длиннее и несуразнее. Обработка ошибок с помощью типа Either выглядит понятнее, чем код, в котором где угодно может быть брошено исключение, но, согласимся, код с исключениями намного лаконичнее, чем постоянный возврат значений с Either. Что касается побочных эффектов в состоянии, вводе/выводе и т.д., то они и вовсе тривиальны в императивных языках. Любители функционального программирования (я в их числе) возразили бы, что работать с побочными эффектами в функциональных языках очень легко, но вряд ли кого-то получится убедить, что какое бы то ни было программирование — это легко.


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


Так к чему же всё это?


Дело в том, что с Redux такая же проблема — ему трудно обучать не потому, что он сложный, а как раз потому, что он простой. Понимание заключается скорее не в знании, а в доверии основному принципу, благодаря которому можно прийти ко всему остальному путём индукции.


Этим пониманием нелегко поделиться, потому что основные принципы сводятся к банальным аксиомам («избегайте побочных эффектов») или настолько абстрактны, что почти теряют смысл ((prevState, action) => nextState). Конкретные примеры не помогают: они только демонстрируют многословность Redux, но не показывают его выразительность.


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


И что ты предлагаешь?


Я хочу, чтобы мы осознали, что у нас есть проблема. Redux простой, но не лёгкий. Это оправданное решение разработчиков, но в то же время и компромисс. Многим людям пригодился бы инструмент, который бы частично пожертвовал простотой механизма в пользу простоты использования. Но в сообществе зачастую даже не признают, что какой-то компромисс вообще был сделан.


По-моему, интересно сравнивать React и Redux: хоть React и гораздо сложнее Redux и его API намного шире, как ни странно, его легче изучать и использовать. Всё, что действительно необходимо в API React, — это функции React.createElement и ReactDOM.render , а с состоянием, жизненным циклом компонентов и даже с событиями DOM можно справиться по-другому. Включение всех этих функций в React сделало его сложнее, но при этом и лучше.


"Атомарное состояние" — это абстрактная идея, и оно приносит практическую пользу только после того, как вы в нем разберётесь. С другой стороны, в React метод компонента setState управляет атомарностью состояния за вас, даже если вы не понимаете, как оно работает. Такое решение не идеально — было бы эффективнее отказаться от состояния вообще, или в обязательном порядке обновлять его при каждом изменении. Ещё этот метод может преподнести неприятные сюрпризы, если вызвать его асинхронно, но всё же setState намного полезнее для React в качестве рабочего метода, а не просто теоретического понятия.


И команда разработчиков Redux, и сообщество его пользователей активно выступают против расширения API, но склеивать десятки маленьких библиотек, как это делается сейчас, — утомительное занятие даже для экспертов и тёмный лес для новичков. Если Redux не сможет вырасти до уровня встроенной поддержки простых случаев, ему потребуется фреймворк-«спаситель», который займёт эту нишу. Jumpsuit мог бы стать неплохим кандидатом — он воплощает идеи действий и состояния в виде конкретных функций, при этом сохраняя характер отношений «многие ко многим». Но выбор спасителя не так важен, как сам факт спасения.


Вся ирония в том, что смысл существования Redux — это «Developer Experience»: Дэн разработал Redux, чтобы изучить и воспроизвести time-travel debugger как в Elm. Однако, по мере того, как идея приобретала собственную форму и превращалась, по факту, в объектно-ориентированную среду экосистемы React, удобство «DX» отошло на второй план и уступило гибкости конфигурации. Это вызвало расцвет экосистемы Redux, но там, где должен быть удобный фреймворк с активной поддержкой, пока зияет пустота. Готовы ли мы, сообщество Redux, заполнить её?


Перевод выполнен при содейтсвии Olga Isakova