Перейти к содержимому

Clean architecture что это

  • автор:

Как Clean Architecture станет вашим секретным оружием!

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

Введение в Clean Architecture

Clean Architecture — это архитектурный шаблон, предложенный Робертом Мартином, известным также как «Дядя Боб». Главной идеей Clean Architecture является разделение приложения на независимые слои, каждый из которых выполняет конкретную функцию. Эти слои взаимодействуют между собой через чётко определенные интерфейсы, что позволяет достичь высокой гибкости и расширяемости приложения.

Основные принципы Clean Architecture

Чистая архитектура на Go: плюсы и минусы

15-17 июля в Слёрм пройдёт практический интенсив «Чистая архитектура приложения на Go». Мы пообщались с его автором Николаем Колядко, Senior Go Backend в Robovoice. Он рассказал, что такое чистая архитектура и какие проблемы она помогает решить. А ещё разобрал основные плюсы и минусы такого подхода к разработке приложений.

Что такое чистая архитектура

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

Плюсы чистой архитектуры

На проекте с плохой архитектурой задачу, которую можно решить за час, ты делаешь две недели, а потом ещё чинишь полгода, потому что у тебя много лишних зависимостей. Чистая архитектура убирает лишние зависимости и собирает главную функциональность приложения в одном месте — в домене. Функциональность в домене независима, за счёт чего её проще тестировать. Плюс, обособленный домен помогает быстрее искать ошибки и неточности, упрощает написание тестов.
В чистой архитектуре сценарии приложения (use case) описаны отдельно. Именно они определяют, какие сторонние сервисы нам понадобятся. Благодаря этому мы получаем больше свободы в выборе инструментов и можем подстраивать внешний мир под свои нужды, а не наоборот.

Удобство тестирования, независимость от фреймворков, баз данных и UI — вот основные плюсы чистой архитектуры.

Какие проблемы решает чистая архитектура

  • Проблема реализации. Первый симптом, что пора внедрять чистую архитектуру, — тебе нужно вносить изменения, а ты боишься, потому что не знаешь, к чему это приведёт. Я неоднократно становился свидетелем ситуации, когда люди с опаской добавляли новую функциональность в проект, а потом что-то реально ломалось. Чистая архитектура как раз позволяет избавиться от такого страха и сделать процесс внесения изменений более предсказуемым и управляемым.
  • Проблема расширения. Предположим, тебе нужно сделать голосовой помощник, который бы распознавал ответы клиентов и продолжал диалог. Но идея звукового распознавания ничем не отличается от распознавания ответов клиентов в чате. А такой процесс у тебя уже настроен для Telegram. Хорошим архитектурным решением является создания ядра принятия решений, к которому будут подключаться другие сервисы, которые работают с конкретным каналом связи. Так, логика принятия решения будет находится в одном месте, поэтому её не нужно переписывать с нуля.

Минусы чистой архитектуры

На мой взгляд, основной минус чистой архитектуры в том, что тебе нужно писать больше кода. Допустим, ты хочешь настроить получение контактов из базы данных. Ты можешь написать метод, который обратится к библиотеке, сделать там небольшой select и задать ID-шник. Затем отправить данные на front, и это будет что-то около 200 строк.
В чистой архитектуре тебе сначала нужно сделать папку со слоем delivery, который будет принимать данные от front. Затем описать данные, которые должен прислать front, и вызвать слой use case. Use case проведёт валидацию, вызовет метод обращения к базе, и только после этого ты сможешь получить данные.
Ещё один минус — высокий порог входа. Продумать взаимодействие всех модулей системы довольно сложно, а для новичков это и вовсе непосильная задача.

Как удержать проект в рамках чистой архитектуры

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

Для тех, кто хочет разобраться в слоях и понять, что такое чистая архитектура приложения на Go, мы запускаем новый практический интенсив «Чистая архитектура приложения на Go», который пройдет 15-17 июля.

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

Лекция 4 по архитектуре андроид приложения. Clean Architecture

Четвертая лекция курса по архитектуре клиент-серверных android-приложений, в которой мы поговорим о том, что такое Clean Architecture и как реализовать чистую архитектуру в андроид. Также познакомимся с библиотекой Google Agera, которая реализует парадигму реактивного функционального программирования на Android.

