Представьте, что вы начинаете разработку нового Android-приложения. На этом этапе серьезные проблемы маловероятны. Вы реализовали только базовую функциональность. Есть несколько простых экранов. Навигация по коду проста. Вы с энтузиазмом начинаете добавлять функции одну за другой. Однако со временем разработка становится все сложнее. Кодовая база расширяется, главный экран загромождается многочисленными элементами пользовательского интерфейса и запутанной логикой, а ScreenFlow превращается в сложную цепочку переходов. Теперь сложно добавить что-то новое, не нарушив при этом ничего старого. Как следствие, темпы разработки замедляются. Знакомая ситуация?

Эффективной стратегией преодоления этой сложности является компонентный подход. В MobileUp мы реализовали его в трех крупных Android-приложениях и с трудом представляем, как обходились без него раньше.

Я, руководитель команды в MobileUp, готов помочь вам освоить компонентный подход. Моя цель — сделать этот процесс максимально простым и увлекательным.

Вас ждет серия статей. Первая из них посвящена теории. Мы рассмотрим сложности, возникающие при разработке Android-приложений, и обсудим, почему ни MVVM (Model-View-ViewModel), ни чистая архитектура не являются панацеей. Вы узнаете, что такое компонентный подход и какие у него преимущества. 

Сложность Android-приложений

При разработке Android-приложений обычно сталкиваются с двумя типами сложности.

1. Сложные экраны

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

2. Сложная навигация

По мере роста приложения и добавления новых экранов навигация между ними становится все более сложной. Это приводит к появлению многоступенчатых сценариев, таких как авторизация, регистрация, покупка и опросы, которые взаимосвязаны между собой. В приложение часто интегрируется нижняя навигация — панель с кнопками для переключения между экранами. Для пользователей планшетов часто требуется отображение данных, имеющих отношение «один-ко-многим», когда на экране одновременно отображается список товаров и подробная информация о выбранном товаре. Кроме того, навигация включает использование компонентов Bottom Sheets и диалоговых окон.

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

Проблемы с MVVM и чистой архитектурой

Многие Android-разработчики полагаются на MVVM и чистую архитектуру. Эти методологии, хотя и помогают лучше организовать код, имеют свои недостатки (рассмотрим их ниже).

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

Массивные модели представления

Шаблон MVVM (Model-View-ViewModel) рекомендует выделять логику экрана в отдельный класс, известный как ViewModel. Этот класс содержит поля для всех данных, отображаемых на экране, и методы для обработки всех действий пользователя.

UI-элементы экрана соответствуют полям и методам в модели представления

Допустим, мы разрабатываем главный экран банковского приложения и создаем для него модель представления (MainViewModel). В верхней части экрана отображаем имя и аватар пользователя, для чего в модели представления требуется поле profile и метод onAvatarClick. В правом верхнем углу находится значок колокольчика для уведомлений, поэтому добавляем поле isNotificationBadgeVisible и метод onNotificationIconClick. Видим карусель рекламных баннеров, что заставляет нас включить поле advertisementItems и метод для обработки нажатий на баннеры. Постепенно добавляются поля и методы для каждой функции — ежемесячные расходы, банковские карты, курсы валют, депозиты, ипотека и так далее. Модель представления быстро становится чрезвычайно сложной.

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

Многослойная архитектура

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

Предположим, мы разделили приложение на три слоя. Поработав с кодом, обнаружили, что он по-прежнему сложен. Каким же будет следующий шаг? Создать еще больше слоев! Именно так мы и поступаем.

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

Разработчик добавил слишком много слоев

Интеракторы не являются сценариями использования

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

Сценарий использования (use case) — термин из инженерии требований. Сценарии использования описывают, что пользователь может делать в приложении. Например, в приложении телефонной книги сценарии использования могут включать просмотр списка контактов, добавление, редактирование, удаление и вызов контакта.

