В чём сила Redux?
- суббота, 22 июля 2017 г. в 03:12:47
Это перевод статьи "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 — это просто дополнительное перенаправление без должного уровня абстракции.
Однако главный подвох состоит в том, что после такой долгой работы над бойлерплейтами для простых случаев вообще забываешь, что для сложных случаев есть решения и получше. Столкнувшись с хитроумным переходом состояний в итоге отправляешь десяток разных 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