├── .langmark ├── .gitattributes ├── psd ├── DataLayer.psd ├── Domain Layer.psd ├── PresentationLayer.psd ├── CleanArchitectureLayers.psd ├── CleanArchitectureScheme.psd └── CleanArchitectureManifest.psd ├── images ├── DataLayer.png ├── DomainLayer.png ├── PresentationLayer.png ├── CleanArchitectureLayers.png └── CleanArchitectureManifest.png ├── contributing.md ├── README.md ├── en ├── testing.md ├── additional_entities.md ├── faq.md └── layers_and_ioc.md ├── ru ├── testing.md ├── additional_entities.md ├── faq.md └── layers_and_ioc.md └── README-RU.md /.langmark: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | .langmark linguist-language=Kotlin 2 | -------------------------------------------------------------------------------- /psd/DataLayer.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/HEAD/psd/DataLayer.psd -------------------------------------------------------------------------------- /images/DataLayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/HEAD/images/DataLayer.png -------------------------------------------------------------------------------- /psd/Domain Layer.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/HEAD/psd/Domain Layer.psd -------------------------------------------------------------------------------- /images/DomainLayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/HEAD/images/DomainLayer.png -------------------------------------------------------------------------------- /psd/PresentationLayer.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/HEAD/psd/PresentationLayer.psd -------------------------------------------------------------------------------- /images/PresentationLayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/HEAD/images/PresentationLayer.png -------------------------------------------------------------------------------- /psd/CleanArchitectureLayers.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/HEAD/psd/CleanArchitectureLayers.psd -------------------------------------------------------------------------------- /psd/CleanArchitectureScheme.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/HEAD/psd/CleanArchitectureScheme.psd -------------------------------------------------------------------------------- /images/CleanArchitectureLayers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/HEAD/images/CleanArchitectureLayers.png -------------------------------------------------------------------------------- /psd/CleanArchitectureManifest.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/HEAD/psd/CleanArchitectureManifest.psd -------------------------------------------------------------------------------- /images/CleanArchitectureManifest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/HEAD/images/CleanArchitectureManifest.png -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | ## Contribution Guidelines 2 | 3 | 1. Fork the repo. 4 | 2. Add new file with name "README-[CODE].md", where **[CODE]** is [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) code of your language. 5 | 3. Add translated text to the file. 6 | 4. Create Pull-Request with your changes. 7 | 8 | **Notes for translators:** 9 | 10 | * Make your changes in the new branch. 11 | * Keep the same content & formatting - the focus is on translation, should anyone want to modify the content or the graphics - let's PR first in English. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![CleanArchitectureManifest](https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/master/images/CleanArchitectureManifest.png) 2 | 3 | # Clean Architecture Manifest (v. 0.9.5) 4 | 5 | Here you will find description of the main principles and rules, that are worth following in developing Android apps using Clean Architecture approach. 6 | 7 | **Translations:** 8 | 9 | [English](/README.md) | [Русский](/README-RU.md) 10 | 11 | If you want to translate this document to your language, please visit [this page](contributing.md). 12 | 13 | ## Table of contents 14 | 15 | - [Introduction](en/introduction.md) 16 | - [Layers and Inversion of Control](en/layers_and_ioc.md) 17 | - [Additional entities used in practice](en/additional_entities.md) 18 | - Errors handling 19 | - [Testing](en/testing.md) 20 | - Start a new application development using Clean Architecture 21 | - Migration project to Clean Architecture 22 | - [Clean Architecture FAQ](en/faq.md) 23 | 24 | ## Introduction 25 | 26 | Clean Architecture is an approach suggested by Robert Martin in 2012. 27 | 28 | Clean Architecture includes two main principles: 29 | 30 | 1. Separation into layers 31 | 2. Inversion of Control 32 | 33 | Let's take a closer look at them. 34 | 35 | **1. Separating into layers** 36 | 37 | The main idea of this principle is separating whole app into layers. In general we have three layers: 38 | 39 | 1. Presentation layer 40 | 2. Domain layer (business logic) 41 | 3. Data layer 42 | 43 | **2.Inversion of Control** 44 | 45 | According to this principle, domain layer must not depends on outer ones. That is, classes from outer layers must not be used in the domain layer. Interaction with outer layers is implemented through interfaces. Declaration of the interfaces contains in domain layer and their implementation contains in outer ones. 46 | 47 | Thanks to separation of concerns between classes we can easily change an application code and add new functional with modifying minimal number of classes. In addition we get testable code. Please note that building the right app architecture depends entirely on a developer experience. 48 | 49 | Advantages of Clean Architecture: 50 | 51 | - independent of UI, DB or frameworks 52 | - allows you to add new features faster 53 | - a higher percentage of test coverage 54 | - easy packages structure navigation 55 | 56 | Disadvantages: 57 | 58 | - large number of classes 59 | - hard for beginners to understand 60 | 61 | **Attention:** this article assumes knowledge on the following topics: 62 | 63 | - Dagger 2 64 | - RxJava 2 65 | 66 | **P. S.** I developed an application to demonstrate the use of Clean Architecture in practice. You can find the source code here - [Bubbble](https://github.com/ImangazalievM/Bubbble). 67 | 68 | ### Find this article useful? ❤️ 69 | 70 | - Support it by clicking the ⭐️ button on the upper right of this page. ✌️ 71 | -------------------------------------------------------------------------------- /en/testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | One key advantage of Clean Architecture is that we can cover with tests much more functionality of the application through the code into small classes, each of which performs a strictly defined task. Thanks to the Inversion of Control principle, that used in Clean Architecture, we can easily replace the implementation classes with fake ones that implement the behavior we want. 4 | 5 | Before we start writing tests, we must answer ourselves to two questions: 6 | 7 | - What do we want to test? 8 | - How will we test this? 9 | 10 | What we want to test: 11 | 12 | - We want to test our business logic regardless of any framework or library. 13 | - We want to test our integration with the API. 14 | - We want to test integration with our persistence system. 15 | - Everything that contains conditions. 16 | 17 | What we should NOT test: 18 | 19 | - Third-party libraries (we assume that they work correctly because they have already been tested by the developers) 20 | - Trivial code (for example, getters and setters) 21 | 22 | Let's take a closer look at how we will test each of layers. 23 | 24 | ## Presentation layer testing 25 | 26 | This layer includes 2 types of tests: Unit-tests and UI-tests. 27 | 28 | - Unit-tests are used for testing Presenters. 29 | - UI-tests are used for testing Activities (to check if UI works correctly) 30 | 31 | There are different naming conventions for unit tests. For example, this](https://dzone.com/articles/7-popular-unit-test-naming) article describes some of them. In my examples of tests, I will not adhere to an agreement. The most important thing is to understand what the tests are doing and what we want to get as a result. 32 | 33 | Let us take the example of a test for **ArticlesListPresenter**: 34 | 35 | ```kotlin 36 | class ArticlesListPresenterTest { 37 | 38 | @Test 39 | fun shouldLoadArticlesOnViewAttached() { 40 | //preparing 41 | val interactor = Mockito.mock(ArticlesListInteractor::class.java) 42 | val schedulers = TestSchedulersProvider() 43 | val presenter = ArticlesListPresenter(interactor, schedulers) 44 | val view = Mockito.mock(ArticlesListView::class.java) 45 | 46 | val articlesList = ArrayList
47 | `when`(interactor.getArticlesList()).thenReturn(Single.just(articlesList)) 48 | 49 | //testing 50 | presenter.attachView(view) 51 | 52 | //asserting 53 | verify(view, times(1)).showLoadingProgress(true) 54 | verify(view, times(1)).showLoadingProgress(false) 55 | verify(view, times(1)).showArticles(articlesList) 56 | } 57 | 58 | } 59 | ``` 60 | 61 | As you can see, we divided the test code into three parts: 62 | 63 | - Preparation for testing. Here we initialize the objects for testing, prepare the test data, and also define the behavior of the mocks. 64 | - Testing itself 65 | - Checking the test results. Here we check the View methods have been called with necessary arguments. 66 | 67 | ## Domain layer testing 68 | 69 | [this chapter is in preparation] 70 | 71 | ## Data layer testing 72 | 73 | [this chapter is in preparation] -------------------------------------------------------------------------------- /ru/testing.md: -------------------------------------------------------------------------------- 1 | # Тестирование 2 | 3 | Одним из самых главных преимуществ чистой архитектуры является то, что мы можем покрыть тестами намного больший функционал приложения за счет разбиения кода на мелкие классы, каждый из которых выполняет строго определенную задачу. Благодаря принципу инверсии зависимостей, используемому в чистой архитектуре мы можем с легкостью подменять реализацию тех или иных классов на фейковые, которые реализуют нужное нам поведение. 4 | 5 | Прежде чем начать писать тесты, мы должны ответить себе на два вопроса: 6 | 7 | - что мы хотим тестировать? 8 | - как мы будем это тестировать? 9 | 10 | Что мы хотим тестировать: 11 | 12 | - Мы хотим проверить нашу бизнес-логику независимо от какого-либо фреймворка или библиотеки. 13 | - Мы хотим протестировать нашу интеграцию с API. 14 | - Мы хотим протестировать нашу интеграцию с нашей системой персистентности. 15 | - Все, что содержит условия. 16 | 17 | Что мы НЕ должны тестировать: 18 | 19 | - Сторонние библиотеки (мы предполагаем, что они работают правильно, потому что уже протестированы разработчиками) 20 | - Тривиальный код (например, геттеры и сеттеры) 21 | 22 | Теперь, давайте разберём то, как мы будем тестировать каждый из слоев. 23 | 24 | ## Тестирование слоя представления 25 | 26 | Данный слой включает в себя 2 типа тестов: Unit-тесты и UI-тесты. 27 | 28 | - Unit-тесты используются для тестирования Presenter'ов. 29 | - UI-тесты используются для тестирования Activity (проверяется корректность отображения элементов и т. д.). 30 | 31 | Существуют различные соглашения по именованию тестовых методов. Например, в этой [статье](https://dzone.com/articles/7-popular-unit-test-naming) описаны некоторые из них. В примерах, которые я буду приводить далее, я не буду придерживаться какого-либо соглашения. Самое главное понять из названия, что тестирует наш метод и что мы хотим получить в результате. 32 | 33 | Давайте рассмотрим пример теста для **ArticlesListPresenter**: 34 | 35 | ```kotlin 36 | class ArticlesListPresenterTest { 37 | 38 | @Test 39 | fun shouldLoadArticlesOnViewAttached() { 40 | //preparing 41 | val interactor = Mockito.mock(ArticlesListInteractor::class.java) 42 | val schedulers = TestSchedulersProvider() 43 | val presenter = ArticlesListPresenter(interactor, schedulers) 44 | val view = Mockito.mock(ArticlesListView::class.java) 45 | 46 | val articlesList = ArrayList
47 | `when`(interactor.getArticlesList()).thenReturn(Single.just(articlesList)) 48 | 49 | //testing 50 | presenter.attachView(view) 51 | 52 | //asserting 53 | verify(view, times(1)).showLoadingProgress(true) 54 | verify(view, times(1)).showLoadingProgress(false) 55 | verify(view, times(1)).showArticles(articlesList) 56 | } 57 | 58 | } 59 | ``` 60 | 61 | Как видите, мы разделили код теста на три части: 62 | 63 | - Подготовка к тестированию. Здесь мы инициализируем объекты для тестирования, подготавливаем тестовые данные, а также предопределяем поведение моков. 64 | - Само тестирование. 65 | - Проверка результатов тестирования. Здесь мы проверяем, что у View были вызваны нужные методы и переданы аргументы. 66 | 67 | ## Тестирование бизнес-логики 68 | 69 | В данном слое тестируюится классы Interactor'ов и Entity. Необходимо проверить, действительно ли бизнес-логика реализует требуемое поведение . 70 | 71 | [раздел на доработке] 72 | 73 | ## Тестирование слоя работы с данными 74 | 75 | [раздел на доработке] -------------------------------------------------------------------------------- /README-RU.md: -------------------------------------------------------------------------------- 1 | ![CleanArchitectureManifest](https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/master/images/CleanArchitectureManifest.png) 2 | 3 | # Clean Architecture Manifest (v. 0.9.5) 4 | 5 | Здесь вы найдете описание основных принципов и правил, которыми стоит руководствоваться при разработке Android-приложений с использованием чистой архитектуры. 6 | 7 | **Translations:** 8 | 9 | [English](/README.md) | [Русский](/README-RU.md) 10 | 11 | Если вы хотите перевести этот документ на свой язык, пожалуйста посетите [эту страницу](contributing.md). 12 | 13 | ## Содержание 14 | 15 | - [Введение](ru/introduction.md) 16 | - [Слои и инверсия зависимостей](ru/layers_and_ioc.md) 17 | - [Дополнительные сущности, используемые на практике](ru/additional_entities.md) 18 | - [Тестирование](en/testing.md) 19 | - Перенос на Clean Architecture существующих проектов 20 | - [FAQ по Clean Architecture](ru/faq.md) 21 | 22 | ## Введение 23 | 24 | Clean Achitecture — способ построения архитектуры приложения, [предложенный](https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html) Робертом Мартином (который также известен как дядюшка Боб - Uncle Bob) в 2012 году. 25 | 26 | Clean Architecture включает в себя два основных принципа: 27 | 28 | 1. **Разделение на слои** 29 | 2. **Инверсия зависимостей** 30 | 31 | Давайте расшифруем каждый из них. 32 | 33 | **Разделение на слои** 34 | 35 | Суть принципа заключается в разделении всего кода приложения на слои. Всего мы имеем три слоя: 36 | 37 | - слой отображения 38 | - слой бизнес логики 39 | - слой работы с данными 40 | 41 | Самым главным слоем является слой бизнес логики. Особенность данного слоя заключается в том, что он не зависит ни от каких внешних библиотек или фреймворков. Это достигается за счет *инверсии зависимостей*. 42 | 43 | **Инверсия зависимостей** 44 | 45 | Согласно данному принципу слой бизнес-логики не должен зависеть от внешних. То есть классы из внешних слоев не должны использоваться в классах бизнес-логики. Взаимодействие с внешними слоями происходит через интерфейсы, которые реализуют классы внешних слоев. 46 | 47 | Благодаря разделению ответственности между классами мы легко можем изменять код приложения, а также добавлять новую функциональность, затрагивая при этом минимальное количество классов. Помимо этого мы получаем легко тестируемый код. Стоит заметить, что построение правильной архитектуры целиком и полностью зависит от самого разработчика и его опыта. 48 | 49 | Преимущества чистой архитектуры: 50 | 51 | - независимость от UI, БД и фреймворков 52 | - позволяет быстрее добавлять новые функции 53 | - более высокий процент покрытия кода тестами 54 | - повышенная простота навигации по структуре пакетов 55 | 56 | Недостатки чистой архитектуры: 57 | 58 | - большое количество классов 59 | - довольно высокий порог вхождения и, зачастую, неправильное понимание на первых порах 60 | 61 | **Внимание:** перед прочтением данного документа, настоятельно рекомендую ознакомиться со следующими темами (иначе, вы ничего не поймете): 62 | 63 | - Dagger 2 64 | - RxJava 2 65 | 66 | Очень желательно, чтобы у вас был практический опыт их использования, так вы быстрее войдете в курс дела. Если вы уже знакомы с ними, то можете смело приступать к прочтению. Всё объяснение темы Clean Architecture будет строиться вокруг новостного приложения, которое мы, в теории, хотели бы создать. 67 | 68 | **P. S.** Я разработал приложение, чтобы продемонстрировать использование чистой архитектуры на практике. Исходный код вы можете найти здесь - [Bubbble](https://github.com/ImangazalievM/Bubbble). 69 | 70 | ### Эта статья была полезна для вас? ❤️ 71 | 72 | - Поддержите её нажав на кнопку ⭐️ в верху этой страницы. ✌️ 73 | -------------------------------------------------------------------------------- /en/additional_entities.md: -------------------------------------------------------------------------------- 1 | # Additional entities used in practice 2 | 3 | - [Router](#router) 4 | - [Mapper](#mapper) 5 | - [ResourceManager](#resourcemanager) 6 | - [SchedulersProvider](#schedulersprovider) 7 | 8 | ### Router 9 | 10 | Since Presenter decides what happens when you interact with the view, it also know what screen will be opened. But we can't open Activity from Presenter directly. There are two ways of solving this problem - call View method like **openProfileScreen()** and open new screen from Activity or use Router 11 | 12 | **Router** is a special class for navigating between screens (Activities or Fragments). 13 | 14 | We recommend to use [Alligator](https://github.com/aartikov/Alligator) library for implementing navigation. 15 | 16 | ### Mapper 17 | 18 | **Mapper** is a special class for converting models between layers, for example, from DB model to Domain model. Usually, they called like XxxMapper and have a single method with name map (or convert/transform), for example: 19 | 20 | ```kotlin 21 | class ArticleDbModelMapper { 22 | 23 | fun map(model: ArticleDbModel) = 24 | Article( 25 | name = model.name, 26 | lastname = model.lastname, 27 | age = model.age 28 | ) 29 | 30 | fun map(models: Collection) = models.map { map(it) } 31 | 32 | } 33 | ``` 34 | 35 | Since domain layer doesn't know anything about classes from other layers, the mapping of models must be performed in the outer layers, i. e. in Repository (when mapping **data > domain** or **domain> data**) or in Presenter (when mapping **domain> presentation** and vice versa) . 36 | 37 | ### ResourceManager 38 | 39 | In some cases, we need to get a string or a number from resources and use it in Presenter or domain layer. But we know that we can't use Context class. To resolve this problem we must use a special class that called ResourceManager. Let's create an interface for this one: 40 | 41 | ```kotlin 42 | interface ResourceManager { 43 | 44 | fun getString(resourceId: Int): String 45 | 46 | fun getInteger(resourceId: Int): Int 47 | 48 | } 49 | ``` 50 | 51 | This interface must be contained in the domain layer. Then we create the interface implementation in presentation layer: 52 | 53 | ```kotlin 54 | class AndroidResourceManager @Inject constructor( 55 | private val context: Context 56 | ) : ResourceManager { 57 | 58 | override fun getString(resourceId: Int): String { 59 | return context.resources.getString(resourceId) 60 | } 61 | 62 | override fun getInteger(resourceId: Int): Int { 63 | return context.resources.getInteger(resourceId) 64 | } 65 | 66 | } 67 | ``` 68 | 69 | After that we must bind the interface with its implementation in ApplicationModule: 70 | 71 | ```kotlin 72 | @Singleton 73 | @Provides 74 | fun provideResourceManager(resourceManager: AndroidResourceManager) : ResourceManager = resourceManager 75 | ``` 76 | 77 | Now we can use ResourceManager in our Presenters or Interactors: 78 | 79 | ```kotlin 80 | @InjectViewState 81 | class ArticlesListPresenter @Inject constructor( 82 | private val resourceManager: AndroidResourceManager 83 | ) : MvpPresenter() { 84 | 85 | private fun onLoadError(throwable: Throwable) { 86 | ... 87 | 88 | viewState.showMessage(resourceManager.getString(R.string.articles_load_error)) 89 | } 90 | 91 | } 92 | ``` 93 | 94 | Perhaps you have a question: "Why we can use **R** class in the Presenter?" Because it uses Android Framework. Actually, that's not entirely true. **R** class does not use any class at all. So there is nothing bad with using **R** class in Presenter. 95 | 96 | #### SchedulersProvider 97 | 98 | To test our code we need make all operations synchronous. For that, we must replace all Schedulers to **TestScheduler**. For this reason, we set Schedulers through **SchedulersProvider**, but not directly. 99 | 100 | ```kotlin 101 | open class SchedulersProvider @Inject constructor() { 102 | 103 | open fun ui(): Scheduler = AndroidSchedulers.mainThread() 104 | open fun computation(): Scheduler = Schedulers.computation() 105 | open fun io(): Scheduler = Schedulers.io() 106 | open fun newThread(): Scheduler = Schedulers.newThread() 107 | open fun trampoline(): Scheduler = Schedulers.trampoline() 108 | 109 | } 110 | ``` 111 | 112 | It allows easily replace Schedules by extending **SchedulersProvider** and overriding it's methods: 113 | 114 | ```kotlin 115 | class TestSchedulerProvider : SchedulersProvider() { 116 | 117 | val testScheduler = TestScheduler() 118 | 119 | override fun ui(): Scheduler = testScheduler 120 | override fun computation(): Scheduler = testScheduler 121 | override fun io(): Scheduler = testScheduler 122 | override fun newThread(): Scheduler = testScheduler 123 | override fun trampoline(): Scheduler = testScheduler 124 | 125 | } 126 | ``` 127 | 128 | Then, when testing we just need use TestSchedulersProvider instead of SchedulersProvider. More info about testing code with RxJava you can find [here](https://github.com/Froussios/Intro-To-RxJava/blob/master/Part%204%20-%20Concurrency/2.%20Testing%20Rx.md). -------------------------------------------------------------------------------- /en/faq.md: -------------------------------------------------------------------------------- 1 | # Clean Architecture FAQ 2 | 3 | ### Should I rewrite all project when I migrate to Clean Architecture 4 | 5 | There is no definite answer to this question. If your project is big enough and migration to Clean Architecture take a lot of time, the best bet is to rewrite the project gradually, using the approach that we have described above. But you can try to rewrite the project from scratch if your project is small (contains 2-3 screens) and you have enough time. 6 | 7 | Certainly a cautionary tale about developers of Netscape, who decided to rewrite the code from scratch - [Things You Should Never Do, Part I](https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i). 8 | 9 | ### Is it necessary to create separate models for each layer (Domain, Data, Presentation)? 10 | 11 | According to the principles of Clean Architecture, Domain layer should know nothing about outer layers (Data and Presentation), but outer layer can use classes from Domain layer. Consequently, yoг don't have to create separate models for each layer. However, if models for each layer are different, you must to create different classes. If you need to add annotations from libraries (for example, from Gson or Room) to models (without changing model structure), then you can add them directly to Domain-layer models, even though these are external libraries of Data layer, because creating separate models will be unnecessary duplication of code. 12 | 13 | ### Do we need to create interfaces for Presenters and Interactors to improve code testability? 14 | 15 | This is an example of Presenter with interface: 16 | ```kotlin 17 | interface LoginPresenter { 18 | 19 | fun onLoginButtonPressed(email: String, password: String) 20 | 21 | } 22 | 23 | class LoginPresenterImpl : LoginPresenter { 24 | ... 25 | } 26 | ``` 27 | 28 | No, you don't need to create interface for Presenters or Interactors, because it creates additional problems and has no advantages. There are some of the problems created by using redundant interfaces: 29 | 30 | - If we want to add new method or change existing one, we need to change the interface. Also, we need to change implementation of the interface. It takes quite some time, even using such a powerful IDE as Android Studio. 31 | - Using of additional interfaces makes code navigation more difficult. For example, if you want to open an implementation of a Presenter method from Activity, then you go to a method definition in the interface. 32 | - The interface doesn't improve code testability. You can easily replace Presenter's implementation to it's mock, using any mocking library. 33 | 34 | You can read more about this topic in the following articles: 35 | 36 | - [Interfaces for presenters in MVP are a waste of time!](http://blog.karumi.com/interfaces-for-presenters-in-mvp-are-a-waste-of-time) 37 | 38 | ### How to pass arguments into Presenter, when it's instance created via DI-container? 39 | 40 | Often when creating a presenter, we want to pass arguments to constructor For example, we want to pass an article ID to get its content from the server. To do this, we need to create a separate module for Presenter and pass the arguments there: 41 | 42 | ```kotlin 43 | @Module 44 | class ArticleDetailsModule(val articleId: Long) { 45 | 46 | @Provides 47 | @Presenter 48 | fun provideArticleId() = articleId 49 | 50 | } 51 | ``` 52 | 53 | Next we need to add this module to the Component: 54 | ```kotlin 55 | @Component(dependencies = [ApplicationComponent::class], modules = [ArticleDetailsModule::class]) 56 | interface ArticleDetailsComponent { 57 | ``` 58 | 59 | When creating the Component we provide our module with arguments: 60 | ```kotlin 61 | val component = DaggerArticleDetailsComponent.builder() 62 | .applicationComponent(MyApplication.getComponent()) 63 | .articleDetailsModule(ArticleDetailsModule(articleId)) 64 | .build(); 65 | ``` 66 | 67 | Now we can get our identifier through constructor: 68 | ```kotlin 69 | class UserFollowersPresenter @Inject constructor( 70 | private val interactor: ArticleDetailsInteractor, 71 | private val articleId: Long 72 | ) 73 | ``` 74 | 75 | Let's suppose that in addition an article ID, we want to pass an user ID, that also has type "long". If we try to create another provide-method in our module, Dagger will give an error message, because it doesn't know which method provides an user ID or an article ID. 76 | 77 | To fix this, we need to create **Qualifier** annotations that will tell Dagger "who is who": 78 | 79 | ```kotlin 80 | @Qualifier 81 | @Retention(AnnotationRetention.RUNTIME) 82 | annotation class ArticleId 83 | 84 | @Qualifier 85 | @Retention(AnnotationRetention.RUNTIME) 86 | annotation class UserId 87 | ``` 88 | 89 | Add annotations to our provide-methods: 90 | 91 | ```kotlin 92 | @ArticleId 93 | @Provides 94 | @Presenter 95 | fun provideArticleId() = articleId 96 | 97 | @UserId 98 | @Provides 99 | @Presenter 100 | fun provideUserId() = userId 101 | ``` 102 | 103 | Also, we need to mark the constructor arguments with annotations: 104 | 105 | ```kotlin 106 | class UserFollowersPresenter @Inject constructor( 107 | private val interactor: ArticleDetailsInteractor, 108 | @ArticleId private val articleId: Long, 109 | @UserId private val userId: Long 110 | ) 111 | ``` 112 | 113 | Done. Now Dagger can pass the arguments correctly. -------------------------------------------------------------------------------- /ru/additional_entities.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Дополнительные сущности, используемые на практике 4 | 5 | - [Router](#router) 6 | - [Mapper](#mapper) 7 | - [ResourceManager](#resourcemanager) 8 | - [SchedulersProvider](#schedulersprovider) 9 | 10 | ## Дополнительные сущности, используемые на практике 11 | 12 | ### Router 13 | 14 | Т. к. Presenter содержит в себе логику реагирования на действия пользователя, то он также знает о том, на какой экран нужно перейти. Однако сам Presenter не может осуществлять переход на новый экран, т. к. для этого нам требуется Context. Поэтому за открытие нового экрана должна отвечать View. Для осуществления перехода на следующий экран мы должны вызвать метод View, например, **openProfileScreen()**, а уже в реализации самого метода осуществлять переход. Помимо данного подхода некоторые разработчики используют для навигации так называемый Router. 15 | 16 | **Router** - класс, для осуществления переходов между экранами (активити или фрагментами). 17 | 18 | Для реализации Router'а вы можете использовать библиотеку [Alligator](https://github.com/aartikov/Alligator). 19 | 20 | ### Mapper 21 | 22 | **Mapper** - специальный класс, для конвертирования моделей из одного типа в другой, например, из модели БД в модель бизнес-логики. Обычно они имеют название типа XxxMapper, и имеют единственный метод с названием map (иногда встречаются названия convert/transform), например: 23 | 24 | ```kotlin 25 | class ArticleDbModelMapper { 26 | 27 | fun map(model: ArticleDbModel) = 28 | Article( 29 | name = model.name, 30 | lastname = model.lastname, 31 | age = model.age 32 | ) 33 | 34 | fun map(models: Collection) = models.map { map(it) } 35 | 36 | } 37 | ``` 38 | 39 | Т. к. слой **domain** ничего не знает о классах других слоев, то маппинг моделей должен выполняться во внешних слоях, т. е. репозиторием (при конвертации **data** > **domain** или **domain** > **data**) или презентером (при конвертации **domain** > **presentation** и наоборот) . 40 | 41 | ### ResourceManager 42 | 43 | В некоторых случаях может потребоваться получить строку или число из ресурсов приложения в Presenter'е или слое **domain** . Однако, мы знаем, что они не должны напрямую взаимодействовать с фреймворком Android. Чтобы решить эту проблему мы можем создать специальную сущность ResourceManager, для доступа к внешним ресурсам. Для этого мы создаем интерфейс: 44 | 45 | ```kotlin 46 | interface ResourceManager { 47 | 48 | fun getString(resourceId: Int): String 49 | 50 | fun getInteger(resourceId: Int): Int 51 | 52 | } 53 | ``` 54 | 55 | Сам интерфейс должен располагаться в слое **domain**. После этого в слое **presentation** мы создаем реализацию нашего интерфейса: 56 | 57 | ```kotlin 58 | class AndroidResourceManager @Inject constructor( 59 | private val context: Context 60 | ) : ResourceManager { 61 | 62 | override fun getString(resourceId: Int): String { 63 | return context.resources.getString(resourceId) 64 | } 65 | 66 | override fun getInteger(resourceId: Int): Int { 67 | return context.resources.getInteger(resourceId) 68 | } 69 | 70 | } 71 | ``` 72 | 73 | Далее мы должны связать интерфейс и реализацию нашего ResourceManager'а в ApplicationModule: 74 | 75 | ```kotlin 76 | @Singleton 77 | @Provides 78 | fun provideResourceManager(resourceManager: AndroidResourceManager) : ResourceManager = resourceManager 79 | ``` 80 | 81 | Теперь мы можем использовать ResourceManager в Presenter'е или Interactor'ах: 82 | 83 | ```kotlin 84 | @InjectViewState 85 | class ArticlesListPresenter @Inject constructor( 86 | private val resourceManager: AndroidResourceManager 87 | ) : MvpPresenter() { 88 | 89 | private fun onLoadError(throwable: Throwable) { 90 | ... 91 | 92 | viewState.showMessage(resourceManager.getString(R.string.articles_load_error)) 93 | } 94 | 95 | } 96 | ``` 97 | 98 | Наверное, у внимательных читателей возник вопрос: почему мы используем класс **R** в Presenter'е? Ведь он также относится к Android? На самом деле, это не совсем так. Класс **R** вообще не использует никакие классы, и представляет из себя набор идентификаторов ресурсов. Поэтому, нет ничего плохого, чтобы использовать его в Presenter'е. 99 | 100 | ### SchedulersProvider 101 | 102 | Перед началом тестирования нам нужно сделать все операции синхронными. Для этого мы должны заменить все Scheduler'ы на **TestScheduler**, поэтому мы не устанавливаем Scheduler'ы напрямую через класс **Schedulers**, а используем **SchedulersProvider**: 103 | 104 | ```kotlin 105 | open class SchedulersProvider @Inject constructor() { 106 | 107 | open fun ui(): Scheduler = AndroidSchedulers.mainThread() 108 | open fun computation(): Scheduler = Schedulers.computation() 109 | open fun io(): Scheduler = Schedulers.io() 110 | open fun newThread(): Scheduler = Schedulers.newThread() 111 | open fun trampoline(): Scheduler = Schedulers.trampoline() 112 | 113 | } 114 | ``` 115 | 116 | Благодаря этому мы можем легко заменить Scheduler'ы на нужные нам, всего лишь создав наследника класса SchedulersProvider'а и переопределив методы: 117 | 118 | ```kotlin 119 | class TestSchedulerProvider : SchedulersProvider() { 120 | 121 | val testScheduler = TestScheduler() 122 | 123 | override fun ui(): Scheduler = testScheduler 124 | override fun computation(): Scheduler = testScheduler 125 | override fun io(): Scheduler = testScheduler 126 | override fun newThread(): Scheduler = testScheduler 127 | override fun trampoline(): Scheduler = testScheduler 128 | 129 | } 130 | ``` 131 | 132 | Далее, при самом тестировании, нам нужно будет лишь использовать TestSchedulersProvider вместо SchedulersProvider. Более подробно о тестировании кода с RxJava можно почитать [здесь](https://github.com/Froussios/Intro-To-RxJava/blob/master/Part%204%20-%20Concurrency/2.%20Testing%20Rx.md). -------------------------------------------------------------------------------- /ru/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ по Clean Architecture 2 | 3 | ### Стоит ли переписывать весь проект при переносе проекта на Clean Architecture? 4 | 5 | Наверное, нет однозначного ответа на этот вопрос. Если проект большой и переход на Clean Architecture может занять длительный промежуток времени, то лучше переписывать код постепенно, используя подход, который мы описали выше. Если же проект простой и состоит из 2-3 экранов, а сроки не поджимают, то вы можете попробовать переписать проект с нуля. 6 | 7 | В конце хочется привести поучительную историю про Netscape, который переписывали с нуля больше, чем три года - [Things You Should Never Do, Part I](https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/) 8 | 9 | ### Обязательно ли создавать отдельные сущности для каждого из слоев (Domain, Data, Presentaion)? 10 | 11 | Согласно принципам Clean Architecture, слой Domain ничего не должен знать о внешних слоях (Data и Presentation), но внешние слои без проблем могут использовать классы из слоя Domain. Следовательно, можно не создавать отдельные сущности для каждого из слоев, а использовать только те, что лежат в слое Domain. Однако, если их формат не совпадает с тем, что используется во внешних слоях, то нужно создать отдельную сущность. Если вам нужно добавить к моделям аннотации от библиотек (например, от [Gson](https://github.com/google/gson) или [Room](https://developer.android.com/topic/libraries/architecture/room.html)), то вы можете добавить их прямо в модели Domain-слоя, несмотря на то, что это внешние библиотеки слоя Data, т. к. создание отдельных моделей приведёт к ненужному дублированию кода. 12 | 13 | ### Нужно ли создавать интерфейсы для классов Presenter и Interactor для улучшения тестируемости кода? 14 | 15 | Пример Presenter'а с интерфейсом: 16 | 17 | ```kotlin 18 | interface LoginPresenter { 19 | 20 | fun onLoginButtonPressed(email: String, password: String) 21 | 22 | } 23 | 24 | class LoginPresenterImpl : LoginPresenter { 25 | ... 26 | } 27 | ``` 28 | 29 | Нет, интерфейсы для презентера и интерактора создавать не нужно. Это создает дополнительные сложности при разработке, при этом пользы от данного подхода практически нет. Вот лишь некоторые проблемы, которые порождает создание лишних интерфейсов: 30 | 31 | - Если мы хотим добавить новый метод или изменить существующий, нам нужно изменить интерфейс. Помимо этого мы также должны изменить реализацию метода. Это занимает довольно времени, даже при использовании такой продвинутой IDE как Android Studio. 32 | - Использование дополнительных интерфейсов усложняет навигацию по коду. Если вы хотите перейти к реализации метода Presenter'а из Activity (т. е. реализации View), то вы переходите к интерфейсу Presenter'а. 33 | - Интерфейс никак не улучшает тестируемость кода. Вы с легкостью можете заменить класс Presenter'а на mock, используя любую библиотеку для mock'ирования. 34 | 35 | Более подробно можете почитать об этом в следующих статьях: 36 | 37 | - [Interfaces for presenters in MVP are a waste of time!](http://blog.karumi.com/interfaces-for-presenters-in-mvp-are-a-waste-of-time) 38 | - [Франкен-код или франкен-дизайн](https://plus.google.com/u/0/+SergeyTeplyakov/posts/gRaqrqaiGbe) 39 | - [Управление зависимостями](http://sergeyteplyakov.blogspot.ru/2012/11/blog-post.html) 40 | 41 | ### Как передать аргументы в Presenter, если его инстанс создает DI-контейнер? 42 | 43 | Часто при создании презентера возникает необходимость передать дополнительные аргументы. Например, мы хотим передать идентфикатор статьи, чтобы получить её содержимое от сервера. Чтобы сделать это, нам необходимо создать отдельный модуль для Presenter'а и передать аргументы туда: 44 | 45 | ```kotlin 46 | @Module 47 | class ArticleDetailsModule(val articleId: Long) { 48 | 49 | @Provides 50 | @Presenter 51 | fun provideArticleId() = articleId 52 | 53 | } 54 | ``` 55 | 56 | Далее нам нужно добавить наш модуль в Component: 57 | 58 | ```kotlin 59 | @Component(dependencies = [ApplicationComponent::class], modules = [ArticleDetailsModule::class]) 60 | interface ArticleDetailsComponent { 61 | ``` 62 | 63 | При создании Component'а мы должны передать наш модуль с идентификатором: 64 | 65 | ```kotlin 66 | val component = DaggerArticleDetailsComponent.builder() 67 | .applicationComponent(MyApplication.getComponent()) 68 | .articleDetailsModule(ArticleDetailsModule(articleId)) 69 | .build(); 70 | ``` 71 | 72 | Теперь мы можем получить наш идентификатор через конструктор: 73 | 74 | ```kotlin 75 | class UserFollowersPresenter @Inject constructor( 76 | private val interactor: ArticleDetailsInteractor, 77 | private val articleId: Long 78 | ) 79 | ``` 80 | 81 | Теперь представим, что помимо идентфикатора статьи, мы хотим передать ID пользователя, который так же имеет тип long. Если мы попытаемся создать ещё один provide-метод в нашем модуле, Dagger выдаст ошибку, о том, что типы совпадают и он не знает какой из них является идентфикатором статьи, а какой идентфикатором пользователя. 82 | 83 | Чтобы исправить это, нам необходимо создать Qualifier-аннотации, которые будут указывать Dagger'у "who is who": 84 | 85 | ```kotlin 86 | @Qualifier 87 | @Retention(AnnotationRetention.RUNTIME) 88 | annotation class ArticleId 89 | 90 | @Qualifier 91 | @Retention(AnnotationRetention.RUNTIME) 92 | annotation class UserId 93 | ``` 94 | 95 | Добавляем аннотации к нашим provide-методам: 96 | 97 | ```kotlin 98 | @ArticleId 99 | @Provides 100 | @Presenter 101 | fun provideArticleId() = articleId 102 | 103 | @UserId 104 | @Provides 105 | @Presenter 106 | fun provideUserId() = userId 107 | ``` 108 | 109 | Также нужно пометить аннотациями аргументы конструктора: 110 | 111 | ```kotlin 112 | class UserFollowersPresenter @Inject constructor( 113 | private val interactor: ArticleDetailsInteractor, 114 | @ArticleId private val articleId: Long, 115 | @UserId private val userId: Long 116 | ) 117 | ``` 118 | 119 | Готово. Теперь Dagger сможет верно расставить аргументы в конструктор. -------------------------------------------------------------------------------- /en/layers_and_ioc.md: -------------------------------------------------------------------------------- 1 | # Layers and Inversion of Control 2 | 3 | ![CleanArchitectureLayers](https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/master/images/CleanArchitectureLayers.png) 4 | 5 | - [Domain layer](#domain-layer) 6 | - [Data layer](data-layer) 7 | - [Repository](#repository) 8 | - [Presentation layer](#presentation-layer) 9 | - [Model](#model) 10 | - [View](#view) 11 | - [Presenter](#presenter) 12 | - [Binding View with Presenter](#binding-view-with-presenter) 13 | 14 | As mentioned earlier, an app architecture based on Clean Architecture principles can be divided into three layers: 15 | 16 | - presentation 17 | - domain 18 | - data 19 | 20 | Above presents layers interaction scheme. Those black arrows represent dependencies of one layer on another, and blue arrows represent data flow. As you can see, data and presentation layers depend on domain layer, i. e. they use its classes. But **domain** layer doesn't know anything about outer layers and uses only its own classes and interfaces. Next, we will explain each of those layers in more detail, and how they interact. 21 | 22 | As can be seen from the scheme, all three layers can exchange data. It is worth mentioning that direct interaction between the **presentation** and the **data** layers must not be allowed. Data flow should go from the **presentation** layer to the **data** layer through the **domain** (this could be, for example, passing string with search query or user registration data). The same can happen and vice versa (for example, when we return search results). 23 | 24 | ## Domain layer 25 | 26 | ![DomainLayer](https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/master/images/DomainLayer.png) 27 | 28 | **Business logic** is rules, that describe how a business works (for example, a user cannot make a purchase for a price that more than his account balance). Business logic does not depend on the implementation of the database or UI. Business logic changes only when business requirements change. 29 | 30 | Robert Martin divides business logic into two types: a specific for a concrete application and common for all apps (if you want to share your code between platforms). 31 | 32 | **Entity** - contains application independent business rules. 33 | 34 | **Interactor** - an object that implements business logic for a specific application. 35 | 36 | But it's all in theory. In practice, only Interactors are used. At least I have not seen any application that uses Entity. By the way, many confuse Entity with DTO (Data Transfer Object). The fact is that the Entity of Clean Architecture is not exactly the Entity that we are used to seeing. 37 | 38 | **Use Case** - is a series of operations to achieve a goal. Example of a use case for user registration: 39 | 40 | > 1. The system checks user data 41 | > 2. The system sends data to the server for registration 42 | > 3. The system informs the user about the successful registration or error 43 | > 44 | > Exceptional situations: 45 | > 46 | > 1. The user entered incorrect data (the system shows an error) 47 | 48 | Let's see how it looks like in practice. Robert Martin suggests to create the separate class for each use case, that has a single method to run it. An example of such a class: 49 | 50 | ```kotlin 51 | class RegisterUserInteractor( 52 | private val userRepository: UserRepository, 53 | private val registerDataValidator: RegisterDataValidator, 54 | private val schedulersProvider: SchedulersProvider 55 | ) { 56 | 57 | fun execute(userData: User): Single { 58 | return registerDataValidator.validate(userData) 59 | .flatMap { userData -> userRepository.registerUser(userData) } 60 | .subscribeOn(schedulersProvider.io()) 61 | } 62 | 63 | } 64 | ``` 65 | 66 | However, practice shows that with this approach, you get a huge number of classes, with a small amount of code. A better approach would be to create single Interactor for one screen, the methods of which implement a certain use case. For example: 67 | 68 | ```kotlin 69 | class ArticleDetailsInteractor( 70 | private val articlesRepository: ArticlesRepository, 71 | private val schedulersProvider: SchedulersProvider 72 | ) { 73 | 74 | fun getArticleDetails(articleId: Long): Single
{ 75 | return articlesRepository.getArticleDetails(articleId) 76 | .subscribeOn(schedulersProvider.io()) 77 | } 78 | 79 | fun addArticleToFavorite(articleId: Long, isFavorite: Boolean): Completable { 80 | return articlesRepository.addArticleToFavorite(articleId, isFavorite) 81 | .subscribeOn(schedulersProvider.io()) 82 | } 83 | 84 | } 85 | ``` 86 | 87 | As you can see, sometimes Interactor methods cannot contain business logic at all, and Interactor methods act as a proxy between Repository and Presenter. 88 | 89 | If you notice, the Interactor methods return not just the result, but the classes of RxJava 2 (depending on the type of operation we use different classes - Single, Completable, etc.). This gives several advantages: 90 | 91 | 1. You don't need to create listeners to get results. 92 | 2. It's easy to switch threads. 93 | 3. It's easy to handle errors. 94 | 95 | 96 | To switch between threads, we use the` subscribeOn` method, as usual, but we get the Scheduler not through the static methods of the Schedulers class, but with the [SchedulersProvider](#schedulersprovider). It will help us in the future when we want to test our code. 97 | 98 | ### Data layer 99 | 100 | ![DataLayer](https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/master/images/DataLayer.png) 101 | 102 | This layer contains everything about storing and managing data. It could be database, SharedPreferences, network or file system, as well as caching logic. 103 | 104 | As a "bridge" between data and domain layer, there is Repository interface (in the original Uncle Bob's scheme it's called Gateway). The interface itself is stored in the Domain layer, but its implementation is stored in the Data layer. In doing so, domain layer classes don't know where the data comes from - from the database, the network or from somewhere else. That's why all caching logic should be contained in the data layer. 105 | 106 | #### Repository 107 | 108 | **Repository** - is the interface with which Interactor works. It describes what data Interactor wants to obtain from external layers. There may be several repositories in the application, depending on the task. For example, if we develop a news application, the repository that works with articles can be called ArticleRepository, and a repository for working with comments will be called CommentRepository . An example of a repository that works with articles: 109 | 110 | ```kotlin 111 | interface ArticleRepository { 112 | 113 | fun getArticle(articleId: String): Single
114 | 115 | fun getLastNews(): Single> 116 | 117 | fun getCategoryArticles(categoryId: String): Single> 118 | 119 | fun getRelatedPosts(articleId: String): Single> 120 | 121 | } 122 | ``` 123 | 124 | ### Presentation layer 125 | 126 | ![PresentationLayer](https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/master/images/PresentationLayer.png) 127 | 128 | Presentation layer contains all UI components, such as views, Activities, Fragments, etc. Also, it contains Presenters and Views (or ViewModels if you use MVVM). In this tutorial, we will use MVP (Model-View-Presenter) pattern, but you can choose other one (MVVM, MVI). 129 | 130 | The library helps to solve many problems related with Activity lifecycle. Moxy has base classes, there are `MvpView` and `MvpPresenter`, that must be extended by all your Views and Presenters. To save you from coding boilerplate classes Moxy uses Annotation Processing. For the correct work of code generation, you must use the special annotations provided by Moxy. More information about the library you can find [here](https://medium.com/redmadrobot-mobile/android-without-lifecycle-mpvsv-approach-with-moxy-6a3ae33521e). 131 | 132 | #### Model 133 | 134 | Model contains the business-logic and code for managing data. And because we use Clean Architecture + MVP, as Model will act Data and Domain layers. 135 | 136 | #### View 137 | 138 | View is responsible for how the data will be shown to the user. In the case of Android, View must be implemented by an Activity or Fragment. Also, View informs the Presenter of user interaction, such as button click or text input. There is example of View: 139 | 140 | ```kotlin 141 | interface ArticlesListView : MvpView { 142 | 143 | fun showLoadingProgress(show: Boolean) 144 | fun showArticles(articles: List
) 145 | fun showArticlesLoadingErrorMessage() 146 | 147 | } 148 | ``` 149 | 150 | So far we have described View interface, i. e. which View methods the Presenter can call. Note that our View is inherited from the **MvpView**, which is part of the Moxy library. That is a must for correct library work. 151 | 152 | #### Presenter 153 | 154 | According to the MVP concept, View can not directly interact with the Model, so the bridge between them is Presenter. Presenter reacts to user actions that View has notified to it (such as pressing a button, list item click or text input), and then decides what to do next. For example, it could be a data request from the model and display them in the View. Presenter example: 155 | 156 | ```kotlin 157 | @InjectViewState 158 | class ArticlesListPresenter @Inject constructor( 159 | private val articlesListInteractor: ArticlesListInteractor, 160 | private val schedulersProvider: SchedulersProvider 161 | ) : MvpPresenter() { 162 | 163 | init { 164 | loadArticles() 165 | } 166 | 167 | private fun loadArticles() { 168 | viewState.showLoadingProgress(true) 169 | articlesListInteractor.getArticles() 170 | .observeOn(schedulersProvider.ui()) 171 | .subscribe( 172 | { articles -> 173 | getViewState().showLoadingProgress(false) 174 | getViewState().showArticles(articles) 175 | }, 176 | { throwable -> getViewState().showLoadingError() } 177 | ) 178 | } 179 | 180 | fun onArticleSelected(article: Article) { 181 | ... 182 | } 183 | 184 | } 185 | ``` 186 | 187 | All the necessary classes we pass through the Presenter's constructor. It's actually called "constructor injection". 188 | 189 | When creating the Presenter's object we must pass dependencies required by the constructor. If there are a lot of them, the creation of Presenter will be rather difficult. Because we don't want to do this manually, we will delegate this work to the Component. 190 | 191 | ```kotlin 192 | @Presenter 193 | @Component(dependencies = ApplicationComponent::class) 194 | interface ArticlesListComponent { 195 | 196 | val getPresenter() : ArticlesListPresenter 197 | 198 | } 199 | ``` 200 | 201 | It will supply the necessary dependencies, and we need only get the Presenter instance by calling the **getPresenter()** method. If you have a question, "How do you pass arguments to Presenter then?", then look in the FAQ - there is a detailed description of this issue. 202 | 203 | Sometimes you can find the code in which the DI-container (Component) is passed to the constructor, after which all the necessary dependencies inject in the fields: 204 | 205 | ```kotlin 206 | @Inject 207 | lateinit var articlesListInteractor: ArticlesListInteractor 208 | 209 | init { 210 | component.inject(this) 211 | } 212 | ``` 213 | 214 | However, this approach is incorrect, because it complicates the testing of the class and creates a bunch of unnecessary code. If in the first case we could just pass mocked classes through the constructor, then now we need to create a DI-container and pass it. Also, this approach makes the class dependent on a particular DI-framework, which is also not good. 215 | 216 | Also, note that before we display the results from Interactor, we switch the UI thread via `observeOn (schedulersProvider.ui())`, because we don't know in advance in what thread we receive the data 217 | 218 | #### Binding View with Presenter 219 | 220 | In the context of Android developing, the Activity (or Fragment) acts as a View, so after creating the View interface, we need to implement it in our Activity or Fragment: 221 | 222 | ```kotlin 223 | class ArticlesListActivity : MvpAppCompatActivity(), ArticlesListView { 224 | 225 | @InjectPresenter 226 | lateinit var presenter: ArticlesListPresenter 227 | 228 | @ProvidePresenter 229 | fun provideArticlesListPresenter(): ArticlesListPresenter { 230 | val component = DaggerArticlesListPresenterComponent.builder() 231 | .applicationcomponent(MyApplication.getComponent()) 232 | .build() 233 | return component.getPresenter() 234 | } 235 | 236 | override fun onCreate(savedInstanceState: Bundle) { 237 | super.onCreate(savedInstanceState) 238 | setContentView(R.layout.activity_articles_list) 239 | 240 | } 241 | 242 | override fun showArticles(articles: List
) { 243 | ... 244 | } 245 | 246 | override fun showLoadingError() { 247 | ... 248 | } 249 | 250 | } 251 | ``` 252 | 253 | For Moxy to correct work, our Activity must extend **MvpAppCompatActivity** (or **MvpAppCompatFragment** for Fragments). To inject an `ArticlesListPresenter` instance into `presenter` field we use ```@InjectPresenter``` annotation. 254 | 255 | Since Presenter has arguments we must provide Presenter instance via ```provideArticlesListPresenter```, that we annotated with ```@ProvidePresenter```. Note that all annotated fields and methods must have public or package-private visibility. 256 | 257 | ### Packages organization 258 | 259 | We recommend using a *feature based* package structure for your code. Here is an example of package structure for a news app: 260 | 261 | ``` 262 | com.mydomain 263 | | 264 | |----data 265 | | |---- database 266 | | | |---- NewsDao 267 | | |---- filesystem 268 | | | |---- ImageCacheManager 269 | | |---- network 270 | | | |---- NewsApiService 271 | | |---- repositories 272 | | | |---- ArticlesRepositoryImpl 273 | | | |---- CategoriesRepositoryImpl 274 | | 275 | |---- domain 276 | | |---- global 277 | | | |---- models 278 | | | | |---- Article 279 | | | | |---- Category 280 | | | |---- repositories 281 | | | | |---- ArticlesRepository 282 | | | | |---- CategoriesRepository 283 | | |---- articledetails 284 | | | |---- ArticleDetailsInteractor 285 | | |---- articleslist 286 | | | |---- ArticlesListInteractor 287 | | 288 | |---- presentation 289 | | |---- mvp 290 | | | |---- global 291 | | | | |---- routing 292 | | | | | |---- NewsRouter 293 | | | |---- articledetails 294 | | | | |---- ArticleDetailsPresenter 295 | | | | |---- ArticleDetailsView 296 | | | |---- articleslist 297 | | | | |---- ArticlesListPresenter 298 | | | | |---- ArticlesListView 299 | | |---- ui 300 | | | |---- global 301 | | | | |---- views 302 | | | | |---- utils 303 | | | |---- articledetails 304 | | | | |---- ArticleDetailsActivity 305 | | | |---- articleslist 306 | | | | |---- ArticlesListActivity 307 | | 308 | |---- di 309 | | |---- global 310 | | | |---- modules 311 | | | | |---- ApiModule 312 | | | | |---- ApplicationModule 313 | | | |---- scopes 314 | | | |---- modifiers 315 | | | |---- ApplicationComponent 316 | | |---- articledetails 317 | | | |---- ArticleDetailsComponent 318 | | | |---- ArticleDetailsModule 319 | | |---- articleslist 320 | | | |---- ArticleListComponent 321 | ``` 322 | 323 | First of all, we divided the code into layers: data, domain, presentation. Also, we created a separate package for the DI code. The packages **domain**, **presentation** and **di** are divided by features, and the **data** package is divided by data source type (network, database, file system). This is because all features use the same classes (for example, **NewsApiService**) and it will be very difficult to divide them into features. 324 | 325 | Packages named **global** contain common classes that are used in several features. For example, the **data/global** package stores models and repository interfaces. 326 | 327 | The **presentation** layer is divided into two packages - **mvp** and **ui**. In the **mvp** are stored Presenter and View classes, as the name implies. In **ui** is stored the implementation of the View layer from MVP, i.e. Activity, Fragments, etc. 328 | 329 | This structure has the following benefits: 330 | 331 | - **Understandability.** Any developer can tell what functions there are in the application without looking in the code. 332 | - **Easier to add new feature**. If you want to add a new feature to the application, for example, viewing the user profile, then you just need to add the **userprofile** package and work only with it, rather than going through entire package structure for creating the necessary classes. 333 | - **Easier to edit a feature.** When you edit a feature, you need to keep at most two or three packages open and you see only those classes that relate to current feature. When you divide classes by the type, you have to keep open almost the whole tree of packages and you have to see the classes that you do not need now, related to other features. 334 | - **Scalability and easier code navigation**. With the increasing number of app's functions, the number of classes also increases. When you divide classes by type, adding new classes makes navigation among them very uncomfortable since you need to search for the necessary class, among dozens of others, which affects the speed of development. Dividing by features solves this problem because you can combine related packages (for example, you can combine **login** and **registration** packages into the **authentication** package). 335 | 336 | I'd like to say a few words about package naming: should package names be singular or plural? I hold the approach described [here](https://softwareengineering.stackexchange.com/a/75929): 337 | 338 | 1) Use singular for packages with heterogeneous classes. For example, a package, that contains classes like **Dog**, **Cat** and **Cow** will be called **animals**. Another example is different implementations of the same interface (**XmlResponseAdapter**, **JsonResponseAdapter**). 339 | 2) Use the plural for packages with homogeneous classes. For example, **order** package, that contains **OrderInfo**, **OrderInteractor**, **OrderValidation**, etc. -------------------------------------------------------------------------------- /ru/layers_and_ioc.md: -------------------------------------------------------------------------------- 1 | # Слои и инверсия зависимостей 2 | 3 | - [Слой бизнес-логики (Domain)](#Слой-бизнес-логики-domain) 4 | - [Слой работы с данными (Data)](#Слой-работы-с-данными-data) 5 | - [Repository](#repository) 6 | - [Слой отображения (Presentation)](#Слой-отображения-presentation) 7 | - [Model](#model) 8 | - [View](#view) 9 | - [Presenter](#presenter) 10 | - [Связывание View с Presenter'ом](#Связывание-view-c-presenterом) 11 | 12 | ![CleanArchitectureLayers](https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/master/images/CleanArchitectureLayers.png) 13 | 14 | Как уже говорилось ранее, архитектуру приложения, построенную по принципу Clean Architecture можно разделить на три слоя: 15 | 16 | - слой отображения (presentation) 17 | - слой бизнес-логики (domain) 18 | - слой работы с данными (data) 19 | 20 | Выше представлена схема того, как эти слои взаимодействуют. Черными стрелками обозначены зависимости одних слоев от других, а синими - поток данных. Как видите, слои **data** и **presentation** зависят от **domain**, т. е. они используют его классы. Сам же слой **domain** ничего не знает о внешних слоях и использует только собственные классы и интерфейсы. Далее мы разберем более подробно каждый из этих слоев, и то, как они взаимодействуют между собой. 21 | 22 | Как видно из схемы, все три слоя могут обмениваться данными. Следует отметить, что нельзя допускать прямого взаимодействия между слоями **presentation** и **data**. Поток данных должен идти от слоя **presentation** к **domain**, а от него к слою **data** (это может быть, например, передача строки с поисковым запросом или регистрационные данные пользователя). То же самое может происходить и в обратном направлении (например, при передаче списка с результатами поиска). 23 | 24 | ## Слой бизнес-логики (Domain) 25 | 26 | ![DomainLayer](https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/master/images/DomainLayer.png) 27 | 28 | **Бизнес-логика** - это правила, описывающие, как работает бизнес (например, пользователь не может совершить покупку на сумму больше, чем есть на его счёте). Бизнес-логика не зависит от реализации базы данных или интерфейса пользователя. Бизнес-логика меняется только тогда, когда меняются требования бизнеса, и не зависит от используемой СУБД или интерфейса пользователя. 29 | 30 | Роберт Мартин разделяет бизнес-логику на два вида: специфичную для конкретного приложения и общую для всех приложений (в том случае, если вы хотите сделать ваш код общим между приложениями под разные платформы). 31 | 32 | **Бизнес объект (Entity)** - хранят бизнес-логику общую для всех приложений. 33 | 34 | **Interactor** – объект, реализующий бизнес-логику специфичную для конкретного приложения. 35 | 36 | Но это все в теории. На практике же используются только Interactor'ы. По крайней мере, мне не встречались приложения, использующие Entity. Кстати, многие путают Entity с DTO (Data Transfer Object). Дело в том, что Entity из Clean Architecture - это не совсем те Entity, которые мы привыкли видеть. [Данная](https://habrahabr.ru/company/mobileup/blog/335382/) статья проливает свет на этот вопрос, а также на многие другие. 37 | 38 | **Сценарий использования (Use Case)** - набор операций для выполнения какой-либо задачи. Пример сценария использования при регистрации пользователя: 39 | 40 | > 1. Проверяем данные пользователя 41 | > 2. Отправляем данные на сервер для регистрации 42 | > 3. Сообщаем пользователю об успешной регистрации или ошибке 43 | > 44 | > Исключительные ситуации: 45 | > 46 | > 1. Пользователь ввел неверные данные (выдаем ошибку) 47 | 48 | Давайте теперь посмотрим как это выглядит на практике. Роберт Мартин предлагает создавать для каждого сценария использования отдельный класс, который имеет один метод для его запуска. Пример такого класса: 49 | 50 | ```kotlin 51 | class RegisterUserInteractor( 52 | private val userRepository: UserRepository, 53 | private val registerDataValidator: RegisterDataValidator, 54 | private val schedulersProvider: SchedulersProvider 55 | ) { 56 | 57 | fun execute(userData: User): Single { 58 | return registerDataValidator.validate(userData) 59 | .flatMap { userData -> userRepository.registerUser(userData) } 60 | .subscribeOn(schedulersProvider.io()) 61 | } 62 | 63 | } 64 | ``` 65 | 66 | Однако практика показывает, что при таком подходе получается огромное количество классов, с малым количеством кода. Более правильным будет создание одного Interactor'а на один экран, методы которого реализуют определенный сценарий, например: 67 | 68 | ```kotlin 69 | class ArticleDetailsInteractor( 70 | private val articlesRepository: ArticlesRepository, 71 | private val schedulersProvider: SchedulersProvider 72 | ) { 73 | 74 | fun getArticleDetails(articleId: Long): Single
{ 75 | return articlesRepository.getArticleDetails(articleId) 76 | .subscribeOn(schedulersProvider.io()) 77 | } 78 | 79 | fun addArticleToFavorite(articleId: Long, isFavorite: Boolean): Completable { 80 | return articlesRepository.addArticleToFavorite(articleId, isFavorite) 81 | .subscribeOn(schedulersProvider.io()) 82 | } 83 | 84 | } 85 | ``` 86 | 87 | Как видите иногда методы Interactor'а могут и вовсе не содержать бизнес-логики, а методы Interactor'а выступают в качестве прослойки между Repository и Presenter'ом. 88 | 89 | Если вы заметили, методы Interactor'а возвращают не просто результат, а классы RxJava 2 (в зависимости от типа операции мы используем разные классы - Single, Completable и т. д.). Это дает несколько преимуществ: 90 | 91 | 1. Не нужно создавать слушатели для получения результата. 92 | 2. Легко переключать потоки. 93 | 3. Легко обрабатывать ошибки. 94 | 95 | Для переключения потоков мы используем, как обычно, метод subscribeOn, однако мы получаем Scheduler не через статические методы класса Schedulers, а при помощи [SchedulersProvider'а](#schedulersprovider). В будущем это поможет нам при тестировании. 96 | 97 | ## Слой работы с данными (Data) 98 | 99 | ![DataLayer](https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/master/images/DataLayer.png) 100 | 101 | В данном слое содержится всё, что связано с хранением данных и управлением ими. Это может работа с базой данных, SharedPreferences, сетью или файловой системой, а также логика кеширования, если она имеется. 102 | 103 | "Мостом" между слоями data и domain является интерфейс Repository (в оригинальной схеме дядюшки Боба он называется Gateway). Сам интерфейс находится в слое domain, а уже реализация располагается в слое data. При этом классы domain-слоя не знают откуда берутся данные - из БД, сети или откуда-то ещё. Именно поэтому вся логика кеширования должна содержаться в data-слое. 104 | 105 | ### Repository 106 | 107 | **Repository** - представляет из себя интерфейс, с которым работает Interactor. В нем описывается какие данные хочет получать Interactor от внешних слоев. В приложении может быть несколько репозиториев, в зависимости от задачи. Например, если мы делаем новостное приложение, репозиторий работающий со статьями может называться ArticleRepository, а репозиторий для работы с комментариями CommentRepository. Пример репозитория, работающего со статьями: 108 | 109 | ```kotlin 110 | interface ArticleRepository { 111 | 112 | fun getArticle(articleId: String): Single
113 | 114 | fun getLastNews(): Single> 115 | 116 | fun getCategoryArticles(categoryId: String): Single> 117 | 118 | fun getRelatedPosts(articleId: String): Single> 119 | 120 | } 121 | ``` 122 | 123 | ## Слой отображения (Presentation) 124 | 125 | ![PresentationLayer](https://raw.githubusercontent.com/ImangazalievM/CleanArchitectureManifest/master/images/PresentationLayer.png) 126 | 127 | Слой представления содержит все компоненты, которые связаны с UI, такие как View-элементы, Activity, Fragment'ы и т. д. Помимо этого здесь содержатся Presenter'ы и View (или ViewModel'и при использовании MVVM). В данном туториале для реализации слоя presentation будет использован шаблон MVP, но вы можете выбрать любой другой (MVVM, MVI). 128 | 129 | Для более удобной связки View и Presenter мы будем использовать библиотеку [Moxy](https://github.com/Arello-Mobile/Moxy). Она помогает решить многие проблемы, связанные с жизненным циклом Activity или Fragment'а. Moxy имеет базовые классы, такие как ```MvpView``` и ```MvpPresenter```, от которых должны наследоваться наши View и Presenter. Для избежания написания большого количества кода по связыванию View и Presenter, Moxy использует кодогенерацию. Для правильной работы кодогенерации мы должны использовать специальные аннотации, которые предоставляет нам Moxy. Более подробную информацию о библиотеке можно найти [здесь](https://habrahabr.ru/post/276189/). 130 | 131 | ### Model 132 | 133 | MVP расшифровывается как Model-View-Presenter (модель-представление-презентер). Model содержит в себе бизнес-логику и код по работе с данными. Т. к. мы используем связку Clean Architecture + MVP, то Model у нас является код находящийся в слоях Data (работа с данными) и Domain (бизнес-логика). Следовательно, в слое Presentation остаются лишь два компонента - View и Presenter. 134 | 135 | ### View 136 | 137 | View отвечает за то, каким образом данные будут показаны пользователю. В случае с Android в качестве View выступает Activity или Fragment. Также View сообщает о действиях пользователя Presenter'у, будь то нажатие на кнопку или ввод текста. Пример View: 138 | 139 | ```kotlin 140 | interface ArticlesListView : MvpView { 141 | 142 | fun showLoadingProgress(show: Boolean) 143 | fun showArticles(articles: List
) 144 | fun showArticlesLoadingErrorMessage() 145 | 146 | } 147 | ``` 148 | 149 | Пока мы описали лишь интерфейс View, т. е. какие команды Presenter может отдавать View. Обратите внимание, что наш интерфейс наследуется от интерфейса **MvpView**, входящего в библиотеку Moxy. Это является обязательным условием для корректной работы библиотеки. 150 | 151 | ### Presenter 152 | 153 | Согласно концепции MVP, View не может напрямую взаимодействовать с Model, поэтому связующим звеном между ними является Presenter. Presenter реагирует на действия пользователя, о которых ему сообщила View (такие как нажатие на кнопку, пункт списка или ввод текста), после чего принимает решения о том, что делать дальше. Например, это может быть запрос данных у модели и отображение их во View. Пример Presenter'а: 154 | 155 | ```kotlin 156 | @InjectViewState 157 | class ArticlesListPresenter @Inject constructor( 158 | private val articlesListInteractor: ArticlesListInteractor, 159 | private val schedulersProvider: SchedulersProvider 160 | ) : MvpPresenter() { 161 | 162 | init { 163 | loadArticles() 164 | } 165 | 166 | private fun loadArticles() { 167 | viewState.showLoadingProgress(true) 168 | articlesListInteractor.getArticles() 169 | .observeOn(schedulersProvider.ui()) 170 | .subscribe( 171 | { articles -> 172 | getViewState().showLoadingProgress(false) 173 | getViewState().showArticles(articles) 174 | }, 175 | { throwable -> getViewState().showLoadingError() } 176 | ) 177 | } 178 | 179 | fun onArticleSelected(article: Article) { 180 | ... 181 | } 182 | 183 | } 184 | ``` 185 | 186 | Все необходимые классы для работы Presenter'а (как и всех остальных классов) мы передаем через конструктор. Этот способ так и называется - внедрение через конструктор. 187 | 188 | При создании объекта Presenter'а мы должны передать ему запрашиваемые конструктором зависимости. Если их будет много, то создание Presenter'а будет довольно сложным делом. Чтобы не делать этого вручную, мы доверим это дело Component'у. 189 | 190 | ```kotlin 191 | @Presenter 192 | @Component(dependencies = ApplicationComponent::class) 193 | interface ArticlesListComponent { 194 | 195 | val getPresenter() : ArticlesListPresenter 196 | 197 | } 198 | ``` 199 | 200 | Он подставит нужные зависимости, а нам нужно будет лишь получить инстанс Presenter'а вызвав метод **getPresenter()**. Если у вас возник вопрос "А как в таком случае передавать аргументы в Presenter?", то загляните в FAQ - там подробно описан этот вопрос. 201 | 202 | Иногда можно встретить такое, что в конструктор передается DI-контейнер (Component), после чего все необходимые зависимости внедряются в поля: 203 | 204 | ```kotlin 205 | @Inject 206 | lateinit var articlesListInteractor: ArticlesListInteractor 207 | 208 | init { 209 | component.inject(this) 210 | } 211 | ``` 212 | 213 | Однако, данный способ является неправильным, т. к. усложняет тестирование класса и создает кучу ненужного кода. Если в первом случае мы сразу могли передать mock'и классов через конструктор, то теперь нам нужно создать DI-контейнер и передавать его. Также данный способ делает класс зависимым от конкретного DI-фреймворка, что тоже не есть хорошо. 214 | 215 | Также обратите внимание на то, что перед тем как отобразить результаты, полученные от Interactor'а, мы переключаем поток на UI при помощи `observeOn(schedulersProvider.ui())`. Это сделано потому, что мы не знаем заранее в каком потоке нам придут данные. 216 | 217 | ### Связывание View с Presenter'ом 218 | 219 | В контексте разработки под Android роль View на себя берет Activity (или Fragment), поэтому после создания интерфейса View, мы должны реализовать его в нашей Activity или Fragment'е: 220 | 221 | ```kotlin 222 | class ArticlesListActivity : MvpAppCompatActivity(), ArticlesListView { 223 | 224 | @InjectPresenter 225 | lateinit var presenter: ArticlesListPresenter 226 | 227 | @ProvidePresenter 228 | fun provideArticlesListPresenter(): ArticlesListPresenter { 229 | val component = DaggerArticlesListPresenterComponent.builder() 230 | .applicationcomponent(MyApplication.getComponent()) 231 | .build() 232 | return component.getPresenter() 233 | } 234 | 235 | override fun onCreate(savedInstanceState: Bundle) { 236 | super.onCreate(savedInstanceState) 237 | setContentView(R.layout.activity_articles_list) 238 | 239 | } 240 | 241 | override fun showArticles(articles: List
) { 242 | ... 243 | } 244 | 245 | override fun showLoadingError() { 246 | ... 247 | } 248 | 249 | } 250 | ``` 251 | 252 | Хочу заметить, что для правильной работы библиотеки Moxy, наша Activity должна обязательно наследоваться от класса **MvpAppCompatActivity** (или **MvpAppCompatFragment** в случае, если вы используете фрагменты). С помощью аннотации ```@InjectPresenter``` мы сообщаем Annotation Processor'у в какую переменную нужно "положить" Presenter. 253 | 254 | Так как конструктор нашего Presenter'а не пустой, а принимает на вход определенные параметры, нам нужно предоставить библиотеке объект Presenter'а. Мы делаем это при помощи метода ```provideArticlesListPresenter```, который мы пометили аннотацией ```@ProvidePresenter```. Как и во всех других случаях использования кодогенерации, переменные и методы, помеченные аннотациями, должны быть видны на уровне пакета, т. е. у них не должно быть модификаторов видимости (private, public, protected). 255 | 256 | ## Разбиение классов по пакетам 257 | 258 | Ниже представлен пример разбиения пакетов по фичам новостного приложения: 259 | 260 | ``` 261 | com.mydomain 262 | | 263 | |----data 264 | | |---- database 265 | | | |---- NewsDao 266 | | |---- filesystem 267 | | | |---- ImageCacheManager 268 | | |---- network 269 | | | |---- NewsApiService 270 | | |---- repositories 271 | | | |---- ArticlesRepositoryImpl 272 | | | |---- CategoriesRepositoryImpl 273 | | 274 | |---- domain 275 | | |---- global 276 | | | |---- models 277 | | | | |---- Article 278 | | | | |---- Category 279 | | | |---- repositories 280 | | | | |---- ArticlesRepository 281 | | | | |---- CategoriesRepository 282 | | |---- articledetails 283 | | | |---- ArticleDetailsInteractor 284 | | |---- articleslist 285 | | | |---- ArticlesListInteractor 286 | | 287 | |---- presentation 288 | | |---- mvp 289 | | | |---- global 290 | | | | |---- routing 291 | | | | | |---- NewsRouter 292 | | | |---- articledetails 293 | | | | |---- ArticleDetailsPresenter 294 | | | | |---- ArticleDetailsView 295 | | | |---- articleslist 296 | | | | |---- ArticlesListPresenter 297 | | | | |---- ArticlesListView 298 | | |---- ui 299 | | | |---- global 300 | | | | |---- views 301 | | | | |---- utils 302 | | | |---- articledetails 303 | | | | |---- ArticleDetailsActivity 304 | | | |---- articleslist 305 | | | | |---- ArticlesListActivity 306 | | 307 | |---- di 308 | | |---- global 309 | | | |---- modules 310 | | | | |---- ApiModule 311 | | | | |---- ApplicationModule 312 | | | |---- scopes 313 | | | |---- modifiers 314 | | | |---- ApplicationComponent 315 | | |---- articledetails 316 | | | |---- ArticleDetailsComponent 317 | | | |---- ArticleDetailsModule 318 | | |---- articleslist 319 | | | |---- ArticleListComponent 320 | ``` 321 | 322 | Прежде чем делить код по фичам, мы разделили его на слои. Данный подход позволяет сразу определить к какому слою относится тот или иной класс. Если вы заметили, классы слоя **data** разбиты немного не так, как в слоях **domain**, **presentation** и **di**. Здесь вместо фич приложения мы выделили типы источников данных - сеть, база данных, файловая система. Это связано с тем, что все фичи используют практически одни и те же классы (например, **NewsApiService**) и их не имеет смысла разбивать по фичам. 323 | 324 | В пакетах с именем **global** хранятся общие классы, которые используются в нескольких фичах. Например, в пакете **data/global** хранятся модели и интерфейсы репозиториев. 325 | 326 | Слой **presentation** разбит на два пакета - **mvp** и **ui**. В **mvp** хранятся, как понятно из названия, классы Presenter'ов и View. В **ui** хранятся реализация слоя View из MVP, т. е. Activity, Fragment'ы и т. д. 327 | 328 | Разбиение классов по фичам имеет ряд преимуществ: 329 | 330 | - **Очевидность.** Даже не знакомый с проектом разработчик, при первом взгляде на структуру пакетов сможет примерно понять что делает приложение, не заглядывая в сам код. 331 | - **Добавление нового функционала**. Если вы решили добавить новую функцию в приложение, например, просмотр профиля пользователя, то вам лишь нужно добавить пакет **userprofile** и работать только с ним, а не "гулять" по всей структуре пакетов, создавая нужные классы. 332 | - **Удобство редактирования.** При редактировании какой либо фичи, нужно держать открытыми максимум два-три пакета и вы видите только те классы, которые относятся к конкретной фиче. При разбиении по типу класса, раскрытой приходится держать практически всё дерево пакетов и вы видите классы, которые вам сейчас не нужны, относящиеся к другим фичам. 333 | - **Удобство масштабирования**. При увеличении количества функций приложения, увеличивается и количество классов. При разбиении классов по типу, добавление новых классов делает навигацию по ним очень не удобным, т.к. приходится искать нужный класс, среди десятков других, что сказывается на скорости и удосбстве разработки. Разбиение по фичам решает эту проблему, т.к. вы можете объединить связанные между собой пакеты с фичами (например, можно объединить пакеты **login** и **registration** в пакет **authentication**). 334 | 335 | Также хочется сказать пару слов об именовании пакетов: в каком числе их нужно называть - множественном или единственном? Я придерживаюсь подхода, описанного [здесь](https://softwareengineering.stackexchange.com/a/75929): 336 | 337 | 1) Если пакет содержит однородные классы, то имя пакета ставится во множественном числе. Например, пакет с классами **Dog**, **Cat** и **Cow** будет называться **animals**. Другой пример - различные реализации какого-либо интерфейса (**XmlResponseAdapter**, **JsonResponseAdapter**). 338 | 2) Если пакет содержит разнородные классы, реализующую определенную функцию, то имя пакета ставится в единственном числе. Пример - пакет **order**, содержащий классы **OrderInfo**, **OrderInteractor**, **OrderValidation** и т. д. --------------------------------------------------------------------------------