Русский
Русский
English
Статистика
Реклама

Flux

Фреймворк-независимое браузерное SPA

14.03.2021 16:17:43 | Автор: admin

1. Но... зачем?

  1. Существует огромное количество фреймворков для разработкиSPA(Single Page Application).

  2. Существует огромное количество документации, иллюстрирующей как создавать приложение набазе конкретного фреймворка.

  3. Однако всякая подобная документация ставит фреймворк воглаву угла. Тем самым превращая фреймворк издетали реализации вопределяющий фактор. Таким образом значительная часть кода пишется недля удовлетворения нужд бизнеса адля удовлетворения нужд фреймворка.

Учитывая насколько hype-driven является разработка софта внаше время, можно быть уверенным втом что через несколько лет будут существовать новые модные фреймворки для фронтенд разработки. Вмомент когда фреймворк набазе которого построено приложение выходит измоды вывынуждены либо поддерживать устаревшую (legacy) кодовую базу либо стартовать процесс перевода приложения нановый фреймворк.

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

Данная статья является примером построения SPA сиспользованием высокоуровневых принципов дизайна архитектуры. При этом конкретные библиотеки ифреймворки выбираются для удовлетворения ответственностей, определённых желаемой архитектурой.

2. Архитектурные цели иограничения

Цели:

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

  2. Стимулируется разделение ответственностей (separation ofconcerns) иследовательно модульность кода так что:

    • Модули легко поддаются тестированию

    • Интеграции свнешними сервисами (boundaries) атакже грязные хаки иворкэраунды вынесены вотдельные модули инепротянуты через несколько различных файлов. Таким образом смена реализации интеграции ссервисом или отказ отхака становится реалистичной задачей анедолгосрочным рефакторингом

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

  4. Позволяет реализовать слабую связность между модулями. Как следствие изменение реализации модуля (или его замена) недолжна приводить кизменению кода, использующего данный модуль.

  5. Механики взаимодействия модулей неприводят кнедопустимым проблемам спроизводительностью.

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

Ограничения:

Приложение должно работать вбраузере. Следовательно оно должно быть написано сиспользованием (или скомпилированов) HTML+CSS для определения статического интерфейса иJavaScript для добавления динамического поведения.

3. Ограничим тему данной статьи

Существует большое количество архитектурных подходов кструктурированию кода. Наиболее распространенные наданный момент: слоеная (layered), луковичная (onion) ишестигранная (hexagonal). Беглое сравнение было дано вмоей предыдущейстатье.

Данная статья ограничивается слоем представления втерминологии слоеной/луковичной архитектур поскольку большинство SPA занимается исключительно отображением данных. Таким образом слои домена (domain) иприложения (application) могут быть проигнорированы. Как следствие, наиболее естественный способ понять назначение такого приложения получить обзорное представление ослое представления.

Тем неменее данная статья определяет механики взаимодействия сослоями приложения идомена, вслучае когда такие слои присутствуют.

Интересно отметить что вслучае отсутствия вышеупомянутых слоев приложение напоминает классическую шестигранную структуру (также называемуюPorts and Adapters) вкоторой представлениеявляетсяприложением. Взгляните наинтеграцию сlocalStorage вTodoMVCпримере созданном вкачестве иллюстрации кданной статье (папкаboundaries/local-storage).

4. Структура файлов. Как заставить SPAкричать?

Будем исходить из терминологии дяди Боба.

Рассмотрим типичный онлайн магазин. Приблизительно так онмогбы быть нарисован насалфетке владельцем бизнеса:

Рисунок1: типичный онлайн магазин, нарисованный насалфетке

Каким может быть наиболее кричащий способ структурировать кодовую базу? Нарисунке 2все страницы отражены как папки.

Рисунок2: структура папок верхнего уровня, отражающая страницы определённые нарисунке 1

Заметим что мыдобавили папку shared как место где будут определены общие UIблоки, такие как шаблон, панель навигации, корзина.

Наши страницы построены излогических (ивидимых) частей. Пока что назовем их блоками иположим впапку сименем parts. Посмотрим что получилось (рисунок 3).

Рисунок3: размещение вложенных блоков внутри подпапки parts

Как видно, вложенность выглядит отвратительно уже для второго уровня для страницы goods catalogue. Путь goods-catalogue/parts/goods-list/parts/good-details.jsуже награнице адекватной длины пути кфайлу. При том что вреальных приложениях два уровня вложенности далеко непредел.

Давайте избавимся отпапок parts вфайловой структуре. Посмотрим нарисунок 4.

Рисунок4: вложенные блоки вынесены изпапок parts

Теперь внутри пути goods-catalogue/goods-listнаходится три файла.goods-list.js(родительский) расположен между файлами, определяющими вложенные внего блоки. Вреальных проектах, учитывая кол-во разнородных файлов (js, html, css) это приводит кневозможности разделить файлы, определяющие текущий блок ифайлы, отвечающими завложенные внего блоки.

Решение:

  1. Если конкретный блок определяется несколькими файлам создаем для него папку.

    • goods-listявляется блоком исостоит изболее чем одного файла, потому для него создана папка.

    • filtersявляется блоком состоящим изодного файла, потому для него несоздана отдельная папка.

  2. Если конкретный блок (неважно изодного файла или изнескольких) являетсявложенным блоком добавим кназванию файла префикс _. Таким образом все вложенные блоки будут подняты кверху папки вфайловом обозревателе.

    • _goods-list folderявляется вложенным блоком относительноgoods-catalogueсоответственно кназванию папки добавлен префикс.

    • goods-list.jsявляется частью определения блока_goods-listсоответственно префикс недобавлен.

    • _good-details.jsявляется вложенным блоком относительно_goods-listсоответственно префикс добавлен.

Рисунок5: использование префикса _ для разделения вложенных блоков отихродителей

