Прохладные примеры: файловый браузер
Очень часто можно встретить статьи с лозунгами “ваш подход - отстой, вот как правильно!”, где в качестве подхода можно подставить библиотеку, фреймворк или архитектуру как таковую. Для сравнение берется пример какого-то минимального приложения, и как правило простого: счетчики с инкрементом-декрементом, простые запросы на сервер и тому подобное. И иногда даже удается авторам на таких примерах убедить, что вот да, я ошибался, вот он истинный “подход”, заверните два. Но как правило пробуя это в промышленном проекте все становится не так радужно, а аналог примера если и встречается, то в качестве первых шагом и следом, сильно усложняется и вся выгода уже не видна.
Я хочу привести пример задачи, которая будет содержать механики, или фичи, которые мне попадаются весьма часто.
Данные - Рекурсивная структура. Узлы и листья, каталоги и файлы.
Поведение - Новый запрос на результат изменения состояния приложения после первого запроса. Аналог - Jira + управление состоянием задач. Создали ветку - задача переехала в “In Progress”.
На примере этих двух, слабоформализованных задачах хочется и выстроить небольшое приложение, которое может быть полигоном для сравнения разных подходов.
Формализация задачи
Пользователям нужен файловый браузер в интернет-браузере. В данном браузере они могут редактировать выбираемый каталог исходников. Редактирование представляет собой:
- переименовывание корневого каталога
- Создание подкаталогов
- Создание файлов
- Переименовывание файлов и подкаталогов
- Редактирование содержимого файлов
В процессе редактирования пользователь может выполнить undo/redo действий которые изменяют корневой каталог (включая изменения подкаталогов, файлов).
При редактировании каталога, они проходят некоторую проверку, которая может пометить файлы в нем как:
- пустой
- сложный
- неиспользуемый
- идеальный.
С каждым вариантом пользователь решает что делать:
- удалить
- переместить в другой каталог по выбору.
Эти действия пользователь может выполнять интерактивно, файл за файлом, или автоматически, указав необходимое действие в конфигурации.
Кейс использования интерактивной операции
- Пользователь создает каталог исходников некоторого проекта.
- Пользователь отправляет каталог на проверку
- Пользователь получает ответ, где 5 файлов пустых, 3 сложных 1 неиспользуемый и 20 - идеальных. Пользователь видит форму, в которой ему предлается выбрать одно из возможных действий (удалить, переместить, оставить, отменить все) и “галочку” повторять для подобных.
- Интерактивно, пользователь выполняет дейсвия над каждым файлом. Если ему попался какой-то специфичный кейс - он отменяет все действия и никаких изменений не совершается.
Кейс использования автоматизации
- Пользователь создает каталог исходников некоторого проекта.
- Пользователь настраивает конфигурацию так, что пустые и неиспользуемые
удаляются, сложные - перемещаются в новый каталог “complex”, неиспользуемые
- остаются как есть.
- Пользователь отправляет каталог на проверку
- Выполняются все соответствующие операции
Backend API
- сам каталог хранится на сервере
- все изменения над каталогом сохраняются через 1 вызов API: PATCH
/catalogs/:id
- функция проверки занимает по 0.1 сек на каждый файл
- для выполнения действия над файлом, нужно дергать API
По поводу UI
Приложение планируется портировать как на мобильные так и десктопные платформы. Так же, есть необходимость реализовать CLI и TUI.
Процесс решения
Перво наперво, из последнего пункта понимаем, что стоит сразу отказываться от
логики внутри виджетов (привет React::setState
). В целом, можно реализовать
приложение на JS, и портировать на разные платформы.
Бэкенд стоит мокнуть, т.к. дано только описание и то, слишком скупо.
Что на этой задаче мы будем проверять:
- Native vs Canvas vs React vs Vue vs Angular vs Svetle - для отображения
- Redux vs MobX vs Vuex vs Baobab - для управления состоянием.
Имплементация с Redux
Самым интересным с Redux будет работа с иерархическими данными. Как должен выглядеть reducer каталогов, если мы хотим изменять названия вложенных в него каталогов, файлов? Есть такая штука - нормализация данных. Да, я про библиотечку normalizr. Я долго не понимал зачем она, пока не пришлось работать с вложенными данными. Представим ситуацию с каталогом:
{
id: 1,
name: "catalog 1",
files: [],
children: [
{
id: 2,
name: "catalog 2",
files: [],
children: [
{
id: 3,
name: "catalog 3",
files: [
{
id: 1,
name: "file 1",
},
],
children: [ ]
}
]
}
]
}
Пользователь в том или ином виде видит эти данные и изменяет название файла
“file 1” - запускается action RENAME_FILE
. Ок, что должно быть в action, и
как мы изменим эти данные?
Сначала, т.к. action говорит об изменении файла, но мы не знаем какого. Поэтому как минимум id файла должен быть в payload. Достаточного ли этого? Ну, в принципе да. Зная id файла, мы можем пройтись по всем каталогам вернего уровня и устроить им поиск в глубину. Код вроде бы простой, но рекурсивный и цикличный.
Можем ли мы сделать код проще как с точки зрения читателя кода?
Давайте разложим наши каталоги файлы в 2 разных списка: каталоги и файлы. Вся
вложенность - children, files - будут содержать массимы id. Тогда мы просто ищем
нужный файл в files
по id и меняем название. Затем, чтобы все это отрисовать
мы заменяем все вложенные id на непосредственные значения.
Тут палка о двух концах по производительности: В первом варианте мы дольше выполняем изменение, а во втором, мы на подготовке для рендеринга можем потупить немного.
Так же, чтобы не искать по id, мы можем хранить каталоги и файлы не массивами, а словарями, где ключом является id. Он же уникален. И тогда поиск будет простым взятием по ключу.