Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Why do we need Clean Architecture

Igor Wojda
October 26, 2017

Why do we need Clean Architecture

Recording is available here (free registration required)
http://uk.droidcon.com/skillscasts/11002-why-do-we-need-clean-architecture

Igor Wojda

October 26, 2017
Tweet

More Decks by Igor Wojda

Other Decks in Programming

Transcript

  1. “ We should think about our applications as group of

    use cases that describe the intent of the application and group of plugins that give those uses cases access to outside word. Uncle Bob
  2. Why Clean Architecture? ◦ Independent of libraries & frameworks ◦

    Independent of UI ◦ Independent of data sources Business Logic UI Frameworks Data sources
  3. Presentation Layer Data Layer Presenter View Use case Entity Repository

    Network Data Source Disk Data Source Layers Domain Layer Dependencies
  4. Dependency inversion UseCase Repository • Define interface • Class Repository

    implements this interface • Class Repository is injected into class UseCase UseCase Repository
  5. Presentation Layer Domain Layer Data Layer Presenter View Use case

    Repository Network Data Source Disk Data Source Layers Abstractions Entity
  6. Data model data class PatientModel( val id: String, val firstName:

    String, val lastName: String, val dateOfBirth: String )
  7. Data model (GSON) data class PatientModel( @SerializedName("id") val id: String,

    @SerializedName("first_name") val firstName: String, @SerializedName("last_name") val lastName: String, @SerializedName("date_of_birth") val dateOfBirth: String )
  8. Data model (GSON + Room) @Entity(tableName = "patients") data class

    PatientModel( @SerializedName("id") @PrimaryKey val id: String, @SerializedName("first_name") @ColumnInfo(name = "first_name") val firstName: String, @SerializedName("last_name") @ColumnInfo(name = "last_name") val lastName: String, @SerializedName("date_of_birth") @ColumnInfo(name = "date_of_birth") val dateOfBirth: String )
  9. Data model (GSON + Room + Getters) @Entity(tableName = "patients")

    data class PatientModel( @SerializedName("id") @PrimaryKey val id: String, @SerializedName("first_name") @ColumnInfo(name = "first_name") val firstName: String, @SerializedName("last_name") @ColumnInfo(name = "last_name") val lastName: String, @SerializedName("date_of_birth") @ColumnInfo(name = "date_of_birth") val dateOfBirth: String ) { private val UNDER_AGE_LIMIT = 16 val fullName = "$firstName $lastName" val adult = DateTime.parse(dateOfBirth) .plusYears(UNDER_AGE_LIMIT) .isAfter(DateTime.now()) }
  10. Data models Presentation Layer Domain Layer Data Layer PatientPresentation Model

    Patient PatientNetworkModel PatientRoomModel PatientNetworkMapper PatientRoomMapper PatientViewMapper
  11. Separate data model data class PatientNetworkModel( @SerializedName("id") val id: String,

    @SerializedName("first_name") val firstName: String, @SerializedName("last_name") val lastName: String, @SerializedName("date_of_birth") val dateOfBirth: String ) Data Layer
  12. Separate data model Data Layer @Entity(tableName = "patients") data class

    PatientRoomModel( @PrimaryKey val id: String, @ColumnInfo(name = "first_name") val firstName: String, @ColumnInfo(name = "last_name") val lastName: String, @ColumnInfo(name = "date_of_birth") val dateOfBirth: String )
  13. Separate data model Domain Layer data class Patient( val id:

    String, val firstName: String, val lastName: String, val dateOfBirth: DateTime )
  14. Separate data model data class PatientPresentationModel( val fullName: String, val

    adult: Boolean, val dateOfBirth: String ) Presentation Layer
  15. Mapper interface Mapper<in FROM, out TO> { fun map(from: FROM):

    TO } class PatientNetworkModelMapper : Mapper<PatientNetworkModel, Patient> { override fun map(patientNetworkModel: PatientNetworkModel) = Patient( id = patientNetworkModel.id, firstName = patientNetworkModel.firstName, lastName = patientNetworkModel.lastName, dateOfBirth = DateTime.parse(patientNetworkModel.dateOfBirth) ) }
  16. Mapper interface Mapper<in FROM, out TO> { fun map(from: FROM):

    TO } class PatientNetworkModelMapper : Mapper<PatientNetworkModel, Patient> { override fun map(patientNetworkModel: PatientNetworkModel) = Patient( id = patientNetworkModel.id, firstName = patientNetworkModel.firstName, lastName = patientNetworkModel.lastName, dateOfBirth = DateTime.parse(patientNetworkModel.dateOfBirth) ) }
  17. Mapper interface Mapper<in FROM, out TO> { fun map(from: FROM):

    TO } class PatientNetworkModelMapper : Mapper<PatientNetworkModel, Patient> { override fun map(patientNetworkModel: PatientNetworkModel) = Patient( id = patientNetworkModel.id, firstName = patientNetworkModel.firstName, lastName = patientNetworkModel.lastName, dateOfBirth = DateTime.parse(patientNetworkModel.dateOfBirth) ) }
  18. Presentation Layer Domain Layer Data Layer Presenter View Use case

    Entity Repository Network Data Source Disk Data Source Implementations Abstractions
  19. Presenter class PatientsPresenter @Inject constructor( val getPatientsUseCase: PatientsUseCase, val mapper:

    PatientPresentationMapper) : BasePresenter<PatientsContract.View>, PatientsContract.Presenter { override fun onAttachView() { getPatientsUseCase.execute(PatientSubscriber()) } inner class PatientSubscriber : DisposableSingleObserver<List<Patient>>() { override fun onSuccess(patients: List<Patient>) { when { patients.isEmpty -> view?.showEmptyState() else -> view?.showPatients(Patients.map { patientMapper::map }) } } override fun onError(exception: Throwable) { view?.showErrorState() } //… } }
  20. Presenter class PatientsPresenter @Inject constructor( val getPatientsUseCase: PatientsUseCase, val mapper:

    PatientPresentationMapper) : BasePresenter<PatientsContract.View>, PatientsContract.Presenter { override fun onAttachView() { getPatientsUseCase.execute(PatientSubscriber()) } inner class PatientSubscriber : DisposableSingleObserver<List<Patient>>() { override fun onSuccess(patients: List<Patient>) { when { patients.isEmpty -> view?.showEmptyState() else -> view?.showPatients(mapper.map { patientMapper::map }) } } override fun onError(exception: Throwable) { view?.showErrorState() } //… } }
  21. Presenter class PatientsPresenter @Inject constructor( val getPatientsUseCase: PatientsUseCase, val mapper:

    PatientPresentationMapper) : BasePresenter<PatientsContract.View>, PatientsContract.Presenter { override fun onAttachView() { getPatientsUseCase.execute(PatientSubscriber()) } inner class PatientSubscriber : DisposableSingleObserver<List<Patient>>() { override fun onSuccess(patients: List<Patient>) { when { patients.isEmpty -> view?.showEmptyState() else -> view?.showPatients(mapper.map { patientMapper::map }) } } override fun onError(exception: Throwable) { view?.showErrorState() } //… } }
  22. Presenter class PatientsPresenter @Inject constructor( val getPatientsUseCase: PatientsUseCase, val mapper:

    PatientPresentationMapper) : BasePresenter<PatientsContract.View>, PatientsContract.Presenter { override fun onAttachView() { getPatientsUseCase.execute(PatientSubscriber()) } inner class PatientSubscriber : DisposableSingleObserver<List<Patient>>() { override fun onSuccess(patients: List<Patient>) { when { patients.isEmpty -> view?.showEmptyState() else -> view?.showPatients(mapper.map { patientMapper::map }) } } override fun onError(exception: Throwable) { view?.showErrorState() } //… } }
  23. Presenter class PatientsPresenter @Inject constructor( val getPatientsUseCase: PatientsUseCase, val mapper:

    PatientPresentationMapper) : BasePresenter<PatientsContract.View>, PatientsContract.Presenter { override fun onAttachView() { getPatientsUseCase.execute(PatientSubscriber()) } inner class PatientSubscriber : DisposableSingleObserver<List<Patient>>() { override fun onSuccess(patients: List<Patient>) { when { patients.isEmpty -> view?.showEmptyState() else -> view?.showPatients(mapper.map { patientMapper::map }) } } override fun onError(exception: Throwable) { view?.showErrorState() } //… } }
  24. Presentation Layer Domain Layer Data Layer Presenter View Use case

    Entity Repository Network Data Source Disk Data Source Layers Abstractions
  25. Use case (Interactor) class PatientsUseCase @Inject constructor(val patientRepository: PatientRepository, val

    schedulers: Schedulers) { fun execute(): Single<List<Patient>> = patientRepository.getPatients() .observeOn(schedulers.main) .subscribeOn(schedulers.io) .filter { it.adult } }
  26. Use case (Interactor) class PatientsUseCase @Inject constructor(val patientRepository: PatientRepository, val

    schedulers: Schedulers) { fun execute(): Single<List<Patient>> = patientRepository.getPatients() .observeOn(schedulers.main) .subscribeOn(schedulers.io) .filter { it.adult } }
  27. Use case (Interactor) class PatientsUseCase @Inject constructor(val patientRepository: PatientRepository, val

    schedulers: Schedulers) { fun execute(): Single<List<Patient>> = patientRepository.getPatients() .observeOn(schedulers.main) .subscribeOn(schedulers.io) .filter { it.adult } }
  28. Presentation Layer Domain Layer Data Layer Presenter View Use case

    Entity Repository Network Data Source Disk Data Source Layers Abstractions
  29. class PatientDataRepository @Inject constructor(val diskDataStore: PatientDiskDataStore, val networkDataStore: PatientNetworkDataStore) :

    PatientRepository { val networkDataStoreWithSave = networkDataStore.doOnSuccess { saveToDisk(it) } fun getPatients(): Single<List<Patient>> = Single.concat(diskDataStore, networkDataStoreWithSave) .takeFirst() fun saveToDisk() { /* … */ } } Repository
  30. class PatientDataRepository @Inject constructor(val diskDataStore: PatientDiskDataStore, val networkDataStore: PatientNetworkDataStore) :

    PatientRepository { val networkDataStoreWithSave = networkDataStore.doOnSuccess { saveToDisk(it) } fun getPatients(): Single<List<Patient>> = Single.concat(diskDataStore, networkDataStoreWithSave) .takeFirst() fun saveToDisk() { /* … */ } } Repository PatientRepository PatientDataRepository: PatientRepository
  31. class PatientDataRepository @Inject constructor(val diskDataStore: PatientDiskDataStore, val networkDataStore: PatientNetworkDataStore) :

    PatientRepository { val networkDataStoreWithSave = networkDataStore.doOnSuccess { saveToDisk(it) } fun getPatients(): Single<List<Patient>> = Single.concat(diskDataStore, networkDataStoreWithSave) .takeFirst() fun saveToDisk() { /* … */ } } Repository
  32. class PatientDataRepository @Inject constructor(val diskDataStore: PatientDiskDataStore, val networkDataStore: PatientNetworkDataStore) :

    PatientRepository { val networkDataStoreWithSave = networkDataStore.doOnSuccess { saveToDisk(it) } fun getPatients(): Single<List<Patient>> = Single.concat(diskDataStore, networkDataStoreWithSave) .takeFirst() fun saveToDisk() { /* … */ } } Repository
  33. Modularity Presentation Layer Domain Layer Data Layer Presenter Business Logic

    View GridView Recycler View TabLayout BottomBar Patient Repository Tracking Repository SQLite Volley Firebase Firebase Room Retrofit MixPanel
  34. Testing Presentation Layer Domain Layer Data Layer Presenter Use case

    View Repository Network Data Source Entity Room Data Source
  35. Testing Presentation Layer Domain Layer Data Layer Mapper Mock RetrofitService

    Mock Mapper Mock Mock SQLite Data Source Mock Data Source Mock Repository Mock Presenter Mock Mapper Mock Mapper Mock Mapper Mock Entity Entity Entity View Mock Use Case Mock Mapper Mock
  36. Pros and cons Boilerplate code Not suitable for all projects

    Multiple ways to implement Independent of Frameworks and tools Well structured Maintainable Modular Testable Great for big teams Issues Benefits
  37. References Articles https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html Talks https://youtu.be/Nsjsiz2A9mg https://youtu.be/3Mq5newPdck https://youtu.be/-sEmrJOk1uI https://youtu.be/su34EYeQ90E Android projects

    https://github.com/bufferapp/android-clean-architecture-boilerplate https://github.com/bufferapp/clean-architecture-components-boilerplate https://github.com/android10/Android-CleanArchitecture-Kotlin