Принципы создания крупного масштабируемого проекта
Для начала определим нормы разработки крупного приложения:
- Сокращение зависимостей. Любое изменение должно затрагивать как можно меньшее количество кода.
- Возможность повторного использования. Отдельные части кода должны быть пригодны для других проектов.
- Масштабируемость. Добавление новых функций в код не должно вызывать затруднений.
- Стабильность. У разработчика должна быть возможность отключить некоторые блоки кода с помощью переключателей функций. Это пригодится, когда нужно заблокировать устаревшие функции в старой версии приложения или избежать ошибок и сбоев в новой, особенно если в проекте применяется магистральная разработка (trunk-based development — TBD).
- Отзывчивость. Проект следует разделить на модули. Это позволит назначать ответственного за проверку для каждого из них, что значительно упростит ревью кода. Делается это через Github с помощью файла CODEOWNERS. Этот пункт применим не только к крупным частям приложения, таким как модули Gradle и Pod, но и к обычным функциям, за проверку которых отвечают разные специалисты.
Компонент
На картинке представлен стандартный экран профиля. Как правило, для него используются архитектуры уровня представления (те, что начинаются с MV) и создаются классы Presenter/ViewModel/Interactor/Something.
Эта техника подходит для небольших команд, однако по мере их роста в ней возникает путаница. К примеру, при написании кода изменения в одних и тех же классах влияют друг на друга. У целой команды могут возникнуть проблемы с объединением веток. В результате не связанных напрямую изменений начинаются поломки в бизнес-логике. Разработчики будут вынуждены постоянно переписывать и обновлять юнит-тесты.
Другая проблема состоит в том, что по мере усложнения экрана все больше событий генерируется как со стороны пользователя (касания, поиск, пролистывания), так и системы (загрузка, обновления, уведомления). На определенном этапе экран просто переполняется, а понять, что именно на нем происходит и как он выглядит, становится трудно. Учитывая все возможные события на экране, будет непросто отобразить корректное состояние интерфейса.
Решить эту проблему поможет новое правило: каждый экран нужно разделить на несколько небольших компонентов. Все они должны содержать минимальное количество кода и быть максимально изолированы друг от друга.
Требования к компоненту
- Единая ответственность: должен представлять только один бизнес-объект.
- Простая реализация: должен содержать минимальное количество кода.
- Независимость: он не должен знать ничего о других компонентах в приложении и на экране.
- Анонимная связь: общение между компонентами должно происходить через специальный объект, который наблюдает за входящими событиями и не знает, какой именно компонент отправляет каждое из них.
- Определенное состояние UI: упрощает восстановление состояния экрана и предоставляет информацию о том, что видит пользователь в любой момент времени.
- Однонаправленный поток данных: состояние компонента должно быть однозначно определенным и неизменяемым. Это также означает, что приложение будет применять односторонне связанный поток данных и обладать единой точкой истины, которая способна изменять состояние.
- Удаленное управление: компоненты можно настроить через сервер. По крайней мере, стоит добавить возможность их отключения в любой момент времени с помощью переключателей функций. Это пригодится, если вы, например, решите больше не отображать стоимость продукта на экране.
Схема работы компонентов
- Компонент получает данные (DomainObject или “внешнее состояние”) из внешнего источника (назовем его Service).
- Приложение применяет бизнес-логику компонента к входным данным, а затем создается новое состояние UI.
- Приложение отображает новое состояние пользователю.
- Если пользователь взаимодействует с компонентом (нажимает на кнопку, пролистывает страницу и т.д.), создается новое действие (Action). Оно перенаправляется к объекту, отвечающему за бизнес-логику компонента.
- Бизнес-логика решает, стоит ли создавать новое состояние UI или передать Action в Service.
- Другие компоненты могут просматривать данные из 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).
- Однонаправленный поток данных и единая точка истины повышают стабильность приложения и позволяют сэкономить время при отладке и исправлении ошибок.
- Назначение отвечающего за проверку отдельного компонента или функции улучшает устойчивость и качество кода.
Читайте также:
- Операционная система Android
- Знакомьтесь, компонент Navigation в Android!
- Как проще всего выполнять запросы GraphQL в iOS
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Alexey Glukharev: Scalable Architecture For Big Mobile Projects