Как создать масштабируемую архитектуру для крупных мобильных проектов

Принципы создания крупного масштабируемого проекта

Для начала определим нормы разработки крупного приложения:

  1. Сокращение зависимостей. Любое изменение должно затрагивать как можно меньшее количество кода.
  2. Возможность повторного использования. Отдельные части кода должны быть пригодны для других проектов.
  3. Масштабируемость. Добавление новых функций в код не должно вызывать затруднений.
  4. Стабильность. У разработчика должна быть возможность отключить некоторые блоки кода с помощью переключателей функций. Это пригодится, когда нужно заблокировать устаревшие функции в старой версии приложения или избежать ошибок и сбоев в новой, особенно если в проекте применяется магистральная разработка (trunk-based development — TBD).
  5. Отзывчивость. Проект следует разделить на модули. Это позволит назначать ответственного за проверку для каждого из них, что значительно упростит ревью кода. Делается это через Github с помощью файла CODEOWNERS. Этот пункт применим не только к крупным частям приложения, таким как модули Gradle и Pod, но и к обычным функциям, за проверку которых отвечают разные специалисты.

Компонент

На картинке представлен стандартный экран профиля. Как правило, для него используются архитектуры уровня представления (те, что начинаются с MV) и создаются классы Presenter/ViewModel/Interactor/Something.

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

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

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

Требования к компоненту

  1. Единая ответственность: должен представлять только один бизнес-объект.
  2. Простая реализация: должен содержать минимальное количество кода.
  3. Независимость: он не должен знать ничего о других компонентах в приложении и на экране.
  4. Анонимная связь: общение между компонентами должно происходить через специальный объект, который наблюдает за входящими событиями и не знает, какой именно компонент отправляет каждое из них.
  5. Определенное состояние UI: упрощает восстановление состояния экрана и предоставляет информацию о том, что видит пользователь в любой момент времени.
  6. Однонаправленный поток данных: состояние компонента должно быть однозначно определенным и неизменяемым. Это также означает, что приложение будет применять односторонне связанный поток данных и обладать единой точкой истины, которая способна изменять состояние.
  7. Удаленное управление: компоненты можно настроить через сервер. По крайней мере, стоит добавить возможность их отключения в любой момент времени с помощью переключателей функций. Это пригодится, если вы, например, решите больше не отображать стоимость продукта на экране.

Схема работы компонентов

  1. Компонент получает данные (DomainObject или “внешнее состояние”) из внешнего источника (назовем его Service).
  2. Приложение применяет бизнес-логику компонента к входным данным, а затем создается новое состояние UI.
  3. Приложение отображает новое состояние пользователю.
  4. Если пользователь взаимодействует с компонентом (нажимает на кнопку, пролистывает страницу и т.д.), создается новое действие (Action). Оно перенаправляется к объекту, отвечающему за бизнес-логику компонента.
  5. Бизнес-логика решает, стоит ли создавать новое состояние UI или передать Action в Service.
  6. Другие компоненты могут просматривать данные из Service и обновлять их (пункт #1).

Архитектура экрана

Как было сказано ранее, компоненты на одном экране не знают ничего друг о друге, но могут отправлять Action в общие объекты  —  Service, которые позволяют им отслеживать изменения.

Service бывает двух типов:

  • Глобальные охватывают целое приложение (например, UserService, PaymentsService, CartService).
  • Локальные используются для каждого отдельного экрана (например, ProductDetailsService, OrderListService).

Стоит упомянуть, архитектура слоя представления в проекте может быть любой (MVP/MVC/MVVM/MVI и прочие). Тем не менее каждый компонент должен учитывать указанные выше требования.

Машина состояний

Архитектура такого экрана представляет собой машину состояний, которая получает Action (событие) из UI и внешнего Service. На его основе она создает Data State (состояние данных). Затем компоненты обрабатывают его и отображают результаты в UI.

Определим новые объекты:

  • Функции промежуточной обработки (middleware) получают бизнес-логику. Они обрабатывают входящие Action и создают новое состояние. Также они могут создать новое Action, чтобы связаться с другими функциями промежуточной обработки или внешними объектами.
  • Редьюсер (reducer) получает текущее состояние и объединяет его с новым, полученным из middleware. Затем редьюсер отправляет объединенное состояние всем подписчикам.

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

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

Управляемый сервером интерфейс

Наличие автономных компонентов, в которые можно передавать входные данные (DomainObject) для отображения в UI, позволяет получать их список из сервера и динамично настраивать структуру экрана. Основным преимуществом этого подхода является возможность динамически изменять содержимое экрана без необходимости загружать новую версию приложения в Play Store или App Store. Это значительно упрощает работу маркетинговой команды.

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

Ниже показан пример ответа.

{
  "components": [
    {
      "type": "toolbar",
      "version": 3,
      "position": "header",
      "data": {
        "title": "Profile",
        "showUpArrow": true
      }
    },
    {
      "type": "user_info",
      "version": 1,
      "position": "header",
      "data": {
        "id": 1234,
        "first_name": "Alexey",
        "last_name": "Glukharev"
      }
    },
    {
      "type": "user_photo",
      "position": "header",
      "version": 2,
      "data": {
        "user_photo": "https://image_url.png"
      }
    },
    {
      "type": "menu_item",
      "version": 1,
      "position": "content",
      "data": {
        "text": "open user details",
        "deeplink": "app://user/detail/1234"
      }
    },
    {
      "type": "menu_item",
      "version": 1,
      "position": "content",
      "data": {
        "text": "contact us",
        "deeplink": "app://contact_us"
      }
    },
    {
      "type": "button",
      "version": 1,
      "position": "bottom",
      "data": {
        "text": "log out",
        "action": "log_out"
      }
    }
  ]
}

Заключение

  • Разбейте экран на несколько небольших компонентов, каждый из которых будет отвечать за один бизнес-объект.
  • Следуйте указанным в статье правилам, чтобы повысить надежность этих компонентов.
  • Чтобы обеспечить связь между компонентами, задействуйте родительские объекты (в данном случае Service).
  • Однонаправленный поток данных и единая точка истины повышают стабильность приложения и позволяют сэкономить время при отладке и исправлении ошибок.
  • Назначение отвечающего за проверку отдельного компонента или функции улучшает устойчивость и качество кода.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Alexey Glukharev: Scalable Architecture For Big Mobile Projects

Предыдущая статьяЛучшие JavaScript-фреймворки и тенденции веб-разработки в 2021 году
Следующая статья9 Уровней применения функции zip в Python