Готово! Теперь открывая папку сблоком мыможем сразуже увидеть иоткрыть основной файл, определяющий данный блок. После чего при необходимости перейти квложенному блоку. Обратите внимание что папкаpagesбыла переименована вcomponentsнарисунке 5. Так сделано поскольку страницы иблоки логически являются разными вещами новтерминологии HTML итоидругое можетбы представлено какcomponent. Сэтого момента папкаcomponentsявляется основной папкой нашего приложения, домом для слоя представления.

5. Язык разработки. JavaScript?

Единственный язык который может быть выполнен вбраузере это JavaScript. Существует множество статей посвященных его несуразности. Выможетепосмеяться онем (тайм код1-20), ноэто только веселая часть...

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

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

Глубокий анализ возможных опций лежит вне границ данной статьи, номой персональный выборTypeScriptпосколькуон:

  • Обеспечивает проверку типов наэтапе компиляции

  • Будучи над-множеством JavaScript, может выполнять JavaScript код без дополнительного интеграционного кода

  • Определения типов (typings) могут быть добавлены поверх существующего JavaScript кода без его изменения. Благодаря простоте этой возможности, большинство существующих npm пакетов уже покрыты тайпингами. Таким образом выможете использовать эти пакеты так, как будтобы они являются TypeScript пакетами. Соответственно ихиспользование также является типо-безопасным.

Хинт: рекомендую посмотреть всторонуasm.js,blazorиelmесли вызаинтересованы вдругих опциях

6. Требования кдизайну приложения

Давайте вспомним ограничения, накладываемые браузерами: HTML, CSS, JavaScript. Также вспомним структуру файлов, определенную вразделе4: дерево директорий, отражающее дерево визуальных элементов.

Таким образомпервой целью [6.1]будет возможность определения компонентов средствами HTML иCSS иихпоследующее переиспользование другими компонентами.

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

Таким образомвторой целью [6.2]является возможность определить TypeScript интерфейсы отражающие все свойства, используемые вшаблоне (компоненте).После чего наэтапе компиляции выбросить исключение вслучае если вразметке компонента происходит обращение кнеопределенному свойству.

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

Таким образомтретьей целью [6.3]является возможность компонентов принимать данные изатрибутов иизхранилищ одновременно. Компоненты должны быть перерисованы при изменении любой части принимаемых данных.

Четвертой целью [6.4]станет определение требований ктаким хранилищам:

  • Должна существовать возможность использовать хранилище несколькими компонентами, каждый изкоторых отображает лишь часть общего набора данных.

  • Должна существовать возможность создавать хранилища вместе сконкретным компонентом. Это необходимо для случая когда различные экземпляры компонента автономны друг отдруга идолжны иметь различные наборы данных.

  • Хранилища должны иметь возможность использовать сервисы ифункции слоев Domain иApplication. Воизбежание сильной связности между хранилищем играницами приложения, сервисы должны быть использованы спомощью механизмаDependency Injection. Хранилища должны ссылаться только наинтерфейсы.

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

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

  • Лучшей читаемости кода, т.к. разработчик может предположить назначение компонента изнабора данных принимаемых этим компонентом.

  • Лучшей производительности т.к. можно избежать перерисовки компонента вслучае изменения неиспользуемых данных.

Таким образом,пятая цель [6.5] позволить хранилищам данных быть определенными как классические TypeScript классы. Обозначить механику определения среза данных, используемого конкретным компонентом.

Держа эти цели вголове, давайте перечислим необходимые логические блоки кода:

  • Компоненты (Components) строго типизированные HTML шаблоны + CSS стили

  • Модели вида (ViewModels) классы, инкапсулирующие состояние данных, используемое компонентом (ивсей иерархией компонентов под ним).

  • Фасады моделей вида (ViewModel facades) ограничивают видимость свойств модели вида теми, которые используются вконкретном компоненте.

Рисунок6: желаемая структура кода вслое представления

  • Не-пунктирные стрелки отражают рендеринг компонентов родительскими компонентами. Направление стрелки отражает направление передачи атрибутов.

  • Пунктирные линии отражают зависимости одних логических кусков кода отдругих (ссылки).

  • Блоки сзеленой рамкой границы модуля. Каждый модуль/подмодуль отражен выделенной под него папкой. Общие модули лежат впапке shared.

  • Голубые блоки модели вида. Модели вида определены поштуке намодуль/подмодуль.

Что упущено? Заметьте как модели вида нарисунке 6не имеют никаких параметров. Это всегда справедливо для модулей верхнего уровня (страниц) иглобальных моделей вида. Ноподмодули зачастую зависят отпараметров, определённых впроцессе работы сприложением.

Обозначимшестую цель [6.6] позволить атрибутам подмодуля быть использованными моделью вида этого подмодуля.

Рисунок7: атрибуты передаются нетолько вкорневой компонент модуля ноивего модель вида

7. Техническая реализация

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

7.1. Компоненты

Для отрисовки строго-типизированной разметки можно использовать синтаксис tsx (типизированныйjsx). Рендеринг tsx поддерживается различными библиотеками, такими какReact,PreactandInferno. TsxНЕявляется чистым HTML, тем неменее онможет быть автоматически сконвертирован в/из HTML. Потому зависимость отtsx мне кажется допустимой т.к.вслучае миграции начистый HTML, значительная часть работы может быть выполнена автоматически.

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

Хинт: впоследние годы функциональные компоненты ввиде чистых функций вышли измоды всообществе React. Использованиеreact hooksнаделяет функциональные реакт компоненты сайд-еффектами ипоощряет смешивание рендера слогикой управления состоянием. Хуки являются специфическим API для React инедолжны использоваться при разработке вподходе, описанном вданной статье.

Другими словами,компонентылишены состояния. Представим ихчерез выражение UI=F(S) где

  • UI видимая разметка

  • F определение компонента

  • S текущее значение данных внутри модели вида (здесь идалее вьюмодели)

Пример компонента может выглядет так:

