Redux среди нас
Это один из самых популярных state-manager`ов.
Он прост в использовании, прозрачен и предсказуем. С его помощью можно организовать хранение и изменение данных. А если считать, что action`ы и reducer`ы являются частью redux`а, то можно без преувеличения утверждать, что он является держателем всей логики предметной области (aka бизнес-логики) наших приложений.
А все ли так радужно?
При всей своей простоте и прозрачности, использование redux`а обладает рядом недостатков…
Данные в state`е redux`а лежат в простом javascript-объекте и могут быть получены привычным образом, нужно лишь знать полный путь.
Но как мы организуем эти данные? Есть всего 2 варианта: плоский список и иерархическая модель.
Плоский список — отличный вариант для приложения в котором есть только, например, счетчик… Для чего-то более серьезного нам понадобится иерархическая структура. При этом каждый уровень иерархии будет определять логическую составляющую данных — к чему они относятся.
const dataHierarchy = { user: { id, name, experience, achievements: { firstTraining, threeTrainingsInRow, }, }, training: { currentSetId, status, totalAttemptsCount, attemptsLeft, mechanics: { ... }, }, };
Тут под ключом user хранятся данные пользователя, в частности achivements. Но достижениям не нужно ничего знать про остальные данные пользователя.
Точно так же, конкретной механике тренировки не нужно знать, сколько попыток осталось у пользователя — это данные training в целом.
Наличие иерархической структуры данных и отсутствие модульного подхода к этим данным приводит к тому, что в каждом месте, где эти данные используются, необходимо знать полный путь до них. Другими словами, это создает связность структуры хранения данных и структур их отображения и приводит к сложностям с рефактором путей и/или реорганизацией структуры данных.
Еще одной сложностью является тестирование. Да, в документации к redux тестированию посвящена отдельная статья, но сейчас речь идет не о тестировании отдельных артефактов вроде reducer`ов и action-creater`ов.
Данные, action`ы и reducer`ы как правило взаимосвязаны. А одно дерево логически связанных данных часто обслуживается несколькими reducer`ами, тестировать которые нужно в том числе и вместе.
Добавим к этому списку selector`ы, результаты которых зависят в частности от работы reducer`ов…
В общем, протестировать это все можно, но придется иметь дело с пачкой артефактов, связанных между собой только логикой и соглашениями.
А что делать, если мы придумали структуру, к примеру, с данными пользователя, включающими списки друзей, любимых песен и чего-нибудь еще, а так же функционал их изменения через action`ы-reducer`ы. Возможно, мы даже написали пачку тестов на весь этот функционал. А теперь в соседнем проекте нам нужно то же самое…
Как дешево пошарить код?

Поиск решения
Для того, чтобы придумать как сохранить плюсы redux`а и избавиться от описанных недостатков, надо понять, что от чего зависит в жизненном цикле данных:
- Action`ы сообщают о произошедших событиях, пользовательских и не только
- Reducer`ы реагируют на action`ы и изменяют или не изменяют состояние данных
- Изменение данных — само по себе событие и может быть причиной изменения других данных
Controller в данном случае является абстракцией, обрабатывающей как действия пользователя, так и изменения данных в store`е. Ему вовсе не обязательно быть отдельным классом, как правило, он размазан по компонентам.
Объединяем весь redux-зоопарк в черный ящик
А что если завернуть action`ы, reducer`ы и selector`ы в один модуль и научить его не зависеть от конкретного пути для расположения своих данных?
Что если все действия user`а, для примера, будут совершаться посредством вызова метода экземпляра: user.addFriend(friendId)? А данные получаться через getter: user.getFriendsCount()?
Что если мы сможем импортировать весь функционал пользователя простым import`ом?
const userModule from ‘node_modules/user-module’;
Удобно? Особенно учитывая, что для этого не надо писать кучу лишнего кода:
npm пакет redux-module-creator предоставляет весь функционал для создания слабосвязных, переиспользуемых и тестируемых redux-модулей.
Каждый модуль состоит из controller`а, reducer`а и action`ов и имеет следующие особенности:
- интегрируется в store через вызов метода-интегратора, при этом для изменения места интеграции надо поменять только место вызова интегратора и его параметр
- контроллер имеет связь со своей частью данных в store`е, запоминая путь, который единожды передается в integrator(). Это исключает необходимость знать его при использовании данных
- контроллер является держателем всех необходимых селекторов, адаптеров и т.д.
- для отслеживания изменений есть возможность подписываться на изменения именно в “своих” данных
- reducer`ы могут использовать контекст вызова — экземпляр модуля и получать из него actionType`ы, принадлежащие модулю. Это исключает необходимость импортировать кучу action`ов и снижает вероятность ошибки
- аction`ы получают контекст использования, поскольку становятся частью экземпляра модуля: теперь это не просто trainingFinished, а readingModule.actions.trainingFinished
- action`ы теперь существуют в пространстве имен модуля, что позволяет создавать для разных модулей события с одинаковыми именами
- каждый модуль может быть инстанциирован несколько раз, а каждый экземпляр — интегрирован в разные части store`а
- action`ы для разных экземпляров модуля имеют разные actionType — можно реагировать на события конкретного экземпляра
В итоге мы получаем черный ящик, который умеет сам управлять своими данными и имеет ручки для общения с внешним кодом.
При этом, это все тот же redux, с его однонаправленным потоком данных, прозрачностью и предсказуемостью.
А поскольку это все тот же redux и все те же reducer`ами, из них можно строить любую структуру, которую требует логика предметной области вашего приложения.
ссылка на оригинал статьи https://habr.com/ru/post/459460/
Добавить комментарий