Чтобы более тесно на практике познакомиться с чистой архитектурой и архитектурными компонентами, записывайтесь на продвинутый курс по разработке приложения «Чат-мессенжер»

  1. Введение
  2. Clean Architecture
  3. Бизнес-объекты
  4. Сценарии взаимодействия
  5. Слой представления
  6. Фреймворки
  7. Clean Architecture Android
  8. Слой данных
  9. Слой бизнес-логики
  10. Слой представления
  11. Пример приложения
  12. Дополнительно – Google Agera
  13. Практика
  14. Практика 2
  15. Ссылки и полезные ресурсы
  16. Продолжение:
  17. Лекция 5 по архитектуре андроид приложения. Паттерн MVP

Введение

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

  • Реализация клиент-серверного взаимодействия.
  • Обеспечение возможности тестирования классов, содержащих бизнес-логику приложения.

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

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

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

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

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

Так что же с архитектурой? В этом вопросе у нас все достаточно комфортно. Сейчас идет 2017-ый год, и лучшие умы человечества уже давно продумали различные варианты построения архитектур для сложных систем. Более того, приложения для Android – это не слишком сложная система, поэтому у нас вообще не должно возникнуть никаких проблем.

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

И снова возвращаемся к первому занятию. В нем было сказано, что основой для бурного развития исследования архитектуры клиент-серверных Android-приложений стала статья Фернандо Цехаса, “Architecting Android… The Clean Way?”, которая в свою очередь является переложением подхода “The Clean Architecture” от Роберта Мартина. Именно поэтому для нас логично рассмотреть эти два первоисточника, чтобы двигаться дальше.

Clean Architecture

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

  1. Архитектура должна быть независима от различных фреймворков. Разумеется, в современном мире мы не можем обходиться без каких-то библиотек, которые позволяют решать задачи намного быстрее и чаще эффективнее, чем это сделали бы мы в случае самостоятельной реализации. Но здесь важно понимать, что библиотека должна встраиваться в вашу архитектуру, а не архитектура должна подстраиваться под выбранную библиотеку. При использовании библиотеки, которая вынуждает вас переделывать архитектуру приложения, вы всегда будете сталкиваться с определенными ограничениями этой библиотеки и не сможете перестраивать архитектуру нужным вам образом. Нужно использовать библиотеки только в качестве вспомогательных инструментов.
  2. Система должна быть протестирована. При этом вы должны иметь возможность тестировать как модули системы по отдельности, так и тестировать взаимодействие этих модулей между собой и интеграцию их в системе. Кроме того необходимо тестировать систему без UI, реального сервера и работы с базой данных, то есть архитектура должна быть независима от окружения.
  3. Из предыдущих пунктов плавно вытекают и следующие принципы, которые говорят о том, что ваше приложение должно быть независимо от всего: от интерфейса пользователя, от работы с базой данных, от работы сервера и от других элементов окружения. Независимость архитектуры от окружения очень важна, так как это позволяет менять различные компоненты окружения без изменения самой архитектуры. Что подразумевается под изменением компонентов архитектуры? Это может быть изменение в выборе базы данных (или же вообще отказ от нее) или же изменение в UI-части приложения (к примеру, нужно изменить внешний вид экрана). Если для внесения таких изменений требует от вас изменения архитектуры, то, возможно, вам стоит пересмотреть архитектуру.

Для построения архитектуры системы предлагается рассмотреть следующую схему, где каждая концентрическая окружность является определенным компонентом системы:

Лекция 4 по архитектуре андроид приложения. Clean Architecture

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

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

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

Бизнес-объекты

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

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

Сценарии взаимодействия

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

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

Слой представления

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

На этом слое реализуются такие архитектурные паттерны как Model-View-Controller или Model-View-Presenter. К примеру, Controller может передавать данные в сценарии взаимодействия, получать результат и передавать его для отображения во View.

Фреймворки

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

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

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

Использование такого подхода позволяет создать архитектуру, принципы которой были описаны в начале раздела. В силу правила зависимостей мы получаем соответствие принципам 1 и 3, а такое разбиение системы на модули позволяет тестировать ее на разных уровнях.

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

Clean Architecture Android

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

