Здесь вы найдете описание основных принципов и правил, которыми стоит руководствоваться при разработке Android-приложений с использованием чистой архитектуры.
- Введение
- Слои и инверсия зависимостей
- Дополнительные сущности, используемые на практике
- Обработка ошибок
- Тестирование
- Перенос на Clean Architecture существующих проектов
- FAQ по Clean Architecture
Clean Achitecture — принцип построения архитектуры приложения, предложенный Робертом Мартином (который также известен как дядюшка Боб - Uncle Bob) в 2012 году.
Clean Architecture включает в себя два основных принципа:
- Разделение на слои
- Инверсия зависимостей
Давайте расшифруем каждый из них.
Разделение на слои
Суть принципа заключается в разделении всего кода приложения на слои. Всего мы имеем три слоя:
- слой отображения
- слой бизнес логики
- слой работы с данными
Самым главным слоем является слой бизнес логики. Особенность данного слоя заключается в том, что он не зависит ни от каких внешних библиотек или фреймворков. Это достигается за счет инверсии зависимостей.
Инверсия зависимостей
Согласно данному принципу слой бизнес-логики не должен зависеть от внешних. То есть классы из внешних слоев не должны использоваться в классах бизнес-логики. Взаимодействие с внешними слоями происходит через интерфейсы, которые реализуют классы внешних слоев.
Благодаря разделению ответственности между классами мы легко можем изменять код приложения, а также добавлять новый функционал, затрагивая при этом минимальное количество классов. Помимо этого мы получаем легко тестируемый код. Стоит заметить, что построение правильной архитектуры целиком и полностью зависит от самого разработчика и его опыта.
Преимущества чистой архитектуры:
- независимость от UI, БД и фреймворков
- позволяет быстрее добавлять новые функции
- более высокий процент покрытия кода тестами
- повышенная простота навигации по структуре пакетов
Недостатки чистой архитектуры:
- большое количество классов
- довольно высокий порог вхождения и, зачастую, неправильное понимание на первых порах
Внимание: перед прочтением данного документа, настоятельно рекомендую ознакомиться со следующими темами (иначе, вы ничего не поймете):
- Dagger 2
- RxJava 2
- MVP
Очень желательно, чтобы у вас был практический опыт их использования, так вы быстрее войдете в курс дела. Если вы уже знакомы с ними, то можете смело приступать к прочтению. Всё объяснение темы Clean Architecture будет строиться вокруг новостного приложения, которое мы, в теории, хотели бы создать.
P. S. Я разработал приложение, чтобы продемонстрировать использование чистой архитектуры на практике. Исходный код вы можете найти здесь - Bubbble.
Как уже говорилось ранее, архитектуру приложения, построенную по принципу Clean Architecture можно разделить на три слоя:
- слой отображения (presentation)
- слой бизнес-логики (domain)
- слой работы с данными (data)
Выше представлена схема того, как эти слои взаимодействуют. Черными стрелками обозначены зависимости одних слоев от других, а красными - поток данных. Как видите, слои data и presentation зависят от domain, т. е. они используют его классы. Сам же слой domain ничего не знает о внешних слоях и использует только собственные классы и интерфейсы. Далее мы разберем более подробно каждый из этих слоев, и то, как они взаимодействуют между собой.
Как видно из схемы, все три слоя могут обмениваться данными. Следует отметить, что нельзя допускать прямого взаимодействия между слоями presentation и data. Поток данных должен идти от слоя presentation к domain, а от него к слою data (это может быть, например, передача строки с поисковым запросом или регистрационные данные пользователя). То же самое может происходить и в обратном направлении (например, при передаче списка с результатами поиска).
Бизнес-логика - это правила, описывающие, как работает бизнес (например, пользователь не может совершить покупку на сумму больше, чем есть на его счёте). Бизнес-логика не зависит от реализации базы данных или интерфейса пользователя. Бизнес-логика меняется только тогда, когда меняются требования бизнеса, и не зависит от используемой СУБД или интерфейса пользователя.
Роберт Мартин разделяет бизнес-логику на два вида: специфичную для конкретного приложения и общую для всех приложений (в том случае, если вы хотите сделать ваш код общим между приложениями под разные платформы).
Бизнес объект (Entity) - хранят бизнес-логику общую для всех приложений.
Interactor – объект, реализующий бизнес-логику специфичную для конкретного приложения.
Но это все в теории. На практике же используются только Interactor'ы. По крайней мере, мне не встречались приложения, использующие Entity. Кстати, многие путают Entity с DTO (Data Transfer Object). Дело в том, что Entity из Clean Architecture - это не совсем те Entity, которые мы привыкли видеть. Данная статья проливает свет на этот вопрос, а также на многие другие.
Сценарий использования (Use Case) - набор операций для выполнения какой-либо задачи. Пример сценария использования при регистрации пользователя:
- Проверяем данные пользователя
- Отправляем данные на сервер для регистрации
- Сообщаем пользователю об успешной регистрации или ошибке
Исключительные ситуации:
- Пользователь ввел неверные данные (выдаем ошибку)
Давайте теперь посмотрим как это выглядит на практике. Роберт Мартин предлагает создавать для каждого сценария использования отдельный класс, который имеет один метод для его запуска. Пример такого класса:
public class RegisterUserInteractor {
private UserRepository userRepository;
private RegisterDataValidator registerDataValidator;
public TransferInteractor(UserRepository userRepository, RegisterDataValidator registerDataValidator) {
this.userRepository = userRepository;
this.registerDataValidator = registerDataValidator;
}
public Single<RegisterResult> execute(User userData) {
return registerDataValidator.validate(userData)
.flatMap(userData -> userRepository.registerUser(userData));
}
}
Однако практика показывает, что при таком подходе получается огромное количество классов, с малым количеством кода. Более правильным будет создание одного Interactor'а на один экран, методы которого реализуют определенный сценарий, например:
public class ArticleDetailsInteractor {
private ArticlesRepository articlesRepository;
public ArticleDetailsInteractor(ArticlesRepository articlesRepository) {
this.articlesRepository = articlesRepository;
}
public Single<Article> getArticleDetails(long articleId) {
return articlesRepository.getArticleDetails(articleId);
}
public Completable addArticleToFavorite(long articleId, boolean isFavorite) {
return articlesRepository.addArticleToFavorite(articleId, isFavorite);
}
}
Как видите иногда методы Interactor'а могут и вовсе не содержать бизнес-логики, а методы Interactor'а выступают в качестве прослойки между Repository и Presenter'ом.
Если вы заметили, методы Interactor'а возвращают не просто результат, а классы RxJava 2 (в зависимости от типа операции мы используем разные классы - Single, Completable и т. д.). Это дает несколько профитов:
- Не нужно создавать слушатели для получения результата
- Легко переключать потоки
- Легко обрабатывать ошибки
В данном слое содержится всё, что связано с хранением данных и управлением ими. Это может работа с базой данных, SharedPreferences, сетью или файловой системой, а также логика кеширования, если она имеется.
"Мостом" между слоями data и domain является интерфейс Repository (в оригинальной схеме дядюшки Боба он называется Gateway). Сам интерфейс находится в слое domain, а уже реализация располагается в слое data. При этом классы domain-слоя не знают откуда берутся данные - из БД, сети или откуда-то ещё. Именно поэтому вся логика кеширования должна содержаться в data-слое.
Repository - представляет из себя интерфейс, с которым работает Interactor. В нем описывается какие данные хочет получать Interactor от внешних слоев. В приложении может быть несколько репозиториев, в зависимости от задачи. Например, если мы делаем новостное приложение, репозиторий работающий со статьями может называться ArticleRepository, а репозиторий для работы с комментариями CommentRepository. Пример репозитория, работающего со статьями:
public interface ArticleRepository {
Single<Article> getArticle(String articleId);
Single<List<Article>> getLastNews();
Single<List<Article>> getCategoryArticles(String categoryId);
Single<List<Article>> getRelatedPosts(String articleId);
}
Как вы могли заметить, репозиторий возвращает не просто статьи, а класс из RxJava 2 - Single. Благодаря этому мы легко можем сделать операцию извлечения данных синхронной/асинхронной, просто поменяв Scheduler. Помимо класса Single могут быть использованы другие классы RxJava 2, такие как Completable, Maybe, Observable, Flowable. Использование каждого из них обуславливается решаемой задачей. Подробнее о классах RxJava 2 можно почитать здесь - What's different in 2.0;
Слой представления содержит все компоненты, которые связаны с UI, такие как View-элементы, Activity, Fragment'ы, и т. д. Помимо этого здесь содержатся Presenter'ы и View (или ViewModel'и при использовании MVVM). В данном туториале для реализации слоя presentation будет использован шаблон MVP, но вы можете выбрать любой другой (MVVM, MVI).
Для более удобной связки View и Presenter мы будем использовать библиотеку Moxy. Она помогает решить многие проблемы, связанные с жизненным циклом Activity или Fragment'а. Moxy имеет базовые классы, такие как MvpView
и MvpPresenter
от которых должны наследоваться наши View и Presenter. Для избежания написания большого количества кода по связыванию View и Presenter, Moxy использует кодогенерацию. Для правильной работы кодогенерации мы должны использовать специальные аннотации, которые предоставляет нам Moxy. Более подробную информацию о библиотеке можно найти здесь.
MVP расшифровывается как Model-View-Presenter (модель-представление-презентер). Model содержит в себе бизнес-логику и код по работе с данными. Т. к. мы используем связку Clean Architecture + MVP, то Model у нас является код находящийся в слоях Data (работа с данными) и Domain (бизнес-логика). Следовательно в слое Presentation остаются лишь два компонента - View и Presenter.
View отвечает за то, каким образом данные будут показаны пользователю. В случае с Android в качестве View выступает Activity или Fragment. Также View сообщает о действиях пользователя Presenter'у, будь то нажатие на кнопку или ввод текста. Пример View:
public interface ArticlesListView extends MvpView {
void showLoadingProgress(boolean show);
void showVisits(List<Article> articles);
void showArticlesLoadingErrorMessage();
}
Пока мы описали лишь интерфейс View, т. е. какие команды Presenter может отдавать View. Обратите внимание, что наш интерфейс наследуется от интерфейса MvpView, входящего в библиотеку Moxy. Это является обязательным условием для корректной работы библиотеки.
Согласно концепции MVP, View не может напрямую взаимодействовать с Model, поэтому связующим звеном между ними является Presenter. Presenter реагирует на действия пользователя, о которых ему сообщила View (такие как нажатие на кнопку, пункт списка или ввод текста), после чего принимает решения о том, что делать дальше. Например, это может быть запрос данных у модели и отображение их во View. Пример Presenter'а:
@InjectViewState
public class ArticlesListPresenter extends MvpPresenter<ArticlesListView> {
private ArticlesListInteractor articlesListInteractor;
@Inject
public VisitsPresenter(ArticlesListInteractor articlesListInteractor) {
this.articlesListInteractor = articlesListInteractor;
loadArticles();
}
private void loadArticles() {
getViewState().showLoadingProgress(true);
articlesListInteractor.getArticles()
.subscribe(articles -> {
getViewState().showLoadingProgress(false);
getViewState().showArticles(articles);
},
throwable -> getViewState().showLoadingError());
}
public void onArticleSelected(Article article) {
...
}
}
Все необходимые классы для работы Presenter'а (как и всех остальных классов) мы передаем через конструктор. Этот способ так и называется - внедрение через конструктор.
При создании объекта Presenter'а мы должны передать ему запрашиваемые конструктором зависимости. Если их будет много, то создание Presenter'а будет довольно сложным делом. Чтобы не делать этого вручную, мы доверим это дело Component'у.
@Presenter
@Component(dependencies = ApplicationComponent.class)
public interface ArticlesListComponent {
ArticlesListPresenter getPresenter();
}
Он подставит нужные зависимости, а нам нужно будет лишь получить инстанс Presenter'а вызвав метод getPresenter(). Если у вас возник вопрос "А как в таком случае передавать аргументы в Presenter?", то загляните в FAQ - там подробно описан этот вопрос.
Иногда можно встретить такое, что в конструктор передается DI-контейнер (Component), после чего все необходимые зависимости внедряются в поля:
@Inject
ArticlesListInteractor articlesListInteractor;
public VisitsPresenter(ArticlesListPresenterComponent component) {
component.inject(this);
}
Однако, данный способ является неправильным, т. к. усложняет тестирование класса и создает кучу ненужного кода. Если в первом случае мы сразу могли передать mock'и классов через конструктор, то теперь нам нужно создать DI-контейнер и передавать его. Также данный способ делает класс зависимым от конкретного DI-фреймворка, что тоже не есть хорошо.
В контексте разработки под Android роль View на себя берет Activity (или Fragment), поэтому после создания интерфейса View, мы должны реализовать его нашей Activity или Fragment'е:
public class ArticlesListActivity extends MvpAppCompatActivity implements ArticlesListView {
@InjectPresenter
ArticlesListPresenter presenter;
@ProvidePresenter
ArticlesListPresenter provideArticlesListPresenter() {
ArticlesListPresenterComponent component = DaggerArticlesListPresenterComponent.builder()
.applicationComponent(MyApplication.getComponent())
.build();
return component.getPresenter;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_articles_list);
}
public void showArticles(List<Article> articles) {
...
}
public void showLoadingError() {
...
}
}
Хочу заметить, что для правильной работы библиотеки Moxy, наша Activity должна обязательно наследоваться от класса MvpAppCompatActivity (или MvpAppCompatFragment в случае, если вы используете фрагменты). С помощью аннотации @InjectPresenter
мы сообщаем Annotation Processor'у в какую переменную нужно "положить" Presenter.
Так как конструктор нашего Presenter'а не пустой, а принимает на вход определенные параметры, нам нужно предоставить библиотеке объект Presenter'а. Мы делаем это при помощи метода provideArticlesListPresenter
, который мы пометили аннотацией @ProvidePresenter
. Как и во всех других случаях использования кодогенерации, переменные и методы, помеченные аннотациями, должны быть видны на уровне пакета, т. е. у них не должно быть модификаторов видимости (private, public, protected).
Ниже представлен пример разбиения пакетов по фичам новостного приложения приложения:
com.mydomain
|
|----data
| |---- database
| | |---- NewsDao
| |---- filesystem
| | |---- ImageCacheManager
| |---- network
| | |---- NewsApiService
| |---- repositories
| | |---- ArticlesRepositoryImpl
| | |---- CategoriesRepositoryImpl
|
|---- domain
| |---- global
| | |---- models
| | | |---- Article
| | | |---- Category
| | |---- repositories
| | | |---- ArticlesRepository
| | | |---- CategoriesRepository
| |---- articledetails
| | |---- ArticleDetailsInteractor
| |---- articleslist
| | |---- ArticlesListInteractor
|
|---- presentation
| |---- mvp
| | |---- global
| | | |---- routing
| | | | |---- NewsRouter
| | |---- articledetails
| | | |---- ArticleDetailsPresenter
| | | |---- ArticleDetailsView
| | |---- articleslist
| | | |---- ArticlesListPresenter
| | | |---- ArticlesListView
| |---- ui
| | |---- global
| | | |---- views
| | | |---- utils
| | |---- articledetails
| | | |---- ArticleDetailsActivity
| | |---- articleslist
| | | |---- ArticlesListActivity
|
|---- di
| |---- global
| | |---- modules
| | | |---- ApiModule
| | | |---- ApplicationModule
| | |---- scopes
| | |---- modifiers
| | |---- ApplicationComponent
| |---- articledetails
| | |---- ArticleDetailsComponent
| | |---- ArticleDetailsModule
| |---- articleslist
| | |---- ArticleListComponent
Прежде чем делить код по фичам, мы разделили его на слои. Данный подход позволяет сразу определить к какому слою относится тот или иной класс. Если вы заметили, классы слоя data разбиты немного не так, как в слоях domain, presentation и di. Здесь вместо фич приложения мы выделили типы источников данных - сеть, база данных, файловая система. Это связано с тем, что все фичи используют практически одни и те же классы (например, NewsApiService) и их не имеет смысла разбивать по фичам.
В пакетах с именем global хранятся общие классы, которые используются в нескольких фичах. Например, в пакете data/global хрянятся модели и интерфейсы репозиториев.
Слой presentation разбит на два пакета - mvp и ui. В mvp хранятся, как понятно из названия, классы Presenter'ов и View. В ui хранятся реализация слоя View из MVP, т. е. Activity, Fragment'ы и т. д.
Разбиение классов по фичам имеет ряд преимуществ:
- Очевидность: Даже не знакомый с проектом разработчик, при первом взгляде на структуру пакетов сможет примерно понять что делает приложение, не заглядывая в сам код.
- Добавление нового функционала. Если вы решили добавить новую функцию в приложение, например, просмотр профиля пользователя, то вам лишь нужно добавить пакет userprofle и работать только с ним, а не "гулять" по всей структуре пакетов, создавая нужные классы.
- Удобство редактирования. При редактировании какой либо фичи, нужно держать открытыми максимум два-три пакета и вы видите только те классы, которые относятся к конкретной фиче. При разбиении по типу класса, раскрытой приходится держать практически всё дерево пакетов и вы видите классы, которые вам сейчас не нужны, относящиеся к другим фичам.
- Удобство масштабирования. При увеличении количества функций приложения, увеличивается и количество классов. При разбиении классов по типу, добавление новых классов делает навигацию по ним очень не удобным, т.к. приходится искать нужный класс, среди десятков других, что сказывается на скорости и удосбстве разработки. Разбиение по фичам решает эту проблему, т.к. вы можете объединить связанные между собой пакеты с фичами (напрмер, можно объединить пакеты login и registration в пакет authentication).
Также хочется сказать пару слов об именовании пакетов: в каком числе их нужно называть - множественном или единственном? Я придерживаюсь подхода, описанного здесь:
- Если пакет содержит однородные классы, то имя пакета ставится во множественном числе. Например, пакет с классами Dog, Cat и Cow будет называться animals. Другой пример - различные реализации какого-либо интерфейса (XmlResponseAdapter, JsonResponseAdapter).
- Если пакет содержит разнородные классы, реализующую определенную функцию, то имя пакета ставится в единственном числе. Пример - пакет order, содержащий классы OrderInfo, OrderInteractor, OrderValidation и т. д.
Для связки всех трех слоев используется Dependency Injection. В нашем примере мы используем библиотеку Dagger 2.
Т. к. Presenter содержит в себе логику реагирования на действия пользователя, то он также знает о том, на какой экран нужно перейти. Однако сам Presenter не может осуществлять переход на новый экран, т. к. для этого нам требуется Context. Поэтому за открытие нового экрана должна отвечать View. Для осуществления перехода на следующий экран мы должны вызвать метод View, например, openProfileScreen(), а уже в реализации самого метода осуществлять переход. Помимо данного подхода некоторые разработчики используют для навигации так называемый Router.
Router - класс, для осуществления переходов между экранами (активити или фрагментами).
Для реализации Router'а вы можете использовать библиотеку Alligator.
Mapper - специальный класс, для конвертирования моделей из одного типа в другой, например, из модели БД в модель бизнес-логики. Обычно они имеют название типа XxxMapper, и имеют единственный метод с названием map (иногда встречаются названия convert/transform), например:
public class ArticleDbModelMapper {
public Article map(ArticleDbModel model) {
return new Article(model.getName(), model.getLastname, model.getAge());
}
public List<Article> map(Collection<ArticleDbModel> models) {
final List<Article> result = new ArrayList<>(models.size());
for (ArticleDbModel model : models) {
result.add(map(model));
}
return result;
}
}
Т. к. слой domain ничего не знает о классах других слоев, то маппинг моделей должен выполняться во внешних слоях, т. е. репозиторием (при конвертации data > domain или domain > data) или презентером (при конвертации domain > presentation и наоборот) .
В некоторых случаях может потребоваться получить строку или число из ресурсов приложения в Presenter'е или слое domain . Однако, мы знаем, что они не должны напрямую взаимодействовать с фреймворком Android. Чтобы решить эту проблему мы можем создать специальную сущность ResourceManager, для доступа у внешним ресурсам. Для этого мы создаем интерфейс:
public interface ResourceManager {
String getString(int resourceId);
int getInteger(int resourceId);
}
Сам интерфейс должен располагаться в слое domain. После этого в слое presentation мы создаем реализацию нашего интерфейса:
public class AndroidResourceManager implements ResourceManager {
private Context context;
@Inject
public VisitsPresenter(Context context) {
this.context = context;
}
@Override
public String getString(int resourceId) {
return context.getResources().getString(resourceId);
}
@Override
public int getInteger(int resourceId) {
return context.getResources().getInteger(resourceId);
}
}
Далее мы должны связать интерфейс и реализацию нашего ResourceManager'а в ApplicationModule:
@Singleton
@Provides
protected ResourceManager provideResourceManager(AndroidResourceManager resourceManager) {
return resourceManager
}
Теперь мы можем использовать ResourceManager в Presenter'е или Interactor'ах:
@InjectViewState
public class ArticlesListPresenter extends MvpPresenter<ArticlesListView> {
...
private ResourceManager resourceManager;
@Inject
public ShotsPresenter(..., AndroidResourceManager resourceManager) {
...
this.resourceManager = resourceManager;
}
private void onLoadError(Throwable throwable) {
...
getViewState().showMessage(resourceManager.getString(R.string.articles_load_error));
}
}
Наверное, у внимательных читателей возник вопрос: почему мы используем класс R в Presenter'е? Ведь он также относится к Android? На самом деле, это не совсем так. Класс R вообще не использует никакие классы, и представляет из себя набор идентификаторов ресурсов. Поэтому, нет ничего плохого, чтобы использовать его в Presenter'е.
[раздел на доработке]
Одним из самых главных преимуществ является то, что мы можем покрыть тестами намного больший функционал приложения, за счет разбиения кода на мелкие классы, каждый из которых выполняет строго определенную задачу. Благодаря принципу инверсии зависимостей, используемому в чистой архитектуре мы можем с легкостью подменять реализацию тех или иных классов на фейковые, которые реализуют нужное нам поведение.
Прежде чем начать писать тесты, мы должны ответить себе на два вопроса:
- что мы хотим тестировать?
- как мы будем это тестировать?
Что мы хотим тестировать:
- Мы хотим проверить нашу бизнес-логику независимо от какого-либо фреймворка или библиотеки.
- Мы хотим протестировать нашу интеграцию с API.
- Мы хотим протестировать нашу интеграцию с нашей системой персистентности.
- Мы хотим протестировать некоторые общие компоненты пользовательского интерфейса.
Что мы НЕ должны тестировать:
- Сторонние библиотеки (мы предполагаем, что они работают правильно, потому что уже протестированы разработчиками)
- Тривиальный код (например, геттеры и сеттеры)
Теперь, давайте разберём то, как мы будем тестировать каждый из слоев.
Перед началом тестирования нам нужно сделать все операции синхронными.
[создание TestSchedulersProvider]
Данный слой включает в себя 2 типа тестов: Unit-тесты и UI-тесты.
- Unit-тесты используются для тестирования Presenter'ов.
- UI-тесты используются для тестирования Activity (проверяется корректность отображения элементов и т. д.).
Существуют различные соглашения по именованию тестовых методов. Например, в этой статье описаны некоторые из них. В примерах, которые я буду приводить далее, я не буду придерживаться какого-либо соглашения. В общем, нет большой разницы, как их называть. Самое главное понять из названия, что тестирует наш метод и что мы хотим получить в результате.
Давайте рассмотрим пример теста для ArticlesListPresenter:
public class ArticlesListPresenterTest {
@Test
public void shouldLoadArticlesOnViewAttached() {
//preparing
ArticlesListInteractor interactor = Mockito.mock(ArticlesListInteractor.class);
TestSchedulersProvider schedulers = new TestSchedulersProvider();
ArticlesListPresenter presenter = new ArticlesListPresenter(interactor, schedulers);
ArticlesListView view = Mockito.mock(ArticlesListView.class);
ArrayList<Article> articlesList = ArrayList<Article>;
when(interactor.getArticlesList()).thenReturn(Single.just(articlesList));
//testing
presenter.attachView(view)
//asserting
verify(view, times(1)).showLoadingProgress(true);
verify(view, times(1)).showLoadingProgress(false);
verify(view, times(1)).showArticles(articlesList);
}
}
Как видите, мы разделили код теста на три части:
- Подготовка к тестированию. Здесь мы инициализируем объекты для тестирования, подготавливаем тестовые данные, а также предопределяем поведение моков.
- Само тестирование.
- Проверка результатов тестирования. Здесь мы проверяем, что у View были вызваны нужные методы и переданы аргументы.
В данном слое тестируюится классы Interactor'ов и Entity. Необходимо проверить, действительно ли бизнес-логика реализует требуемое поведение .
[раздел на доработке]
[раздел на доработке]
Если вы начиначете разработку нового мобильного приложения, то лучше начать с создания пользовательского интерфейса, т. к. именно UI определяет
[раздел на доработке]
Наверное, нет однозначного ответа на этот вопрос. Если проект большой и переход на Clean Architecture может длительный промежуток времени, то лучше переписывать код постепенно, используя подход, который мы описали выше. Если же проект простой и состоит из 2-3 экранов, а сроки не поджимают, то вы можете попробовать переписать проект с нуля.
В конце хочется привести поучительную историю про Netscape, который переписывали с нуля больше, чем три года - Things You Should Never Do, Part I
Согласно принципам Clean Architecture, слой Domain ничего не должен знать о внешних слоях (Data и Presentation), но внешние слои без проблем могут использовать классы из слоя Domain. Следовательно, можно не создавать отдельные сущности для каждого из слоев, а использовать только те, что лежат в слое Domain. Однако, если их формат не совпадает с тем, что используется во внешних слоях, то нужно создать отдельную сущность. Также не следует использовать в моделях слоя Domain аннотации, которые требуются библиотекам, типа Gson или Room. В этом случае нужно создать отдельную сущность в слое Data.
[раздел на доработке]
Пример Presenter'а с интерфейсом:
public interface LoginPresenter {
void onLoginButtonPressed(String email, String password);
}
public class LoginPresenterImpl implements LoginPresenter {
...
}
Нет, интерфейсы для презентера и интерактора создавать не нужно. Это создает дополнительные сложности при разработке, при этом пользы от данного подхода практически нет. Вот лишь некоторые проблемы, которые порождает создаение лишних интерфейсов:
- Если мы хотим добавить новый метод или изменить существующий, нам нужно изменить интерфейс. Помимо это мы также должны изменить реализацию метода. Это занимает довольно времени, даже при использовании такой продвинутой IDE как Android Studio.
- Использование дополнительных интерфейсов усложняет навигацию по коду. Если вы хотите перейти к реализации метода Presenter'а из Activity (т. е. реализации View), то вы переходите к интерфейсу Presenter'а.
- Интерфейс никак не улучшает тестируемость кода. Вы с легкостью можете заменить класс Presenter'а на mock, используя любую библиотеку для mock'ирования.
Более подробно можете почитать об этом в следующих статьях:
- Interfaces for presenters in MVP are a waste of time!
- Франкен-код или франкен-дизайн
- Управление зависимостями
Часто при создании презентера возникает необходимость передать дополнительные аргументы. Например, мы хотим передать идентфикатор статьи, чтобы получить её содержимое от сервера. Чтобы сделать это, нам необходимо создать отдельный модуль для Presenter'а и передать аргументы туда:
@Module
public class ArticleDetailsModule {
private final long articleId;
public ArticleDetailsModule(long articleId) {
this.articleId = articleId;
}
@Provides
@Presenter
long provideArticleId() {
return articleId;
}
}
Далее нам нужно добавить наш модуль в Component:
@Component(dependencies = ApplicationComponent.class, modules = ArticleDetailsModule.class)
public interface ArticleDetailsComponent {
При создании Component'а мы должны передать наш модуль с идентификатором:
long articleId = ...
ArticleDetailsComponent component = DaggerArticleDetailsComponent.builder()
.applicationComponent(MyApplication.getComponent())
.articleDetailsModule(new ArticleDetailsModule(articleId))
.build();
Теперь мы можем получить наш идентификатор через конструктор:
@Inject
public UserFollowersPresenter(ArticleDetailsInteractor interactor, long articleId) {
this.interactor = interactor;
this.articleId = articleId;
}
Теперь представим, что помимо идентфикатора статьи, мы хотим передать ID пользователя, который так же имеет тип long. Если мы попытаемся создать ещё один provide-метод в нашем модуле, Dagger выдаст ошибку, о том, что типы совпадают и он не знает какой из них является идентфикатором статьи, а какой идентфикатором пользователя.
Чтобы исправить это, нам необходимо создать Qualifier-аннотации, которые будут указывать Dagger'у "who is who":
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface ArticleId {
}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface UserId {
}
Добавляем аннотации к нашим provide-методам:
@ArticleId
@Provides
@Presenter
long provideArticleId() {
return articleId;
}
@UserId
@Provides
@Presenter
long provideUserId() {
return userId;
}
Также нужно пометить аннотациями аргументы конструктора:
@Inject
public UserFollowersPresenter(ArticleDetailsInteractor interactor, @ArticleId long articleId, @UserId long userId)
Готово. Теперь Dagger сможет верно расставить аргументы в конструктор.