interfaceITodoItemAttributes{name:string;status:TodoStatus;toggleStatus:()=>void;removeTodo:()=>void;}constTodoItemDisconnected=(props:ITodoItemAttributes)=>{constclassName=props.status===TodoStatus.Completed?'completed':'';return(<liclassName={className}><divclassName="view"><inputclassName="toggle"type="checkbox"onChange={props.toggleStatus}checked={props.status===TodoStatus.Completed}/><label>{props.name}</label><buttonclassName="destroy"onClick={props.removeTodo}/></div></li>)}

Этот компонент отвечает заотрисовку одного todo элемента внутриTodoMVCприложения.

Единственная зависимость вэтом коде это зависимость отсинтаксиса JSX. Соответственно этот компонент может быть отрисован различными библиотеками. Стаким подходом замена библиотеки для отрисовки все еще неявляется бесплатной, ноявляется реалистичной.

Итого мыдостигли целей[6.1]и[6.2].

Хинт: яиспользую react дляTodoMVC приложенияприведенного вкачестве примера.

7.2. Модели Вида (вьюмодели)

Как было сказано ранее, мыхотим чтобы вьюмодели были написаны ввиде TypeScript классов стем что-бы:

  • Обеспечивать инкапсуляцию данных.

  • Предоставлять возможность взаимодействия сослоями domain/application посредством механизма dependency injection.

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