Такая адаптация была изложена в статье Фернандо Цехаса “Architecting Android… The clean way?”, которую мы и разберем детально.

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

Чтобы достичь независимости и возможности тестирования, предлагается разбить приложение на 3 ключевых слоя:

  • Слой данных (Data Layer)
  • Слой бизнес-логики (Domain Layer)
  • Слой представления (Presentation Layer)

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

Схема этих слоев выглядит следующим образом:

Лекция 4 по архитектуре андроид приложения. Clean Architecture

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

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

Рассмотрим указанные слои подробнее.

Слой данных

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

Лекция 4 по архитектуре андроид приложения. Clean Architecture

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

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

Слой бизнес-логики

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

Фернандо Цехас предлагает реализовывать слой бизнес-логики в виде Java-модуля, который не содержит никаких зависимостей от Android-классов. И это хороший подход, так как для реализации бизнес-логики нам нужны только классы моделей и стандартные средства языка Java. Более того, такой подход позволит легко тестировать этот слой с помощью обычных тестов на JUnit, что очень удобно. В таком случае иногда не будет возможности выполнить какой-либо метод или использовать некоторые классы из других слоев. Поэтому для взаимодействия с этим слоем используются интерфейсы.

Слой представления

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

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

Конечно, наиболее часто используется паттерн MVP, который мы и будем рассматривать дальше. Подробнее его мы рассмотрим далее, а пока можно сказать, что это достаточно простой паттерн, который позволит нам разделить экран на UI-часть (View), на логику работы с UI (Presenter) и объекты для взаимодействия с UI (Model). В общем виде этот паттерн выглядит следующим образом:

Лекция 4 по архитектуре андроид приложения. Clean Architecture

И, разумеется, при разбиении приложения на такие слои нельзя забывать о главном правиле из “чистой” архитектуры – правиле зависимостей:

Лекция 4 по архитектуре андроид приложения. Clean Architecture

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

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

Очевидно, что самый важный слой с точки зрения тестирования – это слой бизнес-логики. А так как мы используем его в виде обычного Java-модуля, то тестировать его очень просто. Для этого достаточно стандартного фреймворка JUnit и, возможно, Mockito. Здесь нет проблем, специфичных для системы Android.

Слой данных также тестируются с помощью Unit-тестов на JUnit, но это уже требует определенных усилий, так как этот модуль содержит зависимости от классов Android. К примеру, для тестирования этого слоя можно дополнительно использовать фреймворк Robolectric.

И последний слой, слой представления, не содержит критичной логики, зато содержит много UI-элементов, которые также должны быть протестированы. Для этого можно воспользоваться специальными фреймворками, которые позволяют проводить интеграционное и UI-тестирование приложений, к примеру, Espresso.

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

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

И последний открытый вопрос – это вопрос об управлении жизненным циклом. Поскольку слой данных никак не относится к проблемам пересоздания Activity и другим проблемам жизненного цикла, а слой бизнес-логики вообще не знает ничего о системе Android, то очевидно, что обработкой жизненного цикла должен заниматься уровень представления. К примеру, хорошим вариантом будет передача в Presenter делегата для управления жизненным циклом, что и будет использоваться далее. Но, разумеется, здесь можно использовать любой из способов, которые были рассмотрены на первой лекции.

Пример приложения

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

В качестве основы мы возьмем приложение для просмотра фильмов с прошлой лекции и переделаем его архитектуру согласно принципам “чистой” архитектуры.

В примере мы будем следовать идеям, которые изложил Фернандо Цехас в своей статье, и также разобьем приложение на 3 модуля: data, domain и presentation. При этом нужно помнить о том, что модуль domain является Java-модулем и не содержит классов Android.

Начнем со слоя данных. Напомним, как мы получали данные о фильмах на прошлом занятии:

mMoviesSubscription = ApiFactory.getMoviesService() .popularMovies() .map(MoviesResponse::getMovies) .flatMap(movies -> < Realm.getDefaultInstance().executeTransaction(realm ->< realm.delete(Movie.class); realm.insert(movies); >); return Observable.just(movies); >) .onErrorResumeNext(throwable -> < Realm realm = Realm.getDefaultInstance(); RealmResultsresults = realm.where(Movie.class).findAll(); return Observable.just(realm.copyFromRealm(results)); >) .doOnSubscribe(loadingView::showLoadingIndicator) .doAfterTerminate(loadingView::hideLoadingIndicator) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::showMovies, throwable -> showError());

