- Архитектура и дизайн Android приложения (мой опыт)
- Disclaimer
- Проблема: зачем нам нужна архитектура?
- Цель архитектуры и дизайна
- Решение
- Пример
- Архитектура Android-приложений… Правильный путь?
- Приступая к работе
- Наш сценарий
- Android-архитектура
- Presentation Layer (Слой представления)
- Domain Layer (Слой бизнес-логики)
- Data Layer (Слой данных)
- Обработка ошибок
- Тестирование
- Покажите мне код
- Заключение
Архитектура и дизайн Android приложения (мой опыт)
Сегодня я хочу рассказать об архитектуре, которой я следую в своих Android приложениях. За основу я беру Clean Architecture, а в качестве инструментов использую Android Architecture Components (ViewModel, LiveData, LiveEvent) + Kotlin Coroutines. К статье прилагается код вымышленного примера, который доступен на GitHub.
Disclaimer
Проблема: зачем нам нужна архитектура?
Большинство проектов, в которых мне доводилось участвовать, имеют одну и ту же проблему: внутрь андроид окружения помещается логика приложения, что приводит к большому объему кода внутри Fragment и Activity. Таким образом код обрастает зависимостями, которые совсем не нужны, модульное тестирование становится практически невозможным, так же, как и повторное использование. Фрагменты со временем становятся God-объектами, даже мелкие изменения приводят к ошибкам, поддерживать проект становится дорого и эмоционально затратно.
Есть проекты, которые вообще не имеют никакой архитектуры (тут все понятно, к ним вопросов нет), есть проекты с претензией на архитектуру, но там все равно появляются точно такие же проблемы. Сейчас модно использовать Clean Architecture в Android. Часто видел, что Clean Architecture ограничивается созданием репозиториев и сценариев, которые вызывают эти репозитории и больше ничего не делают. Того хуже: такие сценарии возвращают модели из вызываемых репозиториев. И в такой архитектуре смысла нет вообще. И т.к. сценарии просто вызывают нужные репозитории, то часто логика ложится на ViewModel или, еще хуже, оседает во фрагментах и активностях. Все это потом превращается в кашу, не поддающуюся автоматическому тестированию.
Цель архитектуры и дизайна
Цель архитектуры – отделить нашу бизнес-логику от деталей. Под деталями я понимаю, например, внешние API (когда мы разрабатываем клиент для REST сервиса), Android – окружение (UI, сервисы) и т.д. В основе я использую Clean architecture, но со своими допущениями в реализации.
Цель дизайна – связать вместе UI, API, Бизнес-логику, модели так, чтобы все это поддавалось автоматическому тестированию, было слабо связанным, легко расширялось. В дизайне я использую Android Architecture Components.
Для меня архитектура должна удовлетворять следующим критериям:
- UI — максимально простой, и у него есть только три функции:
- Представлять данные пользователю. Данные приходят уже готовые для отображения. Это основная функция UI. Тут виджеты, анимации, фрагменты и т.д.
- Реагировать на события. Здесь отлично помогает ViewModel и LiveData.
- Отправлять команды от пользователя. Для этого я использую свой простой framework, основанный на командах. Об этом чуть позже.
- Бизнес-логика зависит только от абстракций. Это нам позволяет менять реализацию произвольных компонентов.
Решение
Принципиальная схема архитектуры представлена на рисунке ниже:
Мы движемся снизу вверх по слоям, и слой, который находится ниже, ничего не знает о слое сверху. А верхний слой ссылается только на слой, который находится на один уровень ниже. Т.е. слой API не может ссылаться на домен.
Слой домен содержит бизнес сущности со своей логикой. Обычно здесь находятся сущности, которые существуют и без приложения. Например, для банка здесь могут находиться сущности кредитов со сложной логикой расчета процентов и т.д.
Слой логики приложения содержит сценарии работы самого приложения. Именно здесь определяются все связи приложения, выстраивается его суть.
Слой api, android – это лишь конкретная реализация нашего приложения в Android – среде. В идеале этот слой можно менять на что угодно.
Причем, когда я приступаю к разработке приложения, я начинаю с самого нижнего слоя — домена. Потом появляется второй слой сценариев. На 2-ом слое все зависимости от внешних деталей реализуются через интерфейсы. Вы абстрагированы от деталей, можно сконцентрироваться только на логике приложения. Тут же уже можно начинать писать тесты. Это не TDD подход, но близко к этому. И только в самом конце появляется сам Android, API с реальными данными и т.д.
Теперь более развернутая схема дизайна Android-приложения.
Итак, слой логики является ключевым, он и есть приложение. Только слой логики может ссылаться на домен и взаимодействовать с ним. Также слой логики содержит интерфейсы, которые позволяют логике взаимодействовать с деталями приложения (api, android и т.д.). Это так называемый принцип инверсии зависимости, который позволяет логике не зависеть от деталей, а наоборот. Слой логики содержит в себе сценарии использования приложения (Use Cases), которые оперируют разными данными, взаимодействуют с доменом, репозиториями и т.д. В разработке мне нравится мыслить сценариями. На каждое действие пользователя или событие от системы запускается некоторый сценарий, который имеет входные и выходные параметры, а также всего лишь один метод – запустить сценарий.
Кто-то вводит дополнительное понятие интерактора, который может объединять несколько сценариев использования и накручивать дополнительную логику. Но я этого не делаю, я считаю, что каждый сценарий может расширять или включать любой другой сценарий, для этого не нужен интерактор. Если посмотреть на схемы UML, то там можно увидеть связи включения и расширения.
Общая схема работы приложения следующая:
- Создается android-окружение (активити, фрагменты, и т.д.).
- Создается ViewModel (одна или несколько).
- ViewModel создает необходимые сценарии, которые можно запустить из этой ViewModel. Сценарии лучше инжектить с помощью DI.
- Пользователь совершает действие.
- Каждый компонент UI связан с командой, которую он может запустить.
- Запускается сценарий с необходимыми параметрами, например, Login.execute(login,password).
- Сценарий также с помощью DI получает нужные репозитории, провайдеры. Сценарий делает запрос на получение необходимых данных (может быть несколько асинхронных запросов api, да что угодно). Репозиторий выполняет запросы и возвращает данные сценарию. Причем у репозитория есть свои модели данных, которые он использует для своей внутренней работы, например, репозиторий для REST будет содержать модели со всякими JSON конверторами. Но перед тем, как отдать результат в сценарий, репозиторий всегда преобразовывает данные в модели данных сценария. Таким образом, логика ничего не знает о внутреннем устройстве репозитория и не зависит от него. Получив все необходимые данные, сценарий может создать необходимые объекты из домена. Выполнить какую-то логику на домене. Когда сценарий закончит работу, он обязательно преобразует свой ответ в очередную модель представления. Сценарий прячет уровень домена, он отдает данные, которые сразу понятны слою представления. Сценарий использования также может содержать в себе служебные сценарии, например, обработка ошибок.
- Сценарий вернул данные или ошибку в команду. Теперь можно обновить состояние ViewModel, которая в свою очередь обновит UI. Я обычно это делаю с помощью LiveData (п.9 и 10).
Т.е. ключевую роль у нас занимает логика и ее модели данных. Мы увидели двойное преобразование: первое – это преобразование репозитория в модель данных сценария и второе – преобразование, когда сценарий отдает данные в окружение, как результат своей работы. Обычно результат работы сценария отдается во viewModel для отображения в UI. Сценарий должен отдать такие данные, с которыми viewModel и UI ничего больше не делает.
UI запускает выполнение сценария с помощью команды. В моих проектах я использую собственную реализацию команд, они не являются частью архитектурных компонент или еще чего-либо. В общем, их реализация несложная, в качестве более глубокого знакомства с идеей можете посмотреть реализацию команд в reactiveui.net для C#. Я, к сожалению, не могу выложить свой рабочий код, только упрощенную реализацию для примера.
Основная задача команды — это запускать некоторый сценарий, передав в него входные параметры, а после выполнения вернуть результат команды (данные или сообщение об ошибке). Обычно все команды выполняются асинхронно. Причем команда инкапсулирует метод background-вычислений. Я использую корутины, но их легко заменить на RX, и сделать это придется всего лишь в абстрактной связке command+use case. В качестве бонуса команда может сообщать свое состояние: выполняется ли она сейчас или нет и может ли она выполниться в принципе. Команды легко решают некоторые проблемы, например, проблему двойного вызова (когда пользователь кликнул несколько раз на кнопку, пока операция выполняется) или проблемы видимости и отмены выполнения.
Пример
Реализовать фичу: вход в приложение с помощью логина и пароля.
Окно должно содержать поля ввода логина и пароля и кнопку “Вход”. Логика работы следующая:
- Кнопка “Вход” должна быть неактивной, если логин и пароль содержат менее 4 символов.
- Кнопка “Вход” должна быть неактивной во время выполнения процедуры входа.
- Во время выполнения процедуры входа должен отображаться индикатор (лоадер).
- Если вход выполнен успешно, то должно отобразиться приветственное сообщение.
- Если логин и/или пароль неверные, то должна появиться надпись об ошибке над полем ввода логина.
- Если надпись об ошибке отображена на экране, то любой ввод символа в полях логин или пароль, убирают эту надпись до следующей попытки.
Эту задачу можно решить разными способами, например, поместить все в MainActivity.
Но я всегда слежу за выполнением моих двух главных правил:
- Бизнес-логика не зависит от деталей.
- UI максимально простой. Он занимается только своей задачей (представляет данные, которые ему переданы, а также транслирует команды от пользователя).
Так выглядит приложение:
MainActivity выглядит следующим образом:
Активити достаточно прост, правило UI выполняется. Я написал несколько простых расширений, типа bindVisibleWithCommandIsExecuting, чтобы связывать команды с элементами UI и не дублировать код.
Код этого примера с комментариями доступен на GitHub, если интересно, можете скачать и ознакомиться.
Источник
Архитектура Android-приложений… Правильный путь?
От переводчика: Некоторые термины, которые использует автор, не имеют общепринятого перевода (ну, или я его не знаю:), поэтому я решил оставить большинство на языке оригинала — они всё равно понятны и для тех, кто пишет под android, но не знает английский.
Куда писать об ошибках и неточностях, вы знаете.
За последние несколько месяцев, а также после дискуссий на Tuenti с коллегами вроде @pedro_g_s и @flipper83 (кстати говоря, 2 крутых Android-разработчика), я решил, что имеет смысл написать заметку о проектировании Android-приложений.
Цель поста — немного рассказать о подходе к проектированию, который я продвигал в последние несколько месяцев, и также поделиться всем тем, что я узнал во время исследования и реализации этого подхода.
Приступая к работе
Мы знаем, что написание качественного программного обеспечения — сложное и многогранное занятие: программа должна не только соответствовать установленным требованиям, но и быть надёжной, удобной в сопровождении, тестируемой и достаточно гибкой для добавления или изменения функций. И здесь появляется понятие “стройной архитектуры”, которое неплохо бы держать в уме при разработке любого приложения.
Идея проста: стройная архитектура основывается на группе методов, реализующих системы, которые являются:
- Независимыми от фреймворков.
- Тестируемыми.
- Независимыми от UI.
- Независимыми от БД.
- Независимыми от любой внешней службы.
Кругов необязательно должно быть 4 (как на диаграмме), это просто схема; важно учитывать Правило Зависимостей: код должен иметь зависимости только во внутренние круги и не должен иметь никакого понятия, что происходит во внешних кругах.
Вот небольшой словарь терминов, необходимых для лучшего понимания данного подхода:
- Entities (Сущности): это бизнес-логика приложения.
- Use Cases (Методы использования): эти методы организуют поток данных в Entities и из них. Также их называют Interactors (Посредниками).
- Interface Adapters (Интерфейс-Адаптеры): этот набор адаптеров преобразовывает данные из формата, удобного для Методов использования и Сущностей. К этим адаптерами принадлежат Presenter-ы и Controller-ы.
- Frameworks and Drivers: место скопления деталей: UI, инструменты, фреймворки, БД и т. д.
Для лучшего понимания обратитесь к данной статье или к данному видео.
Наш сценарий
Я начал с простого, чтобы понять, как обстоят дела: создал простенькое приложение, которое отображает список друзей, который получает с облака, а при клике по другу отображает более детальную информацию о нём на новом экране.
Я оставлю здесь видео, чтобы вы поняли, о чем идёт речь:
Android-архитектура
Наша цель — разделение задач таким образом, чтобы бизнес-логика ничего не знала о внешнем мире, так, чтобы можно было тестировать её без никаких зависимостей и внешних элементов.
Для достижения этого я предлагаю разбить проект на 3 слоя, каждый из которых имеет свою цель и может работать независимо от остальных.
Стоит отметить, что каждый слой использует свою модель данных, таким образом можно достигнуть необходимой независимости (вы можете увидеть в коде, для выполнения трансформации данных необходим преобразователь данных (data mapper). Это вынужденная плата за то, чтобы модели внутри приложения не пересекались). Вот схема, как это выглядит:
Примечание: я не использовал внешних библиотек (кроме gson для парсинга json и junit, mockito, robolectric и espresso для тестирования) для того, чтобы пример был более наглядным. В любом случае, не стесняйтесь использовать ORM для хранения информации или любой dependency injection framework, да и вообще инструменты или библиотеки, которые делают вашу жизнь легче (помните: изобретать велосипед заново — не лучшая идея).
Presentation Layer (Слой представления)
Здесь логика связывается с Views (Представлениями) и происходят анимации. Это не что иное, как Model View Presenter (то есть, MVP), но вы можете использовать любой другой паттерн вроде MVC или MVVM. Я не буду вдаваться в детали, но fragments и activities — это всего лишь views, там нету никакой логики кроме логики UI и рендеринга этого самого отображения. Presenter-ы на этом слое связываются с interactors (посредниками), что предполагает работу в новом потоке (не в UI-потоке), и передачи через коллбэки информации, которая будет отображена во view.
Если вы хотите увидеть крутой пример эффективного Android UI, который использует MVP и MVVM, взгляните на пример реализации от моего друга Pedro Gómez.
Domain Layer (Слой бизнес-логики)
Вся логика реализована в этом слое. Рассматривая проект, вы увидите здесь реализацию interactor-ов (Use Cases — методы использования).
Этот слой — модуль на чистой джаве без никаких Android-зависимостей. Все внешние компоненты используют интерфейсы для связи с бизнес-объектами.
Data Layer (Слой данных)
Все данные, необходимые для приложения, поставляются из этого слоя через реализацию UserRepository (интерфейс находится в domain layer — слое бизнес-логики), который использует Repository Pattern со стратегией, которая, через фабрику, выбирает различные источники данных, в зависимости от определенных условий.
Например, для получения конкретного пользователя по id источником данных выбирается дисковый кэш, если пользователь уже загружен в него, в противном случае облаку отправляется запрос на получение данных для дальнейшего сохранения в тот же кэш.
Идея всего этого заключается в том, что происхождение данных является понятным для клиента, которого не волнует, поступают данные из памяти, кэша или облака, ему важно только то, что данные будут получены и доступны.
Примечание: что касается кода, помня, что код — это учебный пример, я реализовал очень простой, даже примитивный кэш, используя хранения shared preferences. Помните: НЕ СТОИТ ИЗОБРЕТАТЬ ВЕЛОСИПЕД, если существуют библиотеки, хорошо решающие поставленную задачу.
Обработка ошибок
Это большая тема, в которой всегда есть, что обсудить (и здесь автор предлагает делиться своими решениями). Что до моей реализации, я использовал коллбэки, которые, на случай, если что-то случается, скажем, в хранилище данных, имеют 2 метода: onResponse() и onError(). Последний инкапсулирует исключения во wrapper class (класс-обертку) под названием “ErrorBundle”: Такой подход сопряжен с некоторыми трудностями, потому что бывают цепочки обратных вызовов один за другим, пока ошибка не выходит на presentation layer, чтобы отобразиться. Читаемость кода из-за этого может быть немного нарушена.
С другой стороны, я реализовал систему event bus, которая бросает события, если что-то не так, но такое решение похоже на использование GOTO, и, по-моему, иногда, если не управлять событиями предельно внимательно, можно заблудиться в цепи событий, особенно когда их одновременно бросается несколько.
Тестирование
Что касается тестирования, я применил несколько решений, в зависимости от слоя.
- Presentation Layer: существующий android инструментал и espresso для интеграции и функционального тестирования.
- Domain Layer: JUnit + mockito использовались для юнит-тестов.
- Data Layer: Robolectric (так как этот слой имеет android-зависимости) + junit + для интеграции и юнит-тестов.
Покажите мне код
Думаю, в этом месте вам интересно посмотреть на код. Что ж, вот ссылка на github, где можно найти, что же у меня получилось. Что стоит упомянуть о структуре папок так это то, что разные слои представлены разными модулями:
- presentation: это android-модуль для presentation layer.
- domain: java-модуль без android-зависимостей.
- data: android-модуль, откуда поступают все данные.
- data-test: тесты для data layer. Из-за определённых ограничений, при использовании Robolectric мне пришлось использовать отдельный java-модуль.
Заключение
Дядя Боб сказал: “Архитектура — это намерения, а не фреймворки”, и я полностью с ним согласен. Конечно, есть разные способы реализовать какую-либо вещь, и я уверен, что вы, как и я, каждый день сталкиваетесь с трудностями в этой области, но используя данные приёмы, вы сможете быть уверены, что ваше приложение будет:
- Простым в поддержке.
- Простым в тестировании.
- Составлять единое целое,
- Будучи разделённым.
В заключение я настоятельно рекомендую вам попробовать эти методы, посмотреть на результат и поделиться своим опытом как в отношении этого, так любого другого подхода, который, по вашим наблюдениям, работает лучше: мы уверены, что постоянное совершенствование всегда полезно и положительно.
Источник