Применим принципы реактивного интерфейса (reactive UI). Подробное описание этих принципов приведено вэтом документе. Данный подход был впервые представлен вWPF (C#) иназванModel-View-ViewModel. ВJavaScript сообществе, объекты предоставляющие доступ кобозреваемым (observable) данным чаще называются хранилищами (stores) следуя терминологииflux. Отмечу чтохранилищеэто очень абстрактный термин, онможет определять:

  • Глобальное хранилище данных для всего приложения.

  • Доменный объект, инкапсулирующий логику логику бизнеса инепривязанный кконкретному компоненту ноинеявляющийся глобальным.

  • Локальное хранилище данных для конкретного компонента или иерархии компонентов.

Таким образом любая вьюмодель является хранилищем, нонекаждое хранилище является вьюмоделью.

Определим ограничения креализации вьюмоделей:

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

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

Яиспользуюmobxдекораторы для того, чтобы сделать поля класса обозреваемыми. Пример вьюмодели:

classTodosVM{@mobx.observableprivatetodoList:ITodoItem[];//use"poormanDI",butintherealapplicationstodoDaowillbeinitializedbythecalltoIoCcontainerconstructor(props:{status:TodoStatus},privatereadonlytodoDao:ITodoDAO=newTodoDAO()){this.todoList=[];}publicinitialize(){this.todoList=this.todoDao.getList();}@mobx.actionpublicremoveTodo=(id:number)=>{consttargetItemIndex=this.todoList.findIndex(x=>x.id===id);this.todoList.splice(targetItemIndex,1);this.todoDao.delete(id);}publicgetTodoItems=(filter?:TodoStatus)=>{returnthis.todoList.filter(x=>!filter||x.status===filter)asReadonlyArray<Readonly<ITodoItem>>;}///...othermethodssuchascreationandstatustogglingoftodoitems...}

Обратите внимание что мыссылаемся наmobx напрямую, однако декораторы неприсутствуют втеле методов.

Япродемонстрирую как абстрагировать реактивность иубрать зависимость отmobx вследующей статье. Пока что посчитаем достаточным обращаться кдекораторам через пространство имен mobx. При таком ограничении заменить декораторы надекораторы издругой библиотеки можно будет спомощью автоматизированного скрипта.

Также обратите внимание что конструктор вьюмодели принимает первый аргумент типа{status:TodoStatus}. Это позволяет удовлетворитьцели [6.6]. Тип должен совпадать стипом определяющим атрибутыкорневого компонентамодуля. Ниже обобщенный интерфейс вьюмодели:

interfaceIVMConstructor<TProps,TVMextendsIViewModel<TProps>>{new(props:TProps,...dependencies:any[]):TVM;}interfaceIViewModel<IProps=Record<string,unknown>>{initialize?:()=>Promise<void>|void;cleanup?:()=>void;onPropsChanged?:(props:IProps)=>void;}

Все методы вьюмодели необязательны. Они могут быть определены для:

  • Выполнения кода при создании вьюмодели

  • Выполнения кода при удалении вьюмодели

  • Выполнения кода при изменении атрибутов (под-)модуля.

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

Как показано нарисунке7, точкой входа для модуля является его корневой компонент. Таким образом вьюмодель должна быть создана когда корневой компонент модуля добавлен вструктуру DOM(mounted) иудалена когда онудаляется состраницы(unmounted). Решить эту задачу можно спомощью техники компонентов высшего порядка (higher order components).

Определим тип функции:

typeTWithViewModel=<TAttributes,TViewModelProps,TViewModel>(moduleRootComponent:Component<TAttributes&TViewModelProps>,vmConstructor:IVMConstructor<TAttributes,TViewModel>,)=>Component<TAttributes>

Эта функция возвращает компонент высшего порядка над moduleRootComponent, который:

  • Должен обеспечить создание вьюмодели перед созданием имонтированием (mount) компонента.

  • Должен обеспечить зачистку(удаление) вьюмодели при демонтировании (unmount).

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

Пример использования данной функции:

constTodoMVCDisconnected=(props:{status:TodoStatus})=>{return<sectionclassName="todoapp"><Header/><TodoListstatus={props.status}/><FooterselectedStatus={props.status}/></section>};constTodoMVC=withVM(TodoMVCDisconnected,TodosVM);

Вразметку корневой страницы приложения (либо роутера, зависит оттого что как построено ваше приложение), результирующий компонент будет вставлен как<TodoMVCstatus={statusReceivedFromRouteParameters}/>. После чего, экземплярTodosVMстановится доступным для всех под-компонентов внутри компонентаTodoMVC.

Важно что реализация создания вьюмодели, атакже доступности вьюмодели для дочерних компонентов, скрыта внутри withVM.

  • TodoMVCDisconnected компонент независит отбиблиотеки рендера

  • TodoMVC компонент может быть прорендерен вкомпоненте, независящем отбиблиотеки рендера

  • TodosVM ссылается только надекораторы. Потому, как описано выше, еёотвязка отmobx реальна.

Хинт: вреализации изпримера, функцияwithVMзависит отreact context API. Выможете попробовать реализовать аналогичное поведение вобход контекст апи. Важно, что реализация должна быть синхронизирована среализацией доступа квьюмодели изфасадов вьюмоделей смотрите описание функцииconnectFnвследующем разделе.

7.3. Фасады вьюмоделей

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

Попробуем вместо классических фасадов использовать функции, принимающие вьюмодель (или несколько вьюмоделей) ивозвращающие набор функций/данных, необходимых конкретному компоненту. Назовем ихфункциями среза (slicing function). Что если такая функция будет получать атрибуты компонента, который она обслуживает, вкачестве последнего аргумента?

Рисунок8: передача атрибутов компонента фасаду вьюмодели (функции среза/slicing function)

Посмотрим насинтаксис (вслучае одной вьюмодели):

typeTViewModelFacade=<TViewModel,TOwnProps,TVMProps>(vm:TViewModel,ownProps?:TOwnProps)=>TVMProps

Выглядит очень похоже нафункцию connectизбиблиотеки Redux. Стой лишь разницей что вместо аргументовmapStateToProps,mapDispatchToActionsиmergePropsмы имеем один аргумент функцию среза, которая должна вернуть данные иметоды одним объектом. Ниже пример функции среза для компонентаTodoItemDisconnectedивьюмоделиTodosVM.

constsliceTodosVMProps=(vm:TodosVM,ownProps:{id:string,name:string,status:TodoStatus;})=>{return{toggleStatus:()=>vm.toggleStatus(ownProps.id),removeTodo:()=>vm.removeTodo(ownProps.id),}}

Заметка: Яназвал аргумент функции, содержащий атрибуты компонента OwnProps что-бы приблизить его ктерминологии применяемой вreact/redux.

Такая схожесть диктует наиболее естественный способ использования функций среза спомощью компонентов высшего порядка. Представим что вьюмодель уже хостится компонентом выше вдереве компонентов спомощью функцииwithVM. Давайте определим тип функции, которая принимает функцию среза икомпонент, авозвращает компонент высшего порядка, привязанный квьюмодели:

typeconnectFn=<TViewModel,TVMProps,TOwnProps={}>(ComponentToConnect:Component<TVMProps&TOwnProps>,mapVMToProps:TViewModelFacade<TViewModel,TOwnProps,TVMProps>,)=>Component<TOwnProps>constTodoItem=connectFn(TodoItemDisconnected,sliceTodosVMProps);

Отрисовка такового компонента всписке todo элементов:<TodoItemid={itemId}name={itemName}status={itemStatus}/>

Заметьте чтоconnectFnскрывает детали реализации реактивности:

  • Она берёт компонентTodoItemDisconnectedифункцию срезаsliceTodosVMProps обе незнающие ничего ореактивности иобиблиотеке для рендеринга JSX.

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

Смотрите нареализациюфункции connectFnдля TodoMVCприложения, сделанного вкачестве примера.

8. Заключение

Итого весь код, относящийся кконкретным бизнес задачам приложения, независим отфреймворков. TypeScript объекты, функции, TSX это все кчему мыпривязаны.

Надеюсь что прочтение этой статьи продемонстрировало пользу проработки архитектуры SPA приложения вперёд старта разработки. Буду счастлив если майндсет хотябы одного разработчика настарте разработки SPA изменится сберем свежий фреймворк ивсе должно быть хорошо наподумаем что конкретно нужно сделать ивыберем подходящие инструменты.

Всеже, можетли слой представления быть полностью независим отфреймворков вреальном приложении?

Для того что-бы убрать ссылки наmobx, react иmobx-react изслоя представления, нужно сделать немного больше:

  • Абстрагироваться отmobx декораторов

  • Абстрагировать все фреймворко-зависимые библиотеки, используемые слоем представления. КпримеруTodoMVCзависит отбиблиотек react-router иreact-router-dom.

  • Абстрагироваться отсинтетических событий, специфичных для конкретной библиотеки, отрисовывающей JSX.

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

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

P.S. Сравнение рассмотренной структуры иеереализации спопулярными фреймворками для разработки SPA:

  • Всравнении сосвязкойReact/Redux: вьюмодели заменяютreducers,action creatorsиmiddlewares. Вьюмодели содержат состояние (являются stateful). Нет time-travel. Множество хранилищ. Отсутствие просадки производительности вызванной наличием большого числа использований функции connect скакой тологикой внутри. Redux-dirven приложения становятся все медленнее имедленнее стечением времени иззадобавления новых connected компонентов вприложение. При этом несуществует какого токонкретного ботлнека, устранением которого можно былобы исправить ситуацию.

  • Всравнении сvue: строго типизированные представления благодаря TSX. Вьюмодели являются обычными классами инетребуют использования функций сторонних библиотек, равно как необязаны удовлетворять интерфейсу, определенному сторонними фреймворками. Vue.js заставляет определять состояниевнутри определенной структурыимеющей свойства data,methods, ит.д. Отсутствие vue-специфических директив исинтаксиса привязки кмодели.

  • Всравнении сangular: строго типизированные представления благодаря TSX. Отсутствие angular-специфических директив исинтаксиса привязки кмодели. Инкапсуляция данных внутри вьюмоделей вотсутствие двусторонней привязки данных (two-way data binding).Хинт: для определенных сценариев, таких как формы, двусторонняя привязка данных удобна иполезна.

  • Всравнении счистым react где управление состоянием выполняется спомощью хуков (hooks, такие какuseState/useContext):Лучшее разделение ответственностей. Вьюмодели могут восприниматься втерминологии реакта как контейнер компоненты, которые лишены возможность рендерить что-либо иявляются ответственными исключительно заработу сданными. Нет необходимости:

    • следить запоследовательностью вызова хуков.

    • отслеживать зависимости хуков useEffect внутри deps массива.

    • проверять смонтированли все еще компонент после каждого асинхронного действия.

    • следить что замыкания изпредыдущих рендеров неиспользуются внутри обработчика хука эффекта.

    Как любая технология, хуки (ивчастности useEffect) требует разработчика следовать некоторым рекомендациям. Эти рекомендации неявляются частью интерфейсов, ноприняты как подход, модель мышления (mental model) или стандартные практики (best practices). Прекраснаястатья про использование хуковотчлена команды разработки react. Прочитайте ееиответьте себе надва вопроса:

    • Что выполучаете используя хуки?

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

  • Всравнении сreact-mobx интеграцией. Структура кода неопределяется пакетом react-mobx инепредлагается документацией кнему. Разработчик должен придумать подход кструктурированию кода сам. Рассмотренную встатье структуру можно считать таким подходом.

  • Всравнении сmobx-state-tree: Вьюмодели являются обычными классами инетребуют использования функций сторонних библиотек, равно как необязаны удовлетворять интерфейсу, определенному сторонними фреймворками.Определение типавнутри mobx-state-tree опирается наспецифические функции этого пакета. Использование mobx-state-tree всвязке сTypeScript провоцирует дублирование информации поля типа объявляются как отдельный TypeScript интерфейс нопри этом обязаны быть перечислены вобъекте, используемом для определения типа.

Оригинал статьи наанглийском языке вблоге автора (меня же)

Подробнее..

За что я не люблю Redux

19.06.2021 18:15:23 | Автор: admin

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

Flux - это вовсе не что-то новое либо революционное

Не то, чтобы я не люблю его за это. Скорее, в этом даже нет ничего плохого - если решение хорошее и проверенное временем, разве это плохо? Скорее мне просто удивительно слышать, как подход, реализованный в Flux в целом (и в Redux в частности) некоторые пытаются выдавать за что-то инновационное и революционное. Да и само решение, на мой взгляд, как минимум не лишено недостатков. Но об этом далее, а пока вспомню молодость.

В начале нулевых я разрабатывал ПО и библиотеки компонент на Delphi под Windows (сначала Win9x, потом XP). В операционных системах Windows с самых первых, если не ошибаюсь, версий, для визуальных элементов интерфейса (кнопки, поля ввода) существует понятие окна - да, окно это не только то, что с рамкой, почти любой визуальный элемент управления имел свое собственное окно. Окно в данном случае - это некая структура в памяти, которая имеет ассоциированный с ним идентификатор (window handle) и оконную функцию (см. далее). Если мы хотим выполнить какое-либо действие над элементом, например - изменить текст кнопки, мы должны упаковать это действие в специальную структуру-сообщение (Window message) и отправить ее соответствующему окну. Структура состоит из закодированного типа сообщения (например WM_SETTEXT - для установки текста) и собственно payload. Будучи отправленным, сообщение не попадает в обработчик напрямую - вместо этого оно отправится в очередь, из которой его извлекает некий диспетчер и вызывает оконную функцию того окна, в которое мы сообщение отправили, передав его в виде параметра. Оконная функция в простейшем случае - это большой switch, где в зависимости от типа сообщения мы передаем управление более конкретному обработчику. Ничего не напоминает?

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

Нарушение принципа "Low coupling, high cohesion"

Если вы ищите простую и понятную формулировку, что такое качественный дизайн, то эти четыре слова из подзаголовка коротко и емко его описывают - внутри модуля или компонента его элементы должны быть тесно связанны друг с другом, в то время как связи между отдельными модулями/компонентами должны быть слабыми. Это базовая ценность. Все остальные принципы и подходы в проектировании - следствия из этого принципа. "Low coupling, high cohesion" отвечает на вопрос "чего мы хотим добиться", в то время как, скажем, SOLID-принципы или любой из Design Pattern указывает нам "как мы можем этого добиться".

И вот тут Redux подводит - то, что должно быть цельным внутри компонента, оказывается размазанным по множеству файлов и сущностей - получаем Low cohesion вместо High. Связи, которые должны оставаться внутри, выходят наружу. Если нарушение принципа Low Coupling обычно представляют себе в виде переплетений из лапши, то здесь у меня в голове всплывает другое кулинарное блюдо. Позаимствовав терминологию у Java-разработчиков, если отдельный компонент - это фасолинка (Bean) - цельная, замкнутая вещь в себе, то тут мы получаем что-то вроде рагу, где фасоль полопалась и его содержимое вытекло, образовав густую однородную кашу, обволакивающую всю систему целиком, и не позволяющую на нее смотреть как на композицию отдельных законченных и слабо-зависимых сущностей.

Множество Boilerplate кода

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

Неуместное использование

А еще мне не нравится, что Redux или схожие с ним инструменты пытаются использовать там, где они не нужны - скажем, в Angular (angular-redux, NgRx). Redux предназначен для решения проблемы передачи данных в компоненты путем использования глобального State, и в React.js действительно существует такая проблема, там его использование кажется уместным. Но в Angular такой проблемы нет, Injectable-сервисы прекрасно справляются с этой задачей. Зачем решать несуществующую проблему, порождая при этом новые (о которых было написано выше)?

Ну и если говорить о неуместном использовании, также неуместным кажется использования Redux для управления локальным стейтом, что встречается довольно часто.

В заключение надо бы отметить и что-нибудь хорошее. Допустим, это не так плохо, когда у вас большая команда разработчиков разного уровня, и все пишут понятный всем код примерно в одном стиле ничего не выдумывая. Если иначе такая разношерстная команда не сможет работать эффективно, значит овчинка выделки определенно стоит. Но на самом деле хотелось бы иметь простое и эффективное решение, лишенное вышеназванных недостатков. Context API? Может быть.

Подробнее..
Категории: Javascript , React , Reactjs , Web , Redux , Flux

Разгоняем REACTOR

12.06.2021 18:20:44 | Автор: admin

Кому будет интересно?

Реактор сегодня - это стильно, модно, молодежно. Почему многие из нас практикуют реактивное программирование? Мало кто может ответить однозначно на этот вопрос. Хорошо - если Вы понимаете свой выигрыш, плохо - если реактор навязан организацией как данность. Большинство аргументов "ЗА" - это использование микросервисной архитектуры, которая в свою очередь обязывает микросервисы часто и много коммуницировать между собой. Для коммуникации в большинстве случаев выбирают HTTP взаимодействие. Для HTTP нужен легковесный веб-сервер, а что первое приходит на ум? Tomcat. Тут появляются проблемы с лимитом на максимальное количество сессий, при превышении которого веб-сервер начинает реджектить запросы (хотя лимита этого не так уж и легко достичь). Здесь на подмогу приходит реактор, который подобными лимитами не ограничен, и, например, Netty в качестве веб-сервера, который работает с реактивностью из коробки. Раз есть реактивный веб-сервер, нужен реактивный веб-клиент (Spring WebClient или Reactive Feign), а раз клиент реактивный, то вся эта жуть просачивается в бизнес логику, Mono и Flux становятся Вашими лучшими друзьями (хотя по началу есть только ненависть :))

Среди бизнес задач, очень часто встречаются серьезные процедуры, которые обрабатывают большие массивы данных, и нам приходится применять реактор и для них. Тут начинаются сюрпризы, если реактор не уметь готовить, можно и проблем схлопотать очень много. Превышение лимита файловых дескрипторов на сервере, OutOfMemory из-за неконтролируемой скорости работы неблокирующего кода и многое многое другое, о чем мы сегодня поговорим. Мы с коллегами испытали очень много трудностей из-за проблем с пониманием как держать реактор под контролем, но всё что нас не убивает - делает нас умнее!

Блокирующий и неблокирующий код

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

Лидер здесь - HTTP взаимодействие, вариантов масса, выбирай любой. Я предпочитаю Reactive Feign от Playtika, в комбинации со Spring Boot + WebFlux + Eureka мы получаем очень годную сборку для микросервисной архитектуры.

Давайте по-простому: НЕблокирующий код, это обычно всё, в названии чего есть reactive, а блокирующий - все оставшееся :) Hibernate + PostgreSQL - блокирующий, отправить почту через JavaMail - блокирующий, скинуть сообщение в очередь IBMMQ - блокирующий. Но есть, например, реактивный драйвер для MongoDB - неблокирующий. Отличительной особенностью блокирующего кода, является то, что глубоко внутри произойдет вызов метода, который заставит Ваш поток ждать (Thread.sleep() / Socket.read() и многие подобные), что для реактора - как нож в спину. Что же делать? Большинство бизнес логики завязано на базу данных, без нее никуда. На самом деле достаточно знать и уметь делать 2 вещи:

  • Необходимо понимать где блокирующий код. В этом может помочь проект BlockHound или его аналоги (тут тема для отдельной статьи)

  • Исполнение блокирующего кода необходимо переключать на пулы, готовые его выполнять, например: Schedulers.boundedElastic(). Делается это при помощи операторов publishOn & subscribeOn