Этот код был расположен в Activity, и было очевидно, что он слишком сложный и громоздкий. К тому же в нем сразу выполняется множество операций: работа с сервисом Retrofit, кэширование данных, обработка ошибок и чтение данных из кэша. При этом по-хорошему в слое представления (а Activity является именно частью этого слоя) мы должны написать максимум такой код:

mMoviesSubscription = MoviesUseCase.popularMovies() .doOnSubscribe(loadingView::showLoadingIndicator) .doAfterTerminate(loadingView::hideLoadingIndicator) .subscribe(mView.showMovies, throwable -> mView.showError());

То есть во View мы должны писать только код, связанные с UI-логикой. А остальная часть запроса должна быть перенесена в другие слои, к примеру, в слой данных или слой бизнес-логики. Давайте реализуем такой подход и перенесем получение данных из Retrofit и работу с кэшированием в репозиторий.

Создаем интерфейс для репозитория в слое бизнес-логики (для взаимодействия через интерфейсы) и реализуем его в слое данных:

public class MoviesDataRepository implements MoviesRepository < @Override public Observable> popularMovies() < return ApiFactory.getMoviesService() .popularMovies() .map(MoviesResponse::getMovies) .compose(new MoviesCacheTransformer()) .flatMap(Observable::from) .map(new MoviesMapper()) .toList(); >>

Здесь мы вначале получаем данные через сервис Retrofit, после добавляем обработку кэширования (сохранения и восстановления в случае ошибки) следующим образом:

public class MoviesCacheTransformer implements Observable.Transformer, List> < @Override public Observable> call(Observable> moviesObservable) < return moviesObservable .flatMap(mSaveFunc) .onErrorResumeNext(mCacheErrorHandler); >>

Поля mSaveFunc и mCacheErrorHandler определяются в этом же классе, это обычные функции преобразования, которые для удобства были вынесены в отдельные поля:

private final Func1, Observable>> mSaveFunc = movies -> < Realm.getDefaultInstance().executeTransaction(realm ->< realm.delete(Movie.class); realm.insert(movies); >); return Observable.just(movies); >;

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

В слой данных также перемещается вся работа с сетью, к примеру, сервис Retrofit.

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

public class MoviesUseCase < private final MoviesRepository mRepository; private final Observable.Transformer, List> mAsyncTransformer; public MoviesUseCase(MoviesRepository repository, Observable.Transformer, List> asyncTransformer) < mRepository = repository; mAsyncTransformer = asyncTransformer; >public Observable> popularMovies() < return mRepository.popularMovies() .compose(mAsyncTransformer); >>

Слой бизнес-логики обращается к слою данных за получением данных с помощью объекта Repository.

Если бы в приложении была более сложная бизнес-логика, за нее отвечал бы этот класс. Тестировать его достаточно легко, нам нужно только передать ему такой экземпляр Repository, который нужен для конкретного теста (подмена окружения).

И остается последний слой – слой представления. На этом слое для разделения логики и непосредственно отображения UI используются различные паттерны, к примеру, MVP, MVVM, MVC. В силу особенностей системы Android паттерн MVP является наиболее удобным и часто применяемым.

Мы детально рассмотрим паттерн MVP в рамках следующей лекции, а пока сделаем небольшие пояснения относительно его принципов.

Когда мы видим код, написанный в рамках одного экрана (Activity / Fragment), мы можем легко сказать, какой код отвечает за непосредственное отображение данных пользователю, а какой код управляет логикой этого отображения. В этом и заключается суть паттерна MVP. Основным объектом является Presenter, который управляет View. Например, вот код Presenter-а, который хорошо иллюстрирует разделение функций:

public void init() < mMoviesUseCase.popularMovies() .doOnSubscribe(mMoviesView::showLoadingIndicator) .doAfterTerminate(mMoviesView::hideLoadingIndicator) .compose(mLifecycleHandler.load(R.id.movies_request_id)) .subscribe(mMoviesView::showMovies, throwable ->mMoviesView.showError()); >

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

