For several years @vginert, @rubensoleralvarez and I have been working in many projects together. We have implemented the Android Clean Architecture (based on Uncle Bob’s Clean Architecture) by @fernandocejas in many of them, which I believe it’s a pretty neat solution for Android development, but… What if we want to bring that architecture a step further? What if we want to bring this good approach to a Java cross-platform environment? That’s what I call Java Clean Way.
Our goal shouldn’t be only delivering robust, maintainable, testable, flexible and scalable software, but to provide certain level of abstraction which allows our code to be reused by other modules, other projects, and also, by other platforms. This approach will allow your code to be:
- Frameworks independent
- Platform specific UI independent
- Platform specific data-access independent
Achieving also a great horizontal and vertical separation of concern, keeping your code as SOLID as possible.
Our example consists in an app which retrieves a list of countries from a remote host and renders the list to the user regardless the platform.
This approach counts with 3 functional components or layers, composed by at least 5 modules where 2 of them are platform specific.
- Platform View (Presentation layer)
- UI Controller (Presentation layer)
- Core (Domain layer)
- Data handler (Data layer)
- Platform data-access (Data layer)
The purpose is the separation of the business rules and the platform’s frameworks and features, allowing it to run and test regardless the environment since it’s pure Java code. Business set of modules have no idea in what platform it’s being executed, also knows nothing about the views or the data access frameworks. Core module is in the highest level of abstraction, holds the use-cases logic and knows nothing about the rest of the application. Meanwhile, platform’s specific views (windows, activities, forms, web pages, console, etc) and data access are in the lowest level of abstraction having direct access to the system’s input-output. Views and data access are connected to the business through its respective interfaces. All modules are bound together by the main (launch) module via dependency injection. In some cases, like in this example, the launch and Platform Views modules are merged.
Visual components of the presentation layer are placed here. Here is where the data is rendered and animations take place. Views must implement UI Controller module’s views interfaces and the least UI logic is allowed.
View presenters, presentation layer models, model mappers and view interfaces are placed here (MVP pattern or any other related pattern you prefer). Its job is to control the user interface and forward data requests to the Domain layer through its use cases.
Domain layer’s models, use cases and the whole application’s business logic and data flow is placed here. In this case, the Core is the director and brain of the application. It manipulates the data and takes it to its respective destination. It worth to mention that a Domain Layer is not necessarily made of one module, it depends on your app needs and your design. This layer also owns the repositories interfaces definition.
By implementing Core’s repository interfaces, Domain layer is allowed to access Data layer and request information. Data handler module gathers the data from its interfaces and returns it back to Domain. Its job is to provide the data regardless the source and cache it if needed.
This module has direct data access and it’s allowed to use any platform specific framework or system API. It implements Data handler’s interfaces.
You might wonder where is the code. Before you go diving into it, you must know that this ‘presentation’ is just a summary about how the software is structured, it doesn’t really go deep on details. I will keep updating and adding new documentation to this post in order to cover as much as possible. Check the code here. The code was written using Android Studio originally as an Android project. iOS deployment is powered by MultiOS-Engine.
Writing good software is always difficult. As you might already know, requirements always change and keeping your software clean after so many changes is a hard task. Once your system is abstracted from the details (lower level details: system input output, network communication, etc.) the system doesn’t care how or where the data comes from. Your business logic lives in ignorance, it only knows where to get the data, what to do with it and where to forward it. Keeping good practices, applying well known programming principles and a little bit of common sense, programming smells can be mitigated.
“The job of good system architects is to create a structure whereby the components of the system – whether Use-cases, UI components, database components, or what have you – have no idea how they are deployed and how they communicate with the other components in the system”. Robert C. Martin.