Разгоняемся сами

Перед тем, как продолжить, необходимо немного размяться!

Уровень 1

    @Test    fun testLevel1() {        val result = Mono.just("")            .map { "123" }            .block()        assertEquals("123", result)    }

Начнем с простого, такой код обычно пишут начинающие reactor программисты. Как начать цепочку? Mono.just и ты на коне :) Оператор map трансформирует пустую строку в "123" и оператор block делает subscribe.

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

Уровень 2

    fun nonBlockingMethod1sec(data: String)     = data.toMono().delayElement(Duration.ofMillis(1000))    @Test    fun testLevel2() {        val result = nonBlockingMethod1sec("Hello world")            .flatMap { nonBlockingMethod1sec(it) }            .block()        assertEquals("Hello world", result)    }

Усложняем наш код, добавляем неблокирующий метод nonBlockingMethod1sec, все что он делает - ожидает одну секунду. Все что делает данный код - дважды, по очереди, запускает неблокирующий метод.

Уровень 3

    fun collectTasks() = (0..99)    @Test    fun testLevel3() {        val result = nonBlockingMethod1sec("Hello world")            .flatMap { businessContext ->                collectTasks()                    .toFlux()                    .map {                        businessContext + it                    }                    .collectList()            }            .block()!!        assertEquals(collectTasks().toList().size, result.size)    }

