Представьте, что вы начали разработку нового Android-приложения. Поначалу особых проблем не будет. Вы реализовали лишь самые базовые функции. Экранов немного, и все они простые. Вам легко ориентироваться в коде. Вы бодро добавляете одну фичу за другой. Но со временем разработка усложняется: кода становится много, главный экран обрастает большим количеством UI-элементов и логики, экраны образуют сложные цепочки переходов. Приходится ломать голову, чтобы добавить что-то новое, не сломав ничего из старого. Скорость разработки падает. Знакомая ситуация?
Существует эффективный способ борьбы со сложностью — компонентный подход. Мы в MobileUp применили его в трех крупных Android-приложениях и теперь не представляем, как жили без него раньше.
Меня зовут Артур, я тимлид в компании MobileUp. Я помогу вам освоить компонентный подход. Постараюсь сделать это как можно проще и увлекательнее.
Вас ждет серия статей. Это первая из них — теоретическая. В ней мы рассмотрим, какие сложности встречаются в Android-приложениях, и почему MVVM и Clean Architecture не панацея против них. Я расскажу, что такое компонентный подход и в чем его преимущества. А в конце статьи будут ссылки на материалы для углубленного изучения.
Сложность в Android-приложениях
В Android-приложениях чаще всего встречаются два вида сложности:
Сложные экраны
Типичный пример — главный экран приложения. Вспомните банковские приложения, приложения для онлайн-покупок или соцсетей. Главный экран отображает всю важную для пользователя информацию. На нем много UI-элементов, сетевых запросов, логики.Сложная навигация
По мере развития приложения добавляются новые экраны, а значит, усложняется граф переходов между ними. Появляются сценарии, состоящие из нескольких взаимосвязанных шагов: авторизация, регистрация, покупка, опросники. Приложение обзаводится bottom-навигацией — панелью с кнопками переключения экранов. На планшетах может потребоваться поддержка master-detail навигации, когда экран отображает одновременно и список элементов, и детальную информацию выбранного элемента. Показ боттом-шитов и диалоговых окон также относится к навигации.
Если ничего не предпринимать, скорость и качество разработки постепенно будут падать.
Проблемы с MVVM и Clean Architecture
Большинство Android-разработчиков (и я в том числе) применяет MVVM и Clean Architecture. Эти техники помогают нам лучше структурировать код, но, как я покажу далее, имеют свои изъяны.
Сразу оговорюсь, что у MVVM и Clean Architecture может быть много различных трактовок и реализаций. Опишу, как их понимаю я и с какими трудностями столкнулся на практике. Возможно, ваш опыт отличается — расскажите о нем в комментариях.
Массивные вью-модели
Шаблон MVVM рекомендует выделять логическое представление экрана в отдельный класс — ViewModel. Этот класс содержит поля для всех отображаемых на экране данных и методы для обработки всех действий пользователя.
Допустим, мы делаем главный экран банковского приложения и создадим для него класс вью-модели. В самом верху экрана расположены имя и аватарка пользователя. Значит, во вью-модели появится поле «данные пользователя» и метод-обработчик «что делать при нажатии на аватарку». Справа в верхнем углу находится иконка-колокольчик для уведомлений. Добавляем поле «отображать ли бейджик новых уведомлений на иконке» и метод «что делать по нажатию на иконку уведомлений». Видим ленту рекламных баннеров — добавляем поле «список баннеров» и метод для обработки нажатия на баннер. Продолжая в том же духе, мы добавим во вью-модель поля и методы для всех остальных фич: траты за месяц, банковские карты, курсы валют, вклады, ипотека и т. д. Думаю, вы понимаете, что к этому моменту вью-модель станет чрезвычайно сложной.
Конечно, мы будем всячески стараться упростить код вью-модели, например, вынесем загрузку данных в отдельные классы. Но кардинально это ситуацию не поменяет. Поля и методы никуда не денутся. Всякий раз, когда мы что-то добавляем на экран, вью-модель будет становится все сложнее и сложнее. От этой проблемы не уйти, находясь в рамках классического MVVM, где одному экрану соответствует одна вью-модель.
Многослойная архитектура
Clean Architecture предлагает делить приложение на слои. Каждый слой имеет свою ответственность. Например, один слой отвечает за получение данных из внешних источников, другой занимается бизнес-логикой, третий отображает пользовательский интерфейс и т. д. Точное количество слоев, как и их ответственности, может отличаться от проекта к проекту.
Представьте, что мы разделили приложение на три слоя. Смотрим на код, а он все равно сложный. Какая мысль придет в голову в первую очередь? Сделать больше слоев! И мы это делаем.
Но слои не даются бесплатно. Чем их больше, тем сложнее поддерживать проект. Появляются дополнительные абстракции. Требуется организовать взаимодействие между слоями. Ради единообразия разработчики начинают даже самые простые экраны реализовывать с помощью большого количества слоев. Хотели упростить код, а получили обратный эффект.
Интеракторы — не юзкейсы
Другое важное понятие в Clean Architecture — это интеракторы. Но, прежде чем говорить про них, вспомним, что такое сценарии использования.
Сценарии использования (они же юзкейсы, от англ. use case) — это термин из проектирования требований. С их помощью аналитики описывают, что может сделать пользователь в приложении. Например, для приложения «телефонная книга» получатся такие юзкейсы: посмотреть список контактов, добавить контакт, отредактировать контакт, удалить контакт, позвонить контакту.
А интеракторы — термин из программирования. Их пишут программисты, чтоб реализовать сценарии использования. Каждому сценарию соответствует отдельный интерактор. Звучит удобно, не правда ли?
Но есть нюанс. На самом деле, интерактор реализует не весь сценарий использования. По правилам Clean Architecture интерактор не должен ничего знать о пользовательском интерфейсе. Он реализует некоторую очень высокоуровневую логику — так называемые бизнес-правила. А как именно пользователь взаимодействует с приложением, его не касается. Посмотрим, к чему это приводит на практике.
Возьмем в качестве примера ту же самую телефонную книгу и не самый тривиальный юзкейс — удаление нескольких контактов за раз. Для пользователя это выглядит так:
Пользователь видит список контактов.
Пользователь выполняет долгое нажатие на одном из контактов. Контакт становится выбранным. Появляется кнопка «Удалить».
Пользователь нажимает еще на несколько контактов. Они тоже становятся выбранными.
Пользователь нажимает кнопку «Удалить». Появляется диалог подтверждения.
Пользователь подтверждает удаление. Выбранные контакты пропадают.
А таким получится интерактор:
class RemoveContactsInteractor(
private val contactsRepository: ContactsRepository
) {
suspend fun execute(contactIds: Set<ContactId>) {
contactsRepository.removeContacts(contactIds)
}
}
Видите, он практически пустой. Нет ни логики множественного выбора контактов, ни подтверждения удаления. Все это находится за пределами интерактора — скорее всего, во вью-модели. А поскольку тут и бизнес-правила никакого нет, интерактор не делает ничего полезного, а просто пробрасывает вызов в репозиторий.
И такие случаи не редкость. В большинстве мобильных приложений много взаимодействия с пользователем, но мало бизнес-правил.
Как эти рассуждения про юзкейсы и интеракторы относятся к сложным экранам? А дело в том, что сложный экран потому и сложный, что отвечает за несколько юзкейсов сразу. Было бы здорово упаковать каждый юзкейс в отдельный класс. Но интеракторы нам в этом не помогают.
Что такое компонентный подход
Компонентный подход в реальном мире
Хорошая новость в том, что вы уже знакомы с компонентным подходом. Если не из программирования, то из реального мира точно.
Человек состоит из очень мелких элементов — клеток. Но клетки не соединены беспорядочно, прослеживается иерархическая структура: клетки объединяются в ткани, ткани — в органы, органы — в системы органов, а из них складывается весь организм.
Если обобщить: менее сложные элементы объединяются в что-то более сложное, и эта процедура повторяется несколько раз. Это мы и будем называть компонентным подходом.
Можно легко найти примеры, где также проявляется этот принцип:
вся вселенная — планеты и звезды ➜ планетные системы ➜ галактики ➜ скопления галактик
персональный компьютер — от мельчайших транзисторов в процессоре до крупных составляющих, таких как системный блок и монитор
замок Хогвартс, собранный из лего
книжная библиотека
крупная IT-компания
дом
космический корабль
В общем, любые сложные объекты и системы устроены по принципу компонентного подхода.
Компонентный подход позволяет бороться со сложностью. Поскольку структура иерархическая, мы можем ее упростить, отбросив какие-то из уровней. Например, если мы скажем «Человек состоит из органов, органы образуют системы органов, а из систем органов складывается весь организм», это тоже будет правильно. Просто мы не стали спускаться до уровня тканей и клеток. Кстати, в исходной схеме тоже было упрощение, ведь и сами клетки имеют сложное строение.
В зависимости от задачи мы можем выбирать разный уровень детализации. Это свойство компонентного подхода нам еще пригодится.
Компонентный подход в Android-разработке
Мобильное приложение тоже можно представить в виде иерархической структуры. Входящие в нее элементы называются компонентами.
Бывают следующие типы компонентов:
UI-элементы — самые простые компоненты. К ним относится всё, что предоставляет UI-фреймворк (кнопки, текстовые поля, чек-боксы и т. д.), а также нестандартные UI-элементы, которые реализуют разработчики. Как правило, UI-элементы максимально абстрактны и сами по себе не решают никакой задачи пользователя.
Функциональные блоки — это уже более самодостаточные компоненты. Каждый функциональный блок отвечает за определенную функциональность на экране, то есть выполняет что-то полезное с точки зрения конечного пользователя.
Экраны — как следует из названия, отвечают за целые экраны в приложении. Сложный экран выполняет сразу несколько функций, и поэтому состоит из нескольких функциональных блоков.
Флоу (от англ. flow) — цепочки экранов, выполняющих одну общую функцию. Типичные примеры флоу: авторизация, регистрация, покупка, опросник.
Приложение — тоже считается компонентом. Оно отвечает за весь набор функций, доступных пользователю, и состоит из нескольких флоу.
Вы можете адаптировать эту структуру под ваше приложение. Например, для простых экранов можно пропускать уровень функциональных блоков и собирать экраны непосредственно из UI-элементов. В очень простом приложении может не быть нескольких флоу, и тогда оно будет состоять из отдельных экранов. А можно, наоборот, усложнять структуру: разбивать блоки на подблоки, добавлять больше уровней вложенности для навигации.
Компоненты представлены программным кодом. Код объединяется в компоненты по функциональности, а не по слоям, как мы привыкли с Clean Architecture. Обычно компонент не принадлежит какому-то одному слою (data, domain или presentation). В одном компоненте может быть и загрузка данных по сети, и какая-то логика, и отображение данных пользователю. В меньшей степени этому правилу соответствуют UI-элементы. Но даже среди них бывают такие, которые выполняют обязанности data-слоя, например, загружают изображения из интернета.
Компонент — это не обязательно один класс. Чаще всего наоборот — компонент состоит из нескольких классов. Эти классы можно организовать как угодно: делить их на слои, группировать по пакетам, делать общими для нескольких компонентов.
В зависимости от задачи мы можем выбирать разные уровни детализации. Например, когда мы собираем экран из функциональных блоков, уже не важно, из каких UI-элементов состоит каждый блок. Мы воспринимаем блоки как что-то простое и целостное. Точно так же с флоу — мы выстраиваем переходы между экранами, не задумываясь о внутреннем устройстве самих экранов. И даже организация навигации в большом приложении становится посильной задачей, ведь мы оперируем не сотней экранов, а примерно десятком флоу.
Свежий взгляд на MVVM и Clean Architecture
Компонентный подход не противоречит MVVM и Clean Architecture, а, наоборот, дополняет их и дает новые возможности. Имея бóльшую свободу выбора, мы можем по-новому посмотреть на MVVM и Clean Architecture.
MVVM 2.0
Шаблон MVVM отлично подходит для реализации экранов и функциональных блоков. Сложный экран будет состоять из нескольких вью-моделей — одной родительской и нескольких дочерних.
Родительская вью-модель выполняет координирующую функцию. Она организует взаимодействие между дочерними вью-моделями, если это потребуется.
Дочерние вью-модели отвечают за функциональные блоки. Как правило, каждый из них включает небольшой объем функциональности, и тогда его вью-модель получается простой. А если это все же не так, мы еще раз применим компонентный подход — разобьем блок на подблоки и для каждого сделаем свою вью-модель. Таким образом мы полностью решаем проблему массивных вью-моделей.
Избавление от искусственной сложности
Сложные экраны и сложная навигация — это естественные виды сложности. Они продиктованы требованиями к приложению, и поэтому мы не можем просто избавиться от них. Мы справляемся с ними, применяя MVVM, Clean Architecture и компонентный подход.
Но есть и искусственная сложность. Ее создают сами программисты, когда применяют неуместный инструмент для борьбы со сложностью. По-другому это называется overengineering.
С Clean Architecture он встречается повсеместно. Слишком большое количество слоев и абстракций. Интеракторы, которые не выполняют никаких бизнес-правил. Я даже видел примеры кода, где поверх таких интеракторов была еще одна обертка — фасад для интеракторов.
А применяя компонентный подход, сложно переоверэнжинирить. Разделение на экраны, функциональные блоки и флоу происходит естественным образом. Я ни разу не встречал случая, чтоб разработчик разбил экран на функциональные блоки слишком мелко. Также никто не будет создавать флоу, пока не найдет как минимум два экрана, которые в него войдут.
Значит ли это, что нужно повсюду использовать компонентный подход и держаться подальше от Clean Architecture? Конечно, нет! Каждому инструменту — свое применение.
Компонентный подход великолепно справляется с тем, чтобы разбить приложение на слабосвязанные кусочки функциональности. А Clean Architecture незаменим в реализации этих кусочков. Но последовательность именно такая: сначала разбиваем код на компоненты, и только после этого по мере необходимости вводим слои, интеракторы и прочие абстракции.
Поступая так, вы обнаружите, что для решения большинства задач достаточно очень легковесной Clean Architecture. В ней мало слоев — три или даже два. Юзкейсы упакованы по отдельным компонентам, а интеракторы встречаются только там, где есть бизнес-правила.
Дополнительные материалы
Книга Р. Мартин, “Чистая Архитектура. Искусство разработки программного обеспечения” — исчерпывающее объяснение Clean Architecture.
Статья «Заблуждения Clean Architecture» — самая полезная статья про Clean Architecture из известных мне.
Доклад на Mobius «Вы за это заплатите! Цена чистой архитектуры» — на чем можно сэкономить, применяя Clean Architecture.
Доклад «The immense benefits of not thinking in screens» — про преимущества компонентного подхода.
Продолжение следует
Надеюсь, у вас сложилась картина, что такое компонентный подход и чем он так хорош. Буду рад ответить на любые вопросы по теме в комментариях. А в следующих статьях подробно расскажу, как применять компонентный подход на практике.
Комментарии (11)
Master255
28.12.2022 02:55Приходится ломать голову, чтобы добавить что-то новое, не сломав ничего из старого. Скорость разработки падает. Знакомая ситуация?
10+ лет в Андроид разработке. Скорость разработки не падает при добавлении любого количества фич. Дальше читать не интересно...
atc
28.12.2022 06:07+2В этом случае ваши фичи должны совсем (абсолютно) не пересекаться, что маловероятно при их большом количестве в рамках одного продукта. Очень сомнительное утверждение.
abaz20
29.12.2022 17:39+1Во многом согласен. Вообще программирование - это искусство, творя которое, надо уметь использовать различные подходящие для данного произведения инструменты. Последние годы наблюдаю все больше людей-"кодеров" пишущих по инструкции. Хит сезона SOLID. Но серебрянную пулю все никак не могут найти. Борьба с одними недостатками приводит к появлению недостатков в другом месте.
Racheengel
Компонентный подход хорош ровно до того момента, когда появляется необходимость что- либо модифицировать либо масштабировать. Вот тогда и начинаются настоящие танцы с бубнами...
dyadyaSerezha
Вообще-то он придуман именно для того, чтобы было легко модифицировать и масштабировать.
А вообще, low coupling and high cohesion (суть компонентного подхода) - это и есть неявная, но главная цель SOLID принципов хорошего дизайна.
Racheengel
Вы с модульным подходом, случайно, не путаете?
Мы отказались от компонентов в пользу модулей, с тех пор модификации и масшабирование проходит намного проще.
a_artikov Автор
Суть компонентного подхода - разделить сложную штуку на более простые части. Это не может не работать.
А вот криво реализовать это в коде, конечно, можно. Тут с вами спорить не буду)
Racheengel
Это может не работать, если увлечься делением штук на простые штуки и наплодить макаронного монстра со 100500 связей между компонентами, которые никто не будет в состоянии отследить. Главное ещё, каждую компоненту не забывать в генерические интерфейсы оборачивать, а в коде продожить слой dynamic_cast-ов или их аналогов. И тогда "будет счастье ".
a_artikov Автор
Не надо так делать, и тогда будет счастье)
Racheengel
Правильно, не надо так делать. Но компоненты этому способствуют. Поэтому лучше делать в модули.
a_artikov Автор
Нет, это значит, что компонентный подход был применен неправильно. Вы ударили себе молотком по пальцу и из этого сделали вывод "Молотки - это плохо".