Presenter View
Обращение к UseCase за получением данных Нет действий
Показ прогресса при начале загрузки через метод showLoadingIndicator Отображение диалога с ProgressBar
Скрытие прогресса после окончания загрузки Скрытие диалога с ProgressBar

В этом и заключаются роли View и Presenter. В случае с Model все еще проще – это всего лишь классы моделей, которые передаются от Presenter к View.

Еще один важный вопрос заключается в обработке жизненного цикла экрана. Это относится к логике экрана, поэтому такой обработкой должен заниматься Presenter. Есть несколько вариантов того, как он может это делать. Первый – определить в Presenter методы жизненного цикла Activity / Fragment и вызывать их. Второй – передать специальный делегат, который будет заниматься обработкой жизненного цикла, как и сделано в примере:

public class MoviesPresenter < private final MoviesView mMoviesView; private final MoviesUseCase mMoviesUseCase; private final LifecycleHandler mLifecycleHandler; public MoviesPresenter(@NonNull MoviesView moviesView, @NonNull MoviesUseCase moviesUseCase, @NonNull LifecycleHandler lifecycleHandler) < mMoviesView = moviesView; mMoviesUseCase = moviesUseCase; mLifecycleHandler = lifecycleHandler; >public void init() < mMoviesUseCase.popularMovies() .doOnSubscribe(mMoviesView::showLoadingIndicator) .doAfterTerminate(mMoviesView::hideLoadingIndicator) .compose(mLifecycleHandler.load(R.id.movies_request_id)) .subscribe(mMoviesView::showMovies, throwable ->mMoviesView.showError()); > >

Экземпляр LifecycleHandler, который передается в Presenter, является интерфейсом и позволяет обрабатывать события жизненного цикла и корректно сохранять состояния запроса.

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

И последний вопрос, который нужно рассмотреть, – это вопрос тестирования. Presenter тестируется обычными тестами на JUnit с помощью Mockito. Все экземпляры View, делегата для обработки жизненного цикла подставляются в него так, как это нужно для тестирования. View, в качестве которого обычно выступает Activity или Fragment, тестируется с точки зрения UI с помощью таких средств как Espresso.

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

Дополнительно – Google Agera

Мы познакомились и продолжаем работать с библиотекой RxJava как с основным инструментом для выполнения серверных запросов и обеспечения асинхронности. Но это не единственный возможный инструмент. Не так давно Google представил свою библиотеку с похожей парадигмой – Agera.

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

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

Библиотека Agera реализует парадигму реактивного программирования, которую мы рассматривали ранее. Она строится на 4 китах (или, точнее, на 4 интерфейсах):

  • Observable – объект, за изменениями которого можно наблюдать и который хранит список объектов, наблюдающих за изменениями этого объекта.
  • Updatable – наблюдатель за изменениями в Observable. Вместе этот и предыдущий объекты образуют стандартный паттерн наблюдатель.
  • Supplier – объект, который предоставляет данные.
  • Receiver – объект, который получает данные.

Такая схема выглядит весьма просто и логично. Посмотрим, как ее можно использовать на практике. Создадим поставщик данных (Supplier), за которым можно наблюдать, который сможет возвращать и получать данные. Это может выглядеть следующим образом:

private class HelloWorldSupplier implements Observable, Supplier, Receiver  < private final ListmUpdatables = new ArrayList<>(); private String mValue; @Override public void addUpdatable(@NonNull Updatable updatable) < mUpdatables.add(updatable); >@Override public void removeUpdatable(@NonNull Updatable updatable) < mUpdatables.remove(updatable); >@NonNull @Override public String get() < return mValue; >@Override public void accept(@NonNull String value) < mValue = value; // Notify the updatables that we have new data for(Updatable updatable : mUpdatables) < updatable.update(); >> >

И использовать этот поставщик данных можно следующим образом:

HelloWordSupplier supplier = new HelloWordSupplier(); Updatable updatable = () -> Log.i("Agera", supplier.get()); supplier.addUpdatable(updatable); supplier.accept("Hello World!");