Интеракторы пришли к нам из мира программирования. Программисты пишут интеракторы для реализации сценариев использования. Каждому сценарию использования обычно соответствует отдельный интерактор.

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

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

  1. Пользователь просматривает список контактов.
  1. Пользователь долго нажимает на один из контактов. Контакт становится выбранным. Появляется кнопка «Удалить».
  1. Пользователь нажимает еще на несколько контактов. Они также становятся выделенными.
  1. Пользователь нажимает кнопку «Удалить». Появляется диалоговое окно подтверждения.
  1. Пользователь подтверждает удаление. Выбранные контакты исчезают.

Вот такой получился интерактор:

class RemoveContactsInteractor(
   private val contactsRepository: ContactsRepository
) {

   suspend fun execute(contactIds: Set<ContactId>) {
       contactsRepository.removeContacts(contactIds)
   }
}

Видите, он почти пуст. Здесь нет логики для выбора нескольких контактов или подтверждения их удаления. Такие обязанности обычно возлагаются на модель представления, а не на интерактор. И поскольку здесь нет бизнес-правил, интерактор не делает ничего полезного, а просто передает вызов репозиторию.

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

Сценарий использования в корпоративной разработке (слева) и сценарий использования в мобильной разработке (справа)

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

Что такое компонентный подход?

Компонентный подход в реальном мире

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

Человеческое тело структурировано в соответствии с компонентным подходом

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

Простые элементы объединяются, образуя более сложные структуры, и этот процесс повторяется на нескольких уровнях. Мы называем это компонентным подходом.

Легко найти примеры, где этот принцип также находит применение:

  • вся Вселенная: планеты и звезды ➜ планетарные системы ➜ галактики ➜ скопления галактик;
  • персональный компьютер: от мельчайших транзисторов в процессоре до крупных компонентов, таких как системный блок и монитор;
  • замок Хогвартс, собранный из Lego;
  • книжная библиотека;
  • крупная IT-компания;
  • дом;
  • космический корабль.

Любые сложные объекты и системы структурируются в соответствии с компонентным подходом.

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

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

При применении компонентного подхода важно выбрать соответствующий уровень детализации

Компонентный подход в Android-разработке

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

Компонентный подход в мобильной разработке

Типы компонентов, входящие в состав мобильных приложений:

  • UI-элементы: самые простые компоненты, включающие стандартные предложения UI-фреймворка, такие как кнопки, текстовые поля и флажки, а также пользовательские UI-элементы, разработанные программистами. Как правило, UI-элементы абстрактны и не решают самостоятельно задачи пользователя.
  • Функциональные блоки: более автономные компоненты, каждый из которых работает с определенным функционалом, эффективно выполняя задачи с точки зрения конечного пользователя.
  • Экраны: как следует из названия, эти компоненты отвечают за целые экраны приложений. Сложный экран, выполняющий множество функций, обычно состоит из нескольких функциональных блоков.
  • Потоки: представляют собой последовательности экранов, предназначенных для выполнения общей функциональности. Примерами потоков являются процессы авторизации, регистрации, покупки и опроса.
  • Приложение: также рассматривается как компонент, контролирующий весь спектр функций, доступных пользователю, и состоящий из нескольких потоков.

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

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

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

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

Свежий взгляд на MVVM и чистую архитектуру

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

MVVM 2.0

Шаблон MVVM идеально подходит для реализации экранов и функциональных блоков. Сложный экран может быть структурирован с использованием нескольких моделей представления: одной родительской и нескольких дочерних.

Сложный экран с несколькими моделями представления

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

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

Устранение искусственной сложности

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

Однако существует «искусственная сложность», которую разработчики создают, используя неподходящие инструменты или методы. Это явление также известно как чрезмерная инженерия (или переинженеринг).

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

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

Означает ли это, что мы должны использовать исключительно компонентный подход и отказаться от чистой архитектуры? Конечно, нет! У каждого инструмента есть свое конкретное назначение.

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

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

Эволюция архитектурных подходов: отсутствие архитектуры — чистая архитектура — компонентный подход + чистая архитектура

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

Читайте нас в Telegram, VK и Дзен


Перевод статьи Artur Artikov: Component-based Approach. Fighting Complexity in Android Applications

Предыдущая статьяКонкурентность на Go: горутины и каналы для масштабируемых приложений
Следующая статьяApache Spark  —  типичные ошибки и их устранение