Начинаем добавлять самое интересное - Flux! У нас появляется метод collectTasks, который собирает массив из сотни чисел, и далее мы делаем из него Flux - это будет наш список задач. К каждой задаче мы применяем трансформацию через оператор map. Оператор collectList собирает все результаты в итоговый список для дальнейшего использования.

Здесь наш код начинает превращаться в рабочий паттерн, который можно использовать для массового выполнения задач. Сначала мы собираем некий "бизнес контекст", который мы используем в дальнейшем для выполнения задач.

Уровень 4

    fun collectTasks() = (0..100)        @Test    fun testLevel4() {        val result = nonBlockingMethod1sec("Hello world")            .flatMap { businessContext ->                collectTasks().toFlux()                    .flatMap {                        Mono.deferContextual { reactiveContext ->                            val hash = businessContext + it + reactiveContext["requestId"]                            hash.toMono()                        }                    }.collectList()            }            .contextWrite { it.put("requestId", UUID.randomUUID().toString()) }            .block()!!        assertEquals(collectTasks().toList().size, result.size)    }

Добавляем немного плюшек. Появилась запись данных (15) в реактивный контекст, а также чтение (10) из него. Мы почти у цели. Постепенно переходим к итоговому варианту

Уровень 5

    fun collectTasks() = (0..1000)        fun doSomethingNonBlocking(data: String)        = data.toMono().delayElement(Duration.ofMillis(1000))        fun doSomethingBlocking(data: String): String {        Thread.sleep(1000); return data    }    val pool = Schedulers.newBoundedElastic(10, Int.MAX_VALUE, "test-pool")    private val logger = getLogger()    @Test    fun testLevel5() {        val counter = AtomicInteger(0)        val result = nonBlockingMethod1sec("Hello world")            .flatMap { _ ->                collectTasks().toFlux()                    .parallel()                    .runOn(pool)                    .flatMap {                        Mono.deferContextual { _ ->                            doSomethingNonBlocking(it.toString())                                .doOnRequest { logger.info("Added task in pool ${counter.incrementAndGet()}") }                                .doOnNext { logger.info("Non blocking code finished ${counter.get()}") }                                .map { doSomethingBlocking(it) }                                .doOnNext { logger.info("Removed task from pool ${counter.decrementAndGet()}") }                        }                    }.sequential()                    .collectList()            }            .block()!!        assertEquals(collectTasks().toList().size, result.size)    }

