Principles of building a multi-module Android application and their application in SberHealth
Hi all! My name is Alexander. I am an Android developer at SberHealth.
There are a lot of articles about “clean” architecture and multimodularity. But not many companies are ready to share their experience and the results obtained from the implementation of these practices. I want to try to fix the situation.
In this article, I will talk about the principles of building a multi-module application, how we apply them when developing the SberHealth mobile application for Android, and what it gives us.
Yes, this article is also about multimodularity, but do not rush to scroll through, because the principles described below can be applied not only in mobile development, but also in software development in general.
Let’s get started.
Clean architecture and its purpose
In the classical version, the application architecture is a set of modules and links between them that provide the functionality of the software. But in practice, it is not enough to assemble and combine components – without first working out a clear hierarchy, dependencies and organization of interaction, such development is doomed to failure. Don’t believe? Tell that to the hundreds of developers who have made similar mistakes and are now struggling to maintain, scale, and update their software.
To prevent any difficulties, when developing applications (both for Android and other systems), it is advisable to adhere to the principles of clean architecture proposed by Robert S. Martin.
Definitions clean architecture a lot, but I tried to highlight the most significant for myself.
Clean architecture is a set of correct decisions about software organization that we would like to apply at different stages of working on a project: from choosing structural elements and their interfaces to determining scenarios for interacting with an application.
Purpose of clean architecture – reduce human labor costs when creating and maintaining the system. A well-organized architecture simplifies testing, maintenance, modification, development, and deployment, and ensures independence.
Predecessors of Clean Architecture
Robert S. Martin’s recommendations for organizing a clean architecture are based on the ideas and principles of previous architectures that can be considered predecessors. There are several of them:
Hexagonal Architecture (Hexagonal architecture)also known as port and adapter architecture. Involves creating loosely coupled application components that can be easily connected to their software environment using ports and adapters. This makes components interchangeable at any level and simplifies test automation.
Onion Architecture (Onion Architecture). It implies the division of the application into layers. With such an organization, the first level, which is in the center, is independent. But the second depends on it, the third depends on the second, the fourth depends on the third, and so on.
Data-Context-Interaction (Data, Context, Interaction). A paradigm used to program systems of interacting objects. Involves improving the readability of object-oriented code, as well as clear separation of code to quickly change the behavior of the system. The paradigm separates data from context and interaction.
Boundary-Control-Entity (Border, Control, Entity). It is an approach to object modeling based on a three-factor representation of classes. In a properly designed package hierarchy, an actor can only interact with Boundary objects, Entity objects can only interact with Control objects, and Control objects can interact with objects of any type. The main advantage of the BCE approach is the grouping of classes in the form of hierarchical levels. This contributes to a better understanding of the model and reduces its complexity.
All architectures on which a pure architecture is based have a common feature – the goal is to divide software into layers. At the same time, in each of the options, separate levels are provided for business rules, user and system interface – it is desirable to separate them.
For ease of understanding, the application can be represented as a house, and the components as its rooms, that is, the smallest parts of the whole. In development, components are the smallest entities that can be deployed as part of a system. In java and kotlin, these are jar files, in android, aar and modules.
For example, in our Android application, the smallest part (component) is modules.
Now let’s look at the principles of component organization.
I’ll get ahead a little – we use api, impl approach, which is well described in the article Once again about the multimodularity of Android applications. The article also contains a link to a git repository with an example.
There are two principles for building components that should be followed:
connectivity of components;
Let’s take a closer look at each of them.
Connectivity of components and principles that define it
The connectivity of components determines which component a particular class belongs to. This decision should be made in accordance with good software engineering principles. But it must be clarified that often such a choice depends on the context.
The connectivity of components is determined by three principles:
REP (Release Equivalence Principle) — the principle of equivalence of reuse and releases.
The principle states that the classes or modules that make up a component must belong to a related group. That is, classes that are combined into a component must be released together. In our case, core modules are a good example, in which classes are combined according to a common feature: working with resources, networking, and so on.
CCP (Common Closure Principle) — the principle of coordinated change.
Involves the union of all classes that may need to be changed for one common reason. That is, if two classes are so closely related (physically or conceptually) that they will change together, they must belong to the same component.
CRP (Common Reuse Principle) — the principle of shared reuse.
The principle helps determine which classes should be included in the component. At the same time, its main concept is not to force the users of the component to depend on the unnecessary. CRP is a generalized version of the interface separation principle (ISP) from SOLID, which advises against creating dependencies on interfaces whose methods do not use.
According to the CRP principle, you should not create dependencies on components that have unused classes, interfaces, and in general don’t create dependencies on anything unused.
It is noteworthy that the principles contradict each other. So:
Principles REP And CCP – inclusive. They aim to make your component bigger.
Principle CRP on the contrary, exceptional. It aims to make your components as small as possible.
From here we have a diagram of the contradictions of the connectedness principles, which shows their influence on each other. The edges in the diagram describe the cost of violating the principle at the opposite vertex.
The diagram allows you to find a middle ground that will meet the current needs of developers. But you need to remember – look for a balance based on the needs of the application.
Compatibility of components
Now let’s deal with the compatibility of components. Here it is already more interesting than to scatter classes according to logical connections.
Compatibility of components is the relationship between them. To understand what it is, three principles must be disassembled.
ADP (Acyclic Dependency Principle) – the principle of acyclicity of dependencies.
According to the principle, cycles in a dependency graph are not allowed. Here you need to consider the diagram below.
The diagram shows a cyclic relationship: feature-two uses feature-one, feature-one uses feature-three, A feature-three uses feature-two.
To ensure acyclicity in such a scheme, it is necessary that each component can work independently. At the same time, if some component should depend on another, then it is necessary to break the cycle. To do this, you need to create a new module and use the principle of DIP (dependency inversion)
SDP (Stable Dependencies Principle) — the principle of sustainable dependencies.
According to the SDP, dependencies should be directed towards sustainability. At the same time, a stable component is considered to be one on which many other components depend, since it requires more effort and coordination to change it – this component is independent. The scheme shows a stable component X. The arrows show that other components depend on it.
For example, in our Android application, the following are considered sustainable: core components.
In the case of unstable components, the opposite is true – the unstable Y component (consider this feature component) depends on many others.
It is important to note that not all components need to be stable – if all components in a system are stable, it cannot be changed.
SAP (Stable Abstraction Principle) – the principle of stable abstractions.
According to SAP, the robustness of a component is proportional to its abstraction. SAP and the previously mentioned SDP together comply with the Dependency Inversion Principle (DIP) for components. Thus, the SDP principle requires that dependencies be directed towards sustainability, and SAP requires that sustainability imply abstraction. In our case, the module feature-one-api fully corresponds to two principles: it is stable, since it does not depend on anyone, and it is abstract, since there is no implementation in it.
An example of packaging components for functionality in the SberHealth app.
In our case, the scheme contains an unstable feature module feature-one-impl. It depends on two stable ones:
a lightweight feature-one-api, which for example contains an interface for loading our functionality, which can be loaded from different points in the application
Additionally, if required, external dependencies as abstractions are also included in the schema.
I also note that for the sake of convenience and stability, external dependencies of the functionality that are used in several modules are taken out by us in the core-feature modules
An example of architecture inside a functional module in the SberHealth Android app
There is a standard scheme and dependency rules, for example, inside functional modules.
The rules define how the layers should be separated and how the communication between them should take place using dependency inversion (DIP) from SOLID.
The following diagram shows how communication between layers is established in the SberHealth Android application.
You can see that we are breaking the dependency rule. This is done because we believe that it is redundant to produce interfaces in a completely isolated impl feature module. Especially considering that it is as unstable as possible and everything in it is marked with the internal modifier.
But there are also exceptions here – if there are functional dependencies that need to be used in other components, we use dependency inversion (DIP) and add such dependencies to api or core-feature modules of functionality that are as unstable as possible.
Results of our case
In our project, we adhere to both the recommendations of clean architecture and the principles of modularization. This gives us a number of benefits:
Independent development. Each command works in its own functional module, without affecting other parts of the application and without conflicting with each other.
Architecture scalability. We can expand our system as we please without any harm to it.
Testing speed. We can test each component separately instead of running tests throughout the application. It is simpler, more accurate and speeds up Time to market.
Project build speed. Due to api modules, our project will not be completely rebuilt, since we add changes to the impl module, and we have it as unstable as possible, that is, it is not added as a dependency to other modules (except for the main app module, which knows about all the modules of our application ).
But we must understand that a clean architecture is not the final implementation of the architecture, but a property. Therefore, its provision is an important but continuous process.
Can you call your Android app modular? What did modularization give in your case? Share your experience!