Очень часто можно встретить статьи с лозунгами “ваш подход - отстой, вот как правильно!”, где в качестве подхода можно подставить библиотеку, фреймворк или архитектуру как таковую. Для сравнение берется пример какого-то минимального приложения, и как правило простого: счетчики с инкрементом-декрементом, простые запросы на сервер и тому подобное. И иногда даже удается авторам на таких примерах убедить, что вот да, я ошибался, вот он истинный “подход”, заверните два. Но как правило пробуя это в промышленном проекте все становится не так радужно, а аналог примера если и встречается, то в качестве первых шагом и следом, сильно усложняется и вся выгода уже не видна.

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

Данные - Рекурсивная структура. Узлы и листья, каталоги и файлы.

Поведение - Новый запрос на результат изменения состояния приложения после первого запроса. Аналог - Jira + управление состоянием задач. Создали ветку - задача переехала в “In Progress”.

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

Формализация задачи

Пользователям нужен файловый браузер в интернет-браузере. В данном браузере они могут редактировать выбираемый каталог исходников. Редактирование представляет собой:

  1. переименовывание корневого каталога
  2. Создание подкаталогов
  3. Создание файлов
  4. Переименовывание файлов и подкаталогов
  5. Редактирование содержимого файлов

В процессе редактирования пользователь может выполнить undo/redo действий которые изменяют корневой каталог (включая изменения подкаталогов, файлов).

При редактировании каталога, они проходят некоторую проверку, которая может пометить файлы в нем как:

  • пустой
  • сложный
  • неиспользуемый
  • идеальный.

С каждым вариантом пользователь решает что делать:

  • удалить
  • переместить в другой каталог по выбору.

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

Кейс использования интерактивной операции

  1. Пользователь создает каталог исходников некоторого проекта.
  2. Пользователь отправляет каталог на проверку
  3. Пользователь получает ответ, где 5 файлов пустых, 3 сложных 1 неиспользуемый и 20 - идеальных. Пользователь видит форму, в которой ему предлается выбрать одно из возможных действий (удалить, переместить, оставить, отменить все) и “галочку” повторять для подобных.
  4. Интерактивно, пользователь выполняет дейсвия над каждым файлом. Если ему попался какой-то специфичный кейс - он отменяет все действия и никаких изменений не совершается.

Кейс использования автоматизации

  1. Пользователь создает каталог исходников некоторого проекта.
  2. Пользователь настраивает конфигурацию так, что пустые и неиспользуемые удаляются, сложные - перемещаются в новый каталог “complex”, неиспользуемые
    • остаются как есть.
  3. Пользователь отправляет каталог на проверку
  4. Выполняются все соответствующие операции

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. Он же уникален. И тогда поиск будет простым взятием по ключу.