И это только приложение, которое выводит в лог Hello World! Даже с начальным количеством кода в RxJava было проще разобраться. Что здесь происходит? Во-первых, реализуется обычный паттерн наблюдатель (хранится определенное значение, при изменении которого подписчики уведомляются). Во-вторых, есть методы для получения значения и его изменения. И в-третьих, есть подписчики, которые получают результаты изменений. Итоговая схема такого взаимодействия выглядит следующим образом:

Лекция 4 по архитектуре андроид приложения. Clean Architecture

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

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

MutableRepository repository = Repositories.mutableRepository("Initial values"); Updatable updatable = () -> Log.i("Agera", repository.get()); repository.addUpdatable(updatable); repository.accept("Hello World!");

Этот код почти аналогичен предыдущему, за исключением того, что Repository является потокобезопасным и у него более сложная логика уведомлений об изменениях.

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

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

Рассмотрим пример для получения списка фильмов с сервера с последующим их отображением:

mMoviesRepository = Repositories.repositoryWithInitialValue(Result.>absent()) .observe() .onUpdatesPerLoop() .goTo(AsyncExecutor.EXECUTOR) .getFrom(() -> < try < return ApiFactory.getMoviesService().popularMovies().execute().body(); >catch (IOException e) < return new MoviesResponse(); >>) .thenTransform(input -> Result.absentIfNull(input.getMovies())) .compile();

Здесь создается экземпляр Repository, который будет отвечать за получение списка фильмов. Далее мы подписываемся на него в методе onResume и отписываемся в onPause:

@Override protected void onResume() < super.onResume(); mMoviesRepository.addUpdatable(this); >@Override protected void onPause()

После этого, когда будут получены данные, будет вызван метод Update, в котором мы и обработаем результат и ошибки:

@Override public void update() < mMoviesRepository.get() .ifSucceededSendTo(this::showMovies) .ifFailedSendTo(value ->showError()); >

Кажется, что RxJava в плане удобства работы проще и понятнее, но однозначно сказать, что нужно использовать, конечно же, не получится. Нужно попробовать все варианты и выбрать из них наиболее удобный.

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

Практика

2)Нужно реализовать экран с отдельным фильмом и загрузить информацию о трейлерах и отзывах кинокритиков

3)Реализация должна следовать принципам Clean Architecture

4)Нужно обрабатывать события жизненного цикла

5)Требования можно посмотреть по ссылке http://goo.gl/EpF3N2

Практика 2

2)Нужно реализовать экран с отдельным фильмом и загрузить информацию о трейлерах и отзывах кинокритиков

3)Приложение должно использовать Google Agera в качестве основного инструмента для получения данных

4)Требования можно посмотреть по ссылке http://goo.gl/EpF3N2