Вот мы и добрались до итогового варианта! Часть с реактивным контекстом была опущена для более наглядной демонстрации того, зачем мы здесь собрались. У нас появились два новых метода: doSomethingNonBlocking (3) & doSomethingBlocking (6) - один с неблокирующим ожиданием в секунду, второй с блокирующим. Мы создали пул потоков для обработки задач (10), добавили счетчик активных задач в реакторе (15). У нас появился оператор parallel (19) и обратный ему sequential (29). Задачи мы назначили на свежесозданный пул (20). Для понимания, что же происходит внутри, добавили логирование внутри операторов doOnRequest (вызывается перед исполнением метода), doOnNext (вызывается после исполнения метода). Основная задумка - на примере, определить сколько задач одновременно выполняется в реакторе и за какое время цепочка завершит свою работу.

Такой "паттерн", мы с коллегами очень часто применяем для выполнения сложных задач, таких как отправка отчетов или массовая обработка транзакций. Первым делом собирается бизнес контекст - это некая структура, содержащая в себе информацию, полученную в результате вызовов других микросервисов. Бизнес контекст необходим нам для выполнения самих задач, и собирается он заранее, чтобы не тратить время в процессе обработки. Далее мы собираем список задач, превращаем их во Flux и скармливаем реактору на параллельную обработку.

И вот здесь начинается самое интересное. Попробуйте ответить на несколько вопросов. Как Вы считаете, сколько времени будет выполнятся данная цепочка? В ней 100 задач, в каждой задаче неблокирующее ожидание в 1 секунду, блокирующее ожидание в 1 секунду, и у нас в наличии пул из 10 потоков? (Вполне годная задачка на собеседование senior reactor developer :))

Правильный ответ

Около 12 секунд. Рассуждаем от блокирующего :) Блокирующее ожидание никуда не деть, и тут имеем 100 блокирующих секунд на 10 потоков, итого 10 секунд. Неблокирующее ожидание заметно нам лишь в первый раз, далее оно незаметно запускается в передышках между блокирующим. Не забываем про одну секунду сбора "бизнес контекста" перед запуском задач.

А теперь уберем строку (26) .map { doSomethingBlocking(it) } . Освободим наш реактор от блокирующего кода, интересно, сколько теперь времени займет выполнение цепочки?

Правильный ответ

2 секунды! 1 на сбор "бизнес контекста" и 1 на выполнение всех задач. Реактор запустит 100 задач одновременно. Но ведь у нас пул из 10 потоков? Как так? Первый разрыв шаблона.

Мы идем до конца и увеличиваем количество задач в методе collectTasks() до ... 1000? а может быть сразу до 15000? Как долго реактор будет выполнять столько задач?

Правильный ответ

2 секунды! 1 на сбор "бизнес контекста" и 1 на выполнение всех задач. Реактор запустит ВСЕ задачи одновременно. Второй разрыв шаблона. Где предел?

А это вообще легально?

Как же так и как это контролировать? Почему это опасно? Что если внутри параллельной обработки Вы решите вызвать другой микросервис? Если у вас 30000 задач, и по завершению каждой, Вам нужно отправлять запрос соседнему микросервису, Вы с удивлением можете обнаружить, что реактор непременно постарается выполнить все вызовы одновременно (Вы ведь используете реактивный web-client или реактивный feign, верно?) Открытие такого большого количества сокетов повлечет за собой превышение лимита открытых файловых дескрипторов в системе, что как минимум создаст проблемы с невозможностью создания новых сокетов в системе и помешает другим сервисам, а как максимум повалит Вам на сервере SSH и Вы потеряете доступ к серверу. Сомневаюсь, что в этот момент, программист будет кричать "зато смотри как быстро работает".

Разрыв шаблона. Thread Pool & Reactor

Основная проблема начинающего реактор программиста - это образ мышления, если есть медленный процесс - добавь X потоков, будет быстрее в X раз, а если слишком быстро - сократи количество потоков. Как всё просто было раньше? :) С реактором это не работает.

Классический thread pool - двери. Больше дверей - больше пропускная способность, все работает быстрее.

Теперь встречайте reactor! Вы видите двери? Нет никаких дверей

Реактор это большой мешок с подарками, или воздушная труба, задачи в которую валятся и летают там пока не выполнятся. А кто эти люди в желтом? Это наши epoll реактивные потоки, которые ни в коем случае нельзя нагружать блокирующими задачами. Можно провести аналогию с прорабами или инженерами. Они здесь, чтобы управлять процессом, а не чтобы выполнять тяжелую работу. Займите одного инженера тяжелой задачей, и когда к нему придет следующий рабочий с вопросом "что делать дальше?", он не сможет ответить, потому что был занят. Вот так и появляются таймауты в реактивном коде. Казалось бы микросервис стоит без нагрузки, выполняет какие-то задачки, а один из 500 запросов к нему падает с тайм-аутом, и непонятно почему. Велика вероятность что инженер был занят блокирующей задачей! Заботьтесь о своих инженерах и поручайте тяжелую работу специально обученным рабочим, например, Schedulers.boundedElastic().

