January 31, 2020

Архитектура Android-приложений… Правильный путь?

Повествование ведется от лица автора оригинальной статьи Fernando Cejas.

За последние несколько месяцев, а также после дискуссий на Tuenti с коллегами вроде @pedro_g_s и @flipper83 (кстати говоря, 2 крутых Android-разработчика), я решил, что имеет смысл написать заметку о проектировании Android-приложений.

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

Приступая к работе

Мы знаем, что написание качественного программного обеспечения — сложное и многогранное занятие: программа должна не только соответствовать установленным требованиям, но и быть надёжной, удобной в сопровождении, тестируемой и достаточно гибкой для добавления или изменения функций. И здесь появляется понятие Clean Architecture ("Чистая Архитектура"), которое неплохо бы держать в уме при разработке любого приложения.

Идея проста: Clean Architecture основывается на группе методов, реализующих системы, которые являются:

  • Независимыми от фреймворков
  • Тестируемыми
  • Независимыми от 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-модуль.

Заключение

Дядя Боб сказал: “Архитектура — это намерения, а не фреймворки”, и я полностью с ним согласен. Конечно, есть разные способы реализовать какую-либо вещь, и я уверен, что вы, как и я, каждый день сталкиваетесь с трудностями в этой области, но, используя данные приёмы, вы сможете быть уверены, что ваше приложение будет:

  • простым в поддержке,
  • простым в тестировании,
  • составлять единое целое,
  • будучи разделённым.

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

Ссылки и источники

  1. Код: https://github.com/android10/Android-CleanArchitecture
  2. The clean architecture от Дяди Боба
  3. Архитектура — это намерения, а не фреймворки
  4. Model View Presenter
  5. Repository Pattern от Martin Fowler
  6. Презентация по Android Design Patterns

Источник: Архитектура Android-приложений… Правильный путь?