Lesson 05 CLEANer Code ‐ Three Layer Architecture - FranGarc/LearningPath GitHub Wiki

Topic: CLEANer Code: Three layer architecture.

Clean code is a term used to describe code that is easy to read, understand, and maintain. Clean code is written in a way that makes it simple, concise, and expressive. It follows a set of conventions, standards, and practices that make it easy to read and follow.

We call a code CLEAN when it is free from complexity, redundancy, and other bad practices that make it difficult to maintain, debug, and modify.

When code is easy to read and understand, it makes it easier for developers to work on the codebase. It doesn't matter if you're part of a big team or you're alone: everyone needs a CLEAN code for a better work. A new team member might find it easier to find their way around when the code is easy to read and understand. And you yourself will thank when you need to go back to that particular piece of code you wrote months ago and, while you don't remember much of it, you can read it and know almost inmediately what's going on.

Clean code is based on the SOLID principles:
* The Single Responsibility Principle: a class should do one thing and therefore it should have only a single reason to change.
* The Open-Closed Principle: classes should be open for extension and closed to modification.
* The Liskov Substitution Principle: subclasses should be substitutable for their base classes.
* The Interface Segregation Principle: many client-specific interfaces are better than one general-purpose interface. Clients should not be forced to implement a function they do no need.
* The Dependency Inversion Principle: our classes should depend upon interfaces or abstract classes instead of concrete classes and functions.

Do your own research on the SOLID principles - plenty of free resources on the subject.

One way to have a Clean code is by establishing a Clean Architecture for our code, so that the principles are followed. There are several ideas on Clean Architecture, but we'll focus on the Three-Layer Architecture.

Three-layer architecture is a well-established software application architecture that organizes applications into three logical computing layers: the presentation layer, or user interface (app in Android terms); the domain layer, where data is processed following business logics; and the data layer, where the data associated with the application is stored and managed.

The chief benefit of three-layer architecture is that because each tier runs on its own infrastructure, each tier can be developed simultaneously by a separate development team, and can be updated or scaled as needed without impacting the other tiers.

Presentation is the layer with which the user directly interacts. This layer is a user interface, the mechanism for getting input from the user. It may contain controllers and view models, as well as views that make up the user interface

Domain - also known as Business logic - layer contains a set of components that are responsible for processing the data received from the UI layer. It implements all the necessary application logic, all the calculations. It interacts with the database and/or API and passes the result of processing to the UI layer.

Data access layer stores the data models. It also hosts specific classes for working with different data access technologies, like databases or remote APIs.

UseCases: Also called Interactors, they implement dataflow logic. We use them to prevent so-called "God objects" (activities or viewmodels in our case) that deal with both presentation logic and dataflow logic. It also allows for a dataflow logic that can be reused across multiple ViewModels.

Task: Implement Three Layer Architecture.

For a first approach, we'll simulate this architecture with different packages:

/ui/
    ... Activities, ViewModels, composables, etc.
    /data/
    ... Datasources, Data Transfer Objects (DTO), Repositories, etc. (at the moment: MockDataSource)
    /domain/
    ... UseCases, Entities, Mappers, etc.

Our current /data/model package is actually doing both functions, as DAO (the object with all the information our Datasource brings) and DTO (the object that transfer the data we need to the UI).

You need to create a domain package, with an model package where to store the classes that will transfer data to our UI.

Also a usecase package inside domain to store our UseCases.

Requisites

  • Create a /domain package.

  • Create a /domain/model package.

  • Create the Entity Objects (for this moment, copy the ones in /data/model).

  • Rename the classes in /data/model to end in "Dto": i.e. PokemonDto.

  • Create a DataSourceAdapter Interface inside the domain package. Give it the getPokemonList() and getPokemonDetails(pokemonName) methods. Return types must be entities, this is, domain objects.

  • Give the Entity main classes (Pokemon and PokemonDetail) mapper extension functions so you can instantiate an Entity object from a DTO object. Check Tips & Advice for a signature sample

  • Have our MockDataSource implement the Interface you just created. You'll need to change the return type to the Domain ones. Use the mappers for that.

  • Create a /domain/usecase packages.

  • Create two usecases, GetPokemonList and GetPokemonDetails inside that package. Each will call the getPokemonList and getPokemonDetails from the adapter interface.

  • Have each ViewModel call to the corresponding UseCase instead of calling directly to the MockDataSource.

When everything is done and running, check the following:

  • No classes in /ui package have a dependency (import) from /data
  • No classes in /domain package have a dependency from /ui
  • No classes in /data have a dependency from /ui

Tips & Advice.

Mapper signature example

fun PokemonDto.toPokemon(): Pokemon

Use case structure:

class FooUseCase {
   
   operator fun invoke() { // invoke can have parameters when necessary
       // your code here
   }
}

Use case example of usage:

fun example(fooUseCase: FooUseCase) {
  val result = fooUseCase()
}

Research terms.

  • Clean Code
  • SOLID Principles
  • Domain
  • Domain Entity
  • Data Transfer Object / DTO
  • Mapper function
  • UseCase
  • invoke operator

Additional resources.