Как я свой Redux писал

в 20:35, , рубрики: design patterns, flux, javascript, Model View Controller, react.js, ReactJS, redux, redux-thunk, software architecture, Разработка веб-сайтов

Или Охота на Кракена. В предыдущих заметках (тут и тут) я делился своим Braindump на тему различных архитектурных стилей, в частности Model-View-Controller и Flux.

Я отметил, что не увидел в лице Flux какой-то революции, этот шаблон не что-то новое. Я увидел в нем схожесть с Reenskaug-MVC 1979 года. Также, я упомянул, что решил убрать из своего кода Redux (одна из реализаций Flux). Мне кажется, эти моменты необходимо пояснить более развернуто. Моей целью не было убедить читателя в том, что Flux надо называть MVC, так же я не хотел сказать, что redux-модуль плох и от него нужно полностью отказаться.

Так как же относится тогда к Flux?

Для начала надо определится что же такое Flux. Во-первых это определенно архитектурный стиль, при чем на на данный момент, уже не только для клиентских web-приложений. Во-вторых это набор четко определенных компонентов и терминов.

Реализаций этой концепции уже с дюжину, это только самые популярные: Facebook/Flux, Redux, Fluxxor, Reflux, Vuejs/Vuex, Mobx. Есть экзотика в виде SAM.js (State-Action-Model).

Но важно то, что термины Диспетчер (Dispatcher), Действие (Action), Хранилище (Store) и Состояние (State) плотно заняли место в современных архитектурах клиентских приложений. Подобно тому, как раньше разработчик, пришедший в новую систему, искал слой Контроллера, теперь он ищет Хранилище или Диспетчер.

Так как же относиться к этому? Относиться определенно хорошо. Для меня Flux это SoC (Separation of Concerns) второго уровня, разделение ответственности в рамках конкретного MVC компонента. Это не новая идея, это то, что пытались сделать в Taligent-MVP, когда слой Контроллер был переосмыслен и разделен на несколько других слоев.

Это не замена, это следующий виток, это эволюция архитектурных шаблонов.

image
Из заметки “Все новое это хорошо забытое старое”

image
Из презентации И.Панина “Why the domain first?”

Мотивация

Разделение ответственности это подход, который позволяет не превратить код проекта в страшного макаронного монстра, чьи щупальца хаотично переплетаются, выстраивая лабиринты кодовых конструкций (Кракен, см. предыдущие заметки).

Однако, как я упомянул ранее, каждая концепция или шаблон начинаются с проблемы для которой они были придуманы. Facebook это крупная компания, это web-приложение колоссальных масштабов, объем интерактивности на высочайшем уровне. Именно для этого и был придуман Flux. Но и именно поэтому эта концепция была так сложно воспринята сообществом. Сколько компаний разрабатывают подобное ПО?

Задумайтесь, в скольких приложениях, которые вы разрабатывали, действительно необходимо несколько хранилищ? При разработке скольких ваших приложений и вправду помог бы этот шаблон?

Для каждого замка нужен свой ключ. Иногда ключи подходят к нескольким замкам, иногда мы силой его впихиваем и все таки открываем дверь. В голове так и всплывают фразы:

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

В. М. Турский (из книги Ф.Брукса “Мифический человеко-месяц”)

Я видел группы разработчиков, которые теряли головы от разрекламированных стандартов и забывали о своей главной задаче: реализовать интересы клиента

Р. Мартин

Redux полюбился сообществу. Именно эта реализация — это то, что нужно было среднему приложению. Когда я пересел на этот стек технологий, то первые приложения писались, можно сказать, стандартно. Использовалась связка React-Redux-Thunk(Middleware). Были и Создатели Действий (Action Creators), и строковые константы для типов Действий (Actions), были и так называемые Связанные Создатели Действий (Bound Action Creators). Все по гайду официального сайта.