Ссылки и полезные ресурсы
  1. Приложения из репозитория:
    1. PopularMoviesClean – пример приложения, написанного по Clean Architecture и практическое задание.
    2. PopularMoviesAgera – пример приложения, основанного на фреймворке Google Agera и практическое задание.

    Clean Architecture с Kotlin

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

    Причина заключается в том, что код тесно связан с модулем данных ответа. Использование Чистой архитектуры (Clean architecture) помогает решить эту проблему. Это лучшее решение для крупных приложений с большим количеством функций и SOLID-принципами. Она была предложена Робертом С. Мартином (известным как Дядя Боб) в блоге “Чистый код” в 2012 году.

    Зачем нужен чистый подход?

    1. Разделение кода на разные слои с назначенными обязанностями облегчает дальнейшую модификацию
    2. Высокий уровень абстракции
    3. Слабая связанность между частями кода
    4. Легкость тестирования кода

    “Чистый код всегда выглядит так, будто написан с заботой.”

    — Майкл Фэзерс

    Какие бывают слои?

    Domain-слой: Запускает независимую от других уровней бизнес-логику. Представляет собой чистый пакет kotlin без android-зависимостей.

    Data-слой: Отправляет необходимые для приложения данные в domain-слой, реализуя предоставляемый доменом интерфейс.

    Presentation-слой: Включает в себя как domain-, так и data-слои, а также является специфическим для android и выполняет UI-логику.

    Что такое Domain-слой?

    Базовый слой, соединяющий presentation-слой с data-слоем, в котором выполняется бизнес-логика приложения.

    UseCases

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

    class GetNewsUseCase(private val transformer: FlowableRxTransformer, 
    private val repositories: NewsRepository): BaseFlowableUseCase(transformer)
    override fun createFlowable(data: Map?): Flowable return repositories.getNews()
    >

    fun getNews(): Flowable val data = HashMap()
    return single(data)
    >
    >

    Прецедент возвращает Flowable, который можно модифицировать в соответствии с требуемым наблюдателем. Есть два параметра. Трансформер или ObservableTransformer, контролирующий выбор потока для выполнения логики, и репозиторий, который представляет собой интерфейс для data-слоя. Для передачи данных в data-слой используется HashMap.

    Репозитории

    Определяют функциональности в соответствии с требованиями прецедента, которые реализуются data-слоем.

    Что такое Data-слой?

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

    API реализуют удаленную сеть. Он может включать любую сетевую библиотеку, такую как retrofit, volley и т. д. Аналогичным образом, DB реализует локальную базу данных.

    class NewsRepositoryImpl(private val remote: NewsRemoteImpl, 
    private val cache: NewsCacheImpl) : NewsRepository
    override fun getLocalNews(): Flowable return cache.getNews()
    >

    override fun getRemoteNews(): Flowable return remote.getNews()
    >

    override fun getNews(): Flowable val updateNewsFlowable = remote.getNews()
    return cache.getNews()
    .mergeWith(updateNewsFlowable.doOnNext remoteNews -> cache.saveArticles(remoteNews)
    >)
    >
    >

    В репозитории реализуются локальные, удаленные и любые другие источники данных. В примере выше класс NewsRepositoryImpl.kt реализует предоставляемый domain-слоем интерфейс. Он выступает в качестве единой точки доступа для data-слоя.

    Что такое presentation-слой?

    Presentation-слой реализует пользовательский интерфейс приложения. Этот слой выполняет только инструкции без какой-либо логики. Он внутренне реализует такие архитектуры, как MVC, MVP, MVVM, MVI и т. д. В этом слое соединяются все части архитектуры.

    Папка DI обеспечивает внедрение всех зависимостей при запуске приложения, таких как сети, View Models, Прецеденты и т.д. DI в android реализуется с помощью dagger, kodein, koin или шаблона service locator. Все зависит от типа приложения. Я выбрал koin, поскольку его легче понять и реализовать, чем dagger.

    Зачем использовать ViewModels?

    В соответствии с документацией android, ViewModel:

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

    class NewsViewModel(private val getNewsUseCase: GetNewsUseCase, 
    private val mapper: Mapper) : BaseViewModel()
    companion object private val TAG = "viewmodel"
    >

    var mNews = MutableLiveData>()

    fun fetchNews() val disposable = getNewsUseCase.getNews()
    .flatMap < mapper.Flowable(it) >
    .subscribe(< response ->
    Log.d(TAG, "On Next Called")
    mNews.value = Data(responseType = Status.SUCCESSFUL, data = response)
    >, < error ->
    Log.d(TAG, "On Error Called")
    mNews.value = Data(responseType = Status.ERROR, error = Error(error.message))
    >, Log.d(TAG, "On Complete Called")
    >)

    addDisposable(disposable)
    >

    fun getNewsLiveData() = mNews
    >

    Таким образом, ViewModel сохраняет данные при изменении конфигурации. Presenter в MVP привязан к представлению с интерфейсом, что усложняет тестирование, в то время как в ViewModel отсутствует интерфейс из-за архитектурно-ориентированных компонентов.

    Базовый View Model использует CompositeDisposable для добавления всех observables и удаляет их на стадии жизненного цикла @OnCleared.

    data class Data(var responseType: Status, var data: RequestData? = null, var error: Error? = null)

    enum class Status

    Класс data wrapper используется в LiveData в качестве вспомогательного класса, который уведомляет представление о состоянии запроса (запуск, результат и т.д.).

    Каким образом соединяются все слои?

    Каждый слой обладает собственной сущностью (entities), специфичной для данного пакета. Mapper преобразовывает сущность одного слоя в другую. Все слои обладают разными сущностями, благодаря чему каждый из них полностью независим, и лишь необходимые данные могут быть переданы последующему слою.

    Application Flow

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *