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

http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/
  • Перевод
От переводчика: Некоторые термины, которые использует автор, не имеют общепринятого перевода (ну, или я его не знаю:), поэтому я решил оставить большинство на языке оригинала — они всё равно понятны и для тех, кто пишет под 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). Это вынужденная плата за то, чтобы модели внутри приложения не пересекались). Вот схема, как это выглядит:

3 слоя

Примечание: я не использовал внешних библиотек (кроме 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.

image

Если вы хотите увидеть крутой пример эффективного Android UI, который использует MVP и MVVM, взгляните на пример реализации от моего друга Pedro Gómez.

Domain Layer (Слой бизнес-логики)


Вся логика реализована в этом слое. Рассматривая проект, вы увидите здесь реализацию interactor-ов (Use Cases — методы использования).

Этот слой — модуль на чистой джаве без никаких Android-зависимостей. Все внешние компоненты используют интерфейсы для связи с бизнес-объектами.

domain layer

Data Layer (Слой данных)


Все данные, необходимые для приложения, поставляются из этого слоя через реализацию UserRepository (интерфейс находится в domain layer — слое бизнес-логики), который использует Repository Pattern со стратегией, которая, через фабрику, выбирает различные источники данных, в зависимости от определенных условий.

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

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

repository implementation

Примечание: что касается кода, помня, что код — это учебный пример, я реализовал очень простой, даже примитивный кэш, используя хранения 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
  • +29
  • 76,5k
  • 9
Поделиться публикацией

Комментарии 9

    +5
    Решил, что «Архитекрута» — это не опечатка, а новый термин, определяющий крутую архитектуру.
      0
      Хаха, спасибо, поправил.
      +1
      Грамотно, но ничего нового. Рано или поздно любой разработчик приходит к модульной архитектуре. Тут главное не переабстрагироваться, а то можно для решения простейшей задачи написать огромный красивый фреймворк, но иногда это излишне. *картинка булки хлеба-тролейбуса*
        0
        Окей, с точки зрения потока данных все хорошо, вроде бы. Теперь предположим, что для пользователя у нас есть трек, в виде url, который по нажатию кнопки нужно проиграть, стримом. Проигрывание, естественно, выносится в сервис. Как это впишется в предложенную модель?
          +1
          Как то сложно это, для 2 экранов создавать более 50 классов. Может для некоторых классов приложений это актуально, но для клиент-серверных приложений, в которых вся логика, как правило, находится на сервере это уж слишком.
            0
            Главное — помнить, что андроид может убить твое приложение в процессе выполнения долгой задачи (загрузка данных в сервисе и т.д.)
            Необходимо реализовать механизм восстановления процесса получения этих данных, если необходимо.
            Также важно выполнять все долгие операции в фоновых потоках (работа с базой, причем все типы запросов и т.д.)
            Даже банальное отображение картинки может выглядеть так:
            //абстрактный пример
            if (!tryGetImageFromMemory()) {
            	if (!tryGetImageFromFileCache()) {
            		tryGetImageFromInternet();
            	}
            }
              0
              А как автор планирует держать модель в памяти, чтобы к ней могли обращаться все констроллеры (MVC)? Стоит ли использовать для этого шаблон Одиночка?
                0
                "Правило Зависимостей: код должен иметь зависимости только во внутренние круги и не должен иметь никакого понятия, что происходит во внешних кругах. "

                Тогда почему в данном примере Data Layer знает о Domain Layer? Знает о модели данных Domain Layer, реализует некоторые интерфейсы. Следуя правилу зависимостей нужно что бы Data Layer предоставлял интерфейс для получения данных, который будет использоваться в Domain Layer.
                  0
                  В этой дискуссии как раз и объясняется почему Data Layer знает о Domain Layer.
                  https://github.com/android10/Android-CleanArchitecture/issues/136

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

                Самое читаемое