Но в определенный момент стал вопрос, а как же эта пара магических модулей работает под капотом. Для разработчика вроде меня, который имел дело с кодовой базой Documentum Foundation Classes, читал исходники ExtJS 3.1, интегрированного в Webtop, разобраться с парой-тройкой функций redux-модуля не составит труда. Как и для большинства других опытных программистов. Но ни для кого не секрет, что на проектах работают разработчики разных уровней. Как добиться того, чтобы даже самые юные смогли разобраться, что происходит под этим самым “капотом”? Ведь программист должен знать свой инструмент.

Я столкнулся с тем, что не все разработчики, окружающие меня, понимали или хотя бы просто задумывались, как же работает эта магия, каким образом кролик появляется из шляпы. Это пожалуй был первый пункт, который послужил катализатором к тому, чтобы написать свою реализацию Flux/Redux. В конце концов лучший способ разобраться в чем-либо, переосмыслить что-либо, это самому написать это. Код будет доступен всей команде, каждый сможет что-то привнести или убрать, пройтись отладчиком и понять назначение любой строчки.

Пройдемся по остальным мотивирующим пунктам. Концепция Unidirectional Data Flow должна быть чистой. Однонаправленный поток данных это то, что позволит убить монстра со щупальцами. И, наверное, самый неопределенный момент при этом подходе — это асинхронность JavaScript и то, как она ложится на концепцию однонаправленного потока данных.

Есть несколько вариантов, которые люди выбирают, чтобы решить этот момент.
Один из примеров это Thunk. Лично для меня, middleware (промежуточные вычисления) выглядит как некий побочный эффект в рамках архитектурного стиля Flux. Попытка заткнуть дырку, которая не позволяет использовать шаблон в чистом виде. Это как классы Utils, Helper или Service в ООП. Когда разработчик не может вынести логику в какой-то конкретный бизнес-класс, то у него появляется это волшебное место, куда он начинает складывать всю эту побочную (полезную?) функциональность. На самом деле этот побочный эффект, это ни что иное, как особенность нашего Хранилища. Хранилище персистентно, и это то, что мешает нам думать о данных через призму односложных CRUD-операций.

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

Thunk и bindActionCreators — это побочное явление для меня. Я выбираю подход при котором второй аргумент функции connect, а именно mapDispatchToProps, мне не понадобится. Кстати, кто-нибудь когда-нибудь использовал в своих проектах третий аргумент функции connect? Может таинственный флаг pure?

Второй пункт который послужил катализатором это избыточность используемых инструментов.

К слову, одна из любимых фраз Никлауса Вирта:

Инструмент должен соответствовать задаче. Если инструмент не соответствует задаче, нужно придумать новый, который бы ей соответствовал, а не пытаться приспособить уже имеющийся

Третий пункт это “потому что могу”. Да-да, именно так, мы тоже программисты, мы тоже можем писать свои инструменты. Я не призываю переписать под себя, к примеру, React или Spring-фреймворк. Но и тащить весь Spring в проект, которому это не нужно, не имеет смысла. Мне, к примеру, абсолютно не лень потратить один субботний вечер на свою маленькую реализацию Flux, которая будет удовлетворять потребностям моих приложений. Мне это даже в удовольствие.

Велосипед или, пускай у него будет имя, Artux

image

Redux написан в функциональном стиле, в вопросе парадигм программирования я за золотую середину, JavaScript удивительный язык, который открывает нам мультипарадигменный занавес. Я за то самое равновесие темной и светлой стороны.

Но классами легче описывать интерфейсы. Когда ты говоришь «Хранилище», проще воспринимать его, как объект с определенным API-методами, нежели набор функций, объединенных одним названием.

Ниже как раз интерфейс моей реализации Хранилища. Он очень похож на вариант Redux, за исключением того, что в публичных методах появилась функция unsubscribe. В redux-модуле она замкнута в функции subscribe.

image

Конструктор Хранилища принимает на вход карту Моделей (Models) с соответствующими этим моделям Обработчиками Действий (Reducers/Receivers).
Модель это логическое именованное подразделение Хранилища, которое соответствует определенной бизнес-сущности, части домена. В моем представлении Хранилище содержит только бизнес информацию, никаких состояний графических виджетов, типа флажков или переключателей.

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