Как контролировать эту "трубу", в которую валится всё без контроля? Вот мы и подошли к кульминации

Конфигурируем реактор!

В своей дефолтной конфигурации, параллельная обработка в реакторе зависит от количества ядер процессора сервера, на котором запускается код, поэтому, к своему удивлению, Вы получите разные результаты, проверяя работу реактора в тесте на локальной машине с 4-8 ядрами и production сервере с 32 ядрами.

Парад настроек открывает parallel с его аргументом parallelism

Меняя parallelism, мы можем регулировать количество запускаемых rails (это местное понятие реактора, которое похоже на корутины, но по сути является количеством одновременно выполняемых неблокирующих задач). Prefetch мы рассмотрим более подробно в следующем разделе.

Но одного parallelism недостаточно, реактор все еще будет нагребать задач как не в себя.

Мало кто обращал внимание что у оператора flatMap (только того что запускается на Flux) есть перегрузки с интересными аргументами, а именно maxConcurrency

maxConcurrency очень важен, по дефолту значение стоит Integer.MAX_VALUE (определяет сколько неблокирующих задач может выполняться одновременно на одной рельсе. Понимаете теперь откуда аппетит у реактора?

Также, не стоит забывать, что если цепочка будет запущена несколько раз (вызов одного http метода контроллера несколько раз), то все помножится! Никакой пул не спасет.

Количество запусков цепочки напрямую влияет на количество одновременно выполняемых задач.

Подведем небольшой итог:

  • parallel (parallelism)

  • flatMap (maxConcurrency)

  • Количество запусков цепочки

Эти три параметра являются множителями, для расчета количества одновременных задач.

По дефолту это Кол-во ядер * Integer.MAX_VALUE * Количество запусков цепочки

Напротив же, запустив данный код для 5 задач длительностью в секунду мы получим цепочку работающую 5 секунд. Теперь всё под контролем!

        val result = nonBlockingMethod1sec("Hello world")            .flatMap { _ ->                collectTasks().toFlux()                    .parallel(1)                    .runOn(pool, 1)                    .flatMap({                        Mono.deferContextual { _ ->                            doSomethingNonBlocking(it.toString())                        }                    }, false, 1, 1)                    .sequential()                    .collectList()            }            .block()!!

Стоп, или не всё?

Thread Pool

Зачем же нужен пул потоков в реакторе? Думайте о нем как о двигателе для Вашего автомобиля. Чем пул мощнее - тем блокирующие задачи будут разбираться быстрее, а если потоков мало, то и блокирующие задачи задержатся у вас надолго! А куда же мы без блокирующих вызовов? На количество одновременно выполняемых задач в реакторе он не влияет, вот это поворот :)

Надеюсь, Вы не пробовали использовать Schedulers.parallel() для исполнения Вашего блокирующего кода? =) Несмотря на свое подходящее название (ну называется он parallel, значит и нужен для параллельной обработки) использовать этот пул можно только для неблокирующего кода, в доке указано что он живет с одним воркером, и содержит в себе только особенные, реактивные потоки.

Распределение задач по рельсам

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

Зеленые прямоугольники это наши задачи, которые распределяются в реакторе по алгоритму round-robin, что в случае с синтетическими данными дает красивую картинку.

Хорошо загруженный реактор (задачи равномерно распределены). 54 блокирующих задачи (каждая по 1сек), round-robin распределение по 6 рельсамХорошо загруженный реактор (задачи равномерно распределены). 54 блокирующих задачи (каждая по 1сек), round-robin распределение по 6 рельсам

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

Плохо загруженный пул (задачи распределены не равномерно)54 блокирующих задачи (каждая по 1сек кроме 2ух), round-robin распределение по 6 рельсамПлохо загруженный пул (задачи распределены не равномерно)54 блокирующих задачи (каждая по 1сек кроме 2ух), round-robin распределение по 6 рельсам

Оператор collectList() вернет нам результат только после завершения последней задачи, и как мы видим, наш пул будет простаивать пока 1 поток трудится разгребая очередь накопившихся задач. Это создает неприятные задержки, когда Вы знаете что можно быстрее, но быстрее не происходит.

Бороться с этим можно несколькими способами

  • concatMap вместо flatMap (посмотрите в профилировщик на ваш пул, передумаете)

  • правильно планировать задачи, чтобы исключить аномалии (почти невозможно)

  • дробить каждую задачу на много мелких, и также запускать их в параллельную обработку чтобы нивелировать проблемы с распределением (вполне рабочий вариант)

  • prefetch (наш выбор!)

Параметр prefetch у flatMap & runOn позволяет определить, сколько задач будет взято на одну рельсу на старте, а затем при достижении некоторого порога выполнения задач, реквесты будут повторяться с этим количеством. Значение по умолчанию - 256. Сменив значение на 1, можно заставить реактор использовать механизм "work stealing", при котором, рельсы и потоки, которые освободились, будут забирать задачи себе на выполнение и картина получится гораздо более приятная.

Хорошо загруженный пул (задачи равномерно распределены)54 блокирующих задачи (каждая по 1сек кроме 2ух), round-robin распределение по 6 рельсамPrefetch !Хорошо загруженный пул (задачи равномерно распределены)54 блокирующих задачи (каждая по 1сек кроме 2ух), round-robin распределение по 6 рельсамPrefetch !

На этом у меня всё. Будет интересно прочесть Ваши замечания и комментарии, на 100% истину не претендую, но все результаты подкреплены практическими примерами, на Spring Boot + Project Reactor 3.4. Всем спасибо!

Подробнее..
Категории: Kotlin , Java , Concurrency , Parallel , Reactor , Parallelism , Mono , Threads , Flux , Pool , Prefetch

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru