Каким образом ваш интерфейс может получить выгоду от архитектуры программного обеспечения?

Этот пост является частью серии Scalable Frontend, другие части вы можете увидеть здесь: №2 - Общие шаблоны и №3 - Уровень состояния.

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

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

Что такое программная архитектура?

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

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

Есть еще кое-что важное, о чем стоит поговорить об архитектуре: это не означает организацию файлов и не то, как вы называете файлы и папки.

Слои во фронтенд-разработке

Один из способов отделить важное от второстепенного - использовать слои, каждый из которых имеет свой особый набор обязанностей. Распространенный подход в многоуровневой архитектуре - разделить ее на четыре уровня: приложение, домен, инфраструктура и ввод. Эти четыре уровня лучше объяснены в другом посте NodeJS и передовые методы. Мы рекомендуем вам прочитать о них первую часть поста, прежде чем продолжить. Вторую часть читать не обязательно, поскольку она предназначена только для NodeJS.

Уровни домена и приложения не сильно различаются между интерфейсом и серверной частью, поскольку они не зависят от технологий, но мы не можем сказать то же самое о входных уровнях и уровнях инфраструктуры. В веб-браузере на входном слое, представлении, обычно присутствует один субъект, поэтому мы даже можем назвать его слоем представления. Кроме того, у внешнего интерфейса нет доступа к базе данных или механизму очередей, поэтому мы не найдем их на уровне инфраструктуры внешнего интерфейса. Вместо этого мы обнаружим абстракции, которые инкапсулируют запросы AJAX, файлы cookie браузера, LocalStorage или даже единицы, которые взаимодействуют с серверами WebSocket. Основное различие заключается только в том, что является абстракцией, поэтому вы можете даже иметь внешние и внутренние репозитории с точно таким же интерфейсом, но с другой технологией. Вы видите, насколько прекрасной может быть хорошая абстракция?

Неважно, используете ли вы React, Vue, Angular или любой другой инструмент для создания своего представления. Важно следовать правилу входного уровня - не иметь никакой логики, поэтому входные параметры делегируются следующему уровню. Что касается архитектуры на основе внешнего интерфейса, есть еще одно важное правило: чтобы уровень ввода / просмотра всегда синхронизировался с локальным состоянием, вы должны следовать одностороннему потоку данных. Вам знаком этот термин? Мы можем сделать это, добавив пятый слой специально для этого: состояние, также известное как магазин.

Слой состояния

Следуя одностороннему потоку данных, мы никогда не изменяем и не мутируем данные, полученные представлением непосредственно внутри представления. Вместо этого мы отправляем то, что мы называем действиями из представлений. Это происходит так: действие отправляет сообщение источнику данных, источник обновляется, а затем повторно отображает представление с новыми данными. Обратите внимание, что прямого канала из представления в магазин никогда не бывает, поэтому, если два подчиненных представления используют одни и те же данные, вы можете отправить действие из любого из них, и это приведет к повторной визуализации обоих с новыми данными. Может показаться, что я говорю конкретно о React и Redux, но это не так; вы можете достичь тех же результатов практически с любой современной интерфейсной структурой или библиотекой, такой как React + context API, Vue + Vuex, Angular + NGXS или даже Ember, используя подход вниз по данным (он же DDAU ). Вы даже можете сделать это с помощью jQuery, используя его систему событий для отправки действий!

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

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

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

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

Внедрение зависимости

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

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

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

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

Если вы используете redux-thunk, этого очень просто добиться с помощью метода withExtraArgument, который позволяет вам вставлять контейнер в каждое действие преобразователя в качестве третьего параметра после getState. Подход должен быть таким же простым, если вы используете redux-saga, где мы передаем контейнер в качестве второго параметра метода run. Если вы используете Ember или Angular, встроенного механизма внедрения зависимостей должно хватить.

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

Отлично, у нас есть слой состояния, внедренный в уровень представления, и уровень приложения, введенный в уровень состояния. А что насчет остальных? Как мы внедряем зависимости в варианты использования для создания контейнера зависимостей? Это важный вопрос, и есть много способов сделать это. Прежде всего, не забудьте проверить, есть ли в используемом вами фреймворке встроенное внедрение зависимостей, например Angular или Ember. Если это так, вам не следует создавать свою собственную. В противном случае вы можете сделать это двумя способами: вручную или с небольшой помощью пакета.

Сделать это вручную должно быть просто:

  • Определите свои единицы как классы или замыкания,
  • Сначала создайте экземпляры тех, у которых нет зависимостей,
  • Создайте экземпляры тех, которые зависят от них, передав их как параметры,
  • Повторяйте, пока не создадите все варианты использования,
  • Экспортируйте их.

Слишком абстрактно? Взгляните на несколько примеров кода:

Вы заметите, что важная часть, варианты использования, создаются в конце файла и являются единственными экспортируемыми объектами, поскольку они будут вставлены в действия. Остальной части вашего кода не нужно знать, как создается репозиторий и как он работает. Это не важно, это техническая деталь. Для варианта использования не имеет значения, отправляет ли репозиторий запрос AJAX или сохраняет что-то в LocalStorage; знание этого - не обязанность прецедента. Если вы хотите использовать LocalStorage, пока ваш API все еще находится в разработке, а затем переключиться на использование вызовов по сети к API, вам не нужно менять вариант использования, пока код, который взаимодействует с API, следует за тот же интерфейс, что и тот, который взаимодействует с LocalStorage.

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

Эмпирическое правило для проверки того, достаточно ли хорош ваш пакет DI, - это проверить, не требуется ли при переходе от ручного подхода к использованию библиотеки касаться большего, чем код контейнера. Если это так, пакет слишком навязчивый, и вам следует выбрать другой. Если вы действительно хотите использовать пакет, мы рекомендуем Awilix. Он довольно прост в использовании, и для перехода от ручного режима потребуется всего лишь прикоснуться к файлу-контейнеру. Есть очень хорошая серия о том, как и зачем ее использовать, написанная автором пакета.

Следующий

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

Рекомендуемые ссылки

Авторы Талиссон де Оливейра и Яго Далем Лоренсини.