Ниже интерфейс класса StoreProvider (Провайдер Хранилища), он почти идентичен оригиналу и просто ложит экземпляр Хранилища в контекст приложения.

image

Далее описание интерфейса компонента высшего порядка (Higher-order Component/HoC), который порождает функция subscribeToStore, замена функции connect.

image

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

И последний компонент моей скромной реализации. Интерфейс Диспетчера.

image

Основное назначение компонента — это отсылка сообщений Хранилищу. Интерфейс предоставляет метод позволяющий создавать объект-действие, а также методы по диспетчеризации действия, которые “хоронят” в себе асинхронность персистентных операций. Обращу внимание читателя, что обещание не выбрасывается наружу, любые реакции в системе происходят только на почве изменения состояния определенной модели.

Действие представляет из себя JSON-объект, содержащий следующие поля:

  • Тип (Type) — строковый литерал, наименование действия пользователя;
  • Полезная Информация (Payload) — это может быть скалярное значение или структура данных;
  • Мета или Побочная Информация (Meta) — через это свойство можно пробросить дополнительные данные, например входные аргументы, необходимые для выполнения операции сопоставимой действию;
  • Индикатор Ошибки (Error) — булево значение, отражающее успех или провал операции сопоставимой действию;

Также, с использованием es6-объектов Proxy и Reflect, был добавлен механизм логирования в консоль изменения состояния для более удобной отладки.

image

Устройство приложения

На верхнем уровне представления находятся Страницы (Pages). Это, можно сказать, компоненты-контейнеры, которые являются родителем для всего дерева графических виджетов.

Страницы это как раз те самые HoC-компоненты, которые подписываются на изменение состояния Хранилища, пробрасывая поток данных вниз по дереву. Также, каждый такой объект имеет собственный слой Взаимодействия (Interactors). Этот слой сочетает в себе два типа операций: переходы между Страницами (Pages) или Якорями (Anchors) и инициализация выполнения Действий (Actions / Commands).

Класс взаимодействия это, по сути, мост, позволяющий объединить в одном месте коммуникацию с Диспетчером и Маршрутизатором (Router). Ниже пример интерфейса такого класса.

image

Каждой модели в системе соответствует ее Обработчик (Reducer/Receiver), именно он на основе полученного действия меняет или не меняет состояние модели.

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

Мой вариант обработчика — это Редукторы (Reducers). Меня всегда раздражали тонны констант, которые приходилось писать для вилки типов действий (PENDING, SUCCESS, FAIL). Во-первых я решил остановиться на литералах прямо в обработчиках. Во-вторых, я считаю, что объект-действие представляет из себя очень гибкий инструмент, который мог бы нести в себе всю необходимую информацию. В-третьих, лично мне, как-то сложно воспринимать действие пользователя, которое называется GET_ITEMS FAIL. Согласитесь, это не то действие, которое конечный пользователь хотел выполнить, он выполнял GET_ITEMS, и именно это действие и должно доходить до модели в хранилище.

image

image

Теперь можно отобразить общую схему приложения. Она могла бы выглядеть так

image

#DMP_END

Как и любая схема, эта архитектура может тоже превратиться в осьминога со щупальцами. Самое “толстое” место в этом варианте — это представление. Но тут нам призван помочь сам React, библиотека сосредоточенная именно на реализации слоя представления. Также существует ряд техник, к примеру, компонентное наследование (HoC and OOP Inherits). Задача примерно проста — не допустить рождение нового чудовища — FSUV (Fat Stupid Ugly View).

Я должен уточнить, что при описании, я ссылался на модуль redux версии 3.6.0. Буду честен, я особо не выиграл в производительности, рендеринг страниц происходит за тоже время. Но если вдруг этот модуль и вовсе пропадет из npm, то как вы уже поняли, сильно грустить я не буду.

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

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

На этой ноте я оборву свой хаотичный дамп, темой которого был кавардак из мыслей об архитектуре Flux.

Ссылки

+ Flux Standard Actions
+ Redux
+ Flux
+ I.Panin “Why the domain first?”

Автор: Artur Basak

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js