preface
Modern Android projects are Gradle projects, so we are used to using Gradle Module to divide and organize code, the use of a large number of modules also brings a problem, a large project often dozens of modules, However, when the dependency between a large number of modules is not reasonable, it will still seriously slow down the compilation speed of the project. How to organize Gradle Modules more scientifically is a common requirement in the field of Android development.
Those of you who are Android developers have probably heard of Clean Architecture. Google recommends that you use it for more rational layering of MVVM. The concept of clean architecture comes from this book, and it is safe to say that it is the bible of software architecture.
In addition to optimizing business architectures such as MVVM, the book also produces best practices and methodologies for component design that can be used to optimize engineering architectures such as Gradle. This article discusses how to design our Gradle Module based on various design principles in a clean architecture.
The table of contents is as follows:
- Module granularity division
- Reuse publishing Equivalence Principle (REP)
- Common Closure Principle (CCP)
- Principle of common Reuse (CRP)
- The balance of three principles
- Module dependencies
- Acyclic dependence principle (ADP)
- Stability Dependence Principle (SDP)
- Stability formula
- Stable Abstraction Principle (SAP)
- Abstraction formula
- The relationship between instability and abstraction
- Pain zone and useless zone
- conclusion
Module granularity division
Refer to the Clean Architecture definition for components:
A component is a software deployment unit. It is the smallest entity that can independently deploy the entire software system during the deployment process. For example, in the case of Java, its components are JAR files. In Ruby, they are gem files. In.NET, they are DLL files.
In Android, Gradle Module is the basic unit of publishing JAR or AAR, so Module can be regarded as a component. In terms of Module granularity, we apply the three principles of component division in the book:
- Release Reuse Equivalency Principle
- The Common Closure Principle
- The Common Reuse Principle
Reuse publishing Equivalence Principle (REP)
The minimum granularity of software reuse should be equal to the minimum granularity of its distribution
REP tells us that one of the basic principles of Module partitioning is code reusability, and that when some code is valuable for reuse, it should be considered split into modules that can be distributed independently. In addition, REP requires us to be careful that the Module does not split too much. When we publish an AAR, we need to set a release number for it, not least because without a release number there is no guarantee that the components will be compatible with each other. This is particularly true for androidx components, where we often encounter runtime issues due to inconsistent versions. One of the main reasons for this inconsistency is that components are split too much.
If two components that can be distributed independently are always reused as a whole, the reusable granularity will be larger than the releasable granularity, which increases the probability of version conflict. In this case, you can consider combining them into one and simultaneously release them to avoid version inconsistency.
This is less of an issue in small teams, where artificial conventions ensure that all components are released simultaneously, but in large, cross-team projects, communication costs can be prohibitive if multiple teams work together to upgrade a feature.
Common Closure Principle (CCP)
Component of all the classes for the same kind of the nature of the change should be closed together, that is, a change of impact should be limited within a single component as far as possible, and avoid affecting multiple components at the same time, we should amend the those at the same time for the same purpose of class in the same component, and will not modify those classes at the same time for the same purpose in the different components.
While REP focuses on reusability, CCP emphasizes maintainability, and in many scenarios maintainability is more important than reusability. CCP requires that all classes that can be modified together be grouped together, and that two classes that are always modified together be placed in the same component. This is similar to the SRP (single responsibility) of the familiar SOLID design principle, which requires that functions that always change together should be placed in the same class, and CCP can be thought of as a component version of SRP.
Incidentally: SOLID design principles also come from Clean Architecture, SOLID is for OOP classes and interfaces, and this article deals with larger-grained components.
Some people in Android projects like to divide modules according to the functional properties of the code, such as an MVVM architecture project, the directory might be divided as follows:
+ UI
+ Logic
+ Repository
+ API
+ DB
Copy the code
However, in actual development, UI or Logic are rarely modified only, and most of them are vertically modified around a feature. Such cross-module modification obviously violates CCP principle, so Module division based on business attributes may be more reasonable. For example, the directory structure of a short video application would be
+ VideoPlay
+ ui
+ data
+ ...
+ VideoCreation
+ Account
+ ...
Copy the code
This way, our changes can be done in a closed loop within a single component, reducing the number of affected modules in Gradle compilation and improving compilation speed.
Principle of common Reuse (CRP)
Classes in components should be reused simultaneously, that is, components should not rely on classes that do not participate in reuse
REP requires us to distribute closely related classes together in a component, while CRP requires us to emphasize that unrelated classes are not included and that if they are used, they are used together. Note that “common” reuse does not mean that all classes are externally accessible; some classes may serve other classes internally, but they are essential. We don’t want to over-split components, but at the same time we don’t want over-redundancy in the classes of components. We don’t want people to rely on certain classes of components and not others.
+ VideoPlay
+ VideoCreation
+ Account
+ ...
Copy the code
For example, VideoCreation bears the functions related to short VideoCreation. The short VideoCreation link is divided into shooting and editing. In some scenes, the user selects materials through the album and directly enters editing, so the shooting module may not be needed, so the coexistence of shooting and editing modules and one component does not conform to CRP principle. When part of the code changes, components that rely only on the edit module are involved in the compilation. At this point, consider splitting VideoCreation into VideoRecord and VideoEdit modules
In the sense that CRP is similar to SOLID’s ISP (interface Isolation Principle), which refers to unexposed interfaces, it can be called a component VERSION of an ISP.
The balance of three principles
The above three principles are mutually exclusive. REP and CCP are glue principles that tell us which classes to put together, which makes the component bigger. CRP is the rule of exclusion, and unneeded classes are removed from components, making them smaller. The important task of component design is to strike a balance between these three principles
The central idea of REP, CCP and CRP is to pursue reasonable cohesion within components, but they have different emphases, and it is difficult to take into account all three at the same time. If they are not properly considered, they will fall into the dilemma of pushing the bottle. If you just follow REP and CCP and ignore CRP, you’re relying on too many unused components and classes, and changes to those components or classes can lead to too many unnecessary releases of your own components. Following REP and CRP while ignoring CCP, because the component split is too fine, a requirement change may have to change N components, and the cost is also huge. If only following CCP and CRP but ignoring REP, the component capability is too vertical, and the reusability of the underlying capability is sacrificed
It is difficult to find a general conclusion on Gradle Module granularity. We should make trade-offs and trade-offs among the three principles by considering various factors such as project type and project stage. For example, in the early stage of the project, we paid more attention to business development and maintenance efficiency, so CCP was more important than REP. However, with the development of the project, the reusability of underlying capabilities might be considered. REP became more important. At this point, CRP should be used to reasonably split and reconstruct the components.
Module dependencies
In granularity division, what we pursue is how to maintain reasonable cohesion of components. The sorting of dependencies between components helps to better maintain external coupling. There are also three principles of component coupling design in Clean Architecture:
- The Acyclic Dependencies Principle
- The Stable Dependencies Principle
- The Stable Abstractions Principle
Acyclic dependence principle (ADP)
Component dependency diagrams should not contain rings. The diagram should be a directed acyclic diagram (DAG). Circular dependencies between modules can increase the impact of component changes and increase the overall compilation cost.
For example, in A ring dependence such as A -> B -> C -> A, since C depends on A and B depends on C, the change of A will affect BOTH B and C, and the change of any point in the dependence ring will affect other nodes on the ring. Imagine if there were no dependence of C -> A, changes in C would only affect B, B would only affect A, and changes in A would not affect anyone.
Fortunately, we don’t have to worry about circular dependent modules in Gradle. If there are rings between modules, Gradle will alert you at compile time. If there are rings between modules, Gradle will alert you at compile time. If there are rings between modules, Gradle will alert you at compile time.
- Dependency inversion
With the help of the dependency inversion principle (DIP) in SOLID, the dependency content of C > A is abstracted into the interface in C. C programs for the interface, and then lets A implement these interfaces, and the dependency relationship is reversed
- Increase the component
With the addition of D component, the dependency of C > A sinks to D, making C and A co-dependent on D, similar to the intermediary design pattern.
Of course, this approach, if abused, will lead to component expansion of the project, so whether to sink A D component from A should be considered in combination with the REP and CCP principles introduced above.
Stability Dependence Principle (SDP)
For example, if A depends on B, the dependent party B should be more stable than the dependent party A.
The PRINCIPLE of SDP is well understood. If A is A common component that needs to maintain high stability, and if it relies on A component B that changes frequently, it will become unstable due to the change of B. In order to ensure the stability of A, the modification of B will become fearful and difficult to carry out. A component that is expected to change frequently is an unstable component. This definition is too subjective. How can you objectively measure the stability of a component?
Stability formula
Stability can be measured by two indicators: how many components a component depends on (inbound dependence) and how many components it depends on (outbound dependence) :
- Fan-in: The number of reverse dependencies that depend on this component. The greater the value, the greater the responsibility of the component.
- Fan-out: The number of other components that this component positively depends on. The larger this value is, the less independent and naturally more unstable this component is.
- Instability: I(Instability) = fan-out/(Fan-in+Fan-out)
The smaller the value, the more stable the component is:
- When fan-out == 0, this component does not depend on any other component, but other components depend on it. At this point, its I = 0 is the most stable component, and we don’t want to change it easily, because once it changes, other components that depend on it will also be affected.
- When fan-in == 0, this component is not dependent on any other component, but depends on other components. In this case, its I = 1 is the most unstable component. Changes of the components it depends on May affect itself, but it is free to change itself without affecting other components
Note: inbound and out to the sometimes referred to as reverse dependence (Ca) and positive dependence (Ce), nature is one thing: en.wikipedia.org/wiki/Softwa…
Again, take the engineering structure of short video applications as an example:
+ app # host + play # video + UI + data + creation # video + UI + data + common + DB + NET + Camera + cache + infra # Basic framework, etcCopy the code
infra
Is unstable
- Fan-in = 5
- Fan-out = 0
- I = 0/(5+0) = 0
Copy the code
Instability 0, infra is a very stable Module, it can not be modified without authorization
app
Is unstable
- Fan-in = 0
- Fan-out = 3
- I = 3 / ( 0 + 3 ) = 1
Copy the code
Instability degree 1: APP is an extremely unstable Module, and any change of a Module will affect it.
A relatively healthy engineering structure, the direction of the arrow must conform to the SDP principle, unstable components flow to stable components, APP and infra in the overall structure conform to this principle. From the internal view of Module: THE instability of UI is higher than that of DATA, which is also in line with the objective display of UI side requirements and easier to change.
Then analyze the dependency relationship between the two modules VideoPlay and VideoCreation. Assume that in order to encourage user creation, the application adds creation entry during video playback, so VideoPlay is dependent on VideoCreation
- VideoPlay instability
-fan-in = 1 -fan-out = 2 -i = 2 / (1+2) = 0.66Copy the code
- VideoCreation instability
-fan-in = 2-fan-out = 5 -i = 5 / (2+5) = 0.7Copy the code
VideoCreatioin is slightly more unstable than VideoPlay, which is contrary to SDP principles. We made VideoPlay rely on VideoCreation directly to meet the demand at the product level, but it was not a good design at the engineering level. Please refer to the following for solutions.
Stable Abstraction Principle (SAP)
The abstraction of a component should be consistent with its stability; the more stable the component, the more abstract it should be.
At the heart of SOLID is the open closed principle (OCP) : code should be open for extension and closed for modification. We often practice OCP by programming toward abstract classes/interfaces, building the framework with abstractions and extending the details with implementations. The abstraction layer contains the basic protocol and top-level design of the program. This code should not change frequently, so it should be placed in stable components (I=0), while unstable components (I=1) are suitable for implementation parts that can be quickly and easily modified.
SAP establishes a correlation between the stability and abstraction of components. When a stable component needs to change, it should avoid modifying itself and implement the change through the extension of its derived classes, which requires stable components to have good abstraction ability. As for the implementation part of abstract classes, it should be removed from stable components and put into unstable components, so that the code can be modified without pressure without worrying about affecting others.
Abstraction formula
- Nc: the number of classes in the component
- Na: The number of abstract classes and interfaces in a component
- A: Degree of abstraction, A = Na/Nc
The value of A ranges from 0 to 1, where A larger value indicates A higher degree of abstraction within the component. 0 indicates that there are no abstract classes in the component, and 1 indicates that the component has only abstraction without any implementation.
As in the figure above, since infra is extremely stable, it should have a level of abstraction to match. Taking the capability of data layer as an example, we removed all implementations of data layer from infra to Common, leaving only abstract interfaces. The :data of other Modules only relies on stable infra, while APP is responsible for global injection of DB, NET, cache and other concrete implementations.
In addition, since there is no stripping of abstractions and implementations in VideoCreation, modifications to the VideoCreation implementation may undermine the stability it should be.
Based on SAP principles, we added a highly abstract creation: API, which has high stability and high abstraction, while VideoCreation has reduced stability and is responsible for providing a concrete implementation of the API.
The relationship between instability and abstraction
The relationship between component instability (I) and abstraction (A) can be seen in the following figure:
The vertical axis is the A value (the higher the value, the more abstract) and the horizontal axis is the I value (the higher the value, the more unstable). Based on the PRINCIPLE of SAP, a healthy component should be as close to the Main Sequence as possible. We use Distance from the Main Sequence (D) to evaluate the balance between the abstraction degree and stability of components: D is equal to abs(A+I) minus 1. The smaller the value, the more balanced the abstraction and stability of the component. A component located at (A = 1, I = 0) is extremely stable and completely abstract, and A component located at (A = 0, I = 1) is completely representational and extremely unstable.
Pain zone and useless zone
The component located at the lower left corner of the coordinate cannot be modified at will due to its high stability requirements. However, because its code abstraction is low and cannot be modified through extension, it can only be modified itself once there is an upgrade requirement. This contradiction between the need to modify and the inability to modify has led to the area being called the Zone Of Pain.
For example, the Common part in the previous example, if it is directly dependent on as a public module, needs to have a very high stability. However, due to its internal concrete implementation, when we want to upgrade db or NET and other public libraries, we often need to carry out a comprehensive regression test for the program due to the large impact range. Therefore, we do not allow Common to be too dependent, which reduces its stability and reduces its burden when changes occur. When changes occur, we only need to complete unit tests against the infra interfaces on which it depends, avoiding the cost of regression testing.
A component in the upper right corner Of a coordinate is highly unstable meaning that it is not dependent on other components, so it is meaningless to do any abstraction in this part, hence it is also called a Zone Of Useless. This kind of code is usually due to historical reasons, for example, we often see some abstract class left in a corner that is not implemented, such useless code should be removed.
A healthy component should be as far away from the pain and waste zones as possible, and as close to the main sequence as possible.
conclusion
Before we wrap up, let’s take a look at our short video application with a clean architecture optimization
In addition to making the stability and abstraction of VideoPlay more consistent by adding creation: API as described above, we also adjusted the position of camera, and the camera before adjustment was in Common. However, its modification is independent and dependent only on VideoCreation. Firstly, it does not conform to CRP principle, and secondly, camera often upgrades with VideoCreation’s requirements, which also does not conform to CCP’s requirements. So we pulled the camera out of Common and moved it to VideoCreation
Finally, we added a creation: Common. Because VideoCreation relies on infra in many places, although infra is extremely stable component, it is still unreliable as an external component. Once infra changes, the stability of VideoCreation will be affected. Creation: Common will reduce its dependence on infra and improve the stability of VideoCreation. The instability of each component after optimization is shown in the following table:
app
- Fan-in = 0
- Fan-out = 7
- I = 7 / (0 + 7) = 1
Copy the code
VideoCreation
-fan-in = 1 -fan-out = 2 -i = 2 / (1 + 2) = 0.66Copy the code
VideoPaly
-fan-in = 1 -fan-out = 1 -i = 1 / (1 + 1) = 0.5Copy the code
Common
-fan-in = 3-fan-out = 3-i = 3 / (3 + 3) = 0.5Copy the code
infra
- Fan-in = 6
- Fan-out = 0
- I = 0 / (0 + 6) = 0
Copy the code
The degree of instability (I) decreases layer by layer, and the degree of abstraction also increases gradually. The examples in this article are so simple that one would surely think that this level of optimization is intuitive and doesn’t need to follow a formula. But real projects tend to be much more complex, and knowing these formulas can help guide a complex scenario and keep us from getting lost.
As a final conclusion, Gradle Module is a component unit of Android project. We can govern it based on the principles of component design in clean architecture:
- All and only closely related classes or modules should be placed in the same component
- Components that need to be modified at the same time for the same purpose should be kept together as much as possible
- How should component granularity be divided, with trade-offs based on the actual situation
- There should be no circular dependencies between components, which can be resolved by dependency inversion or by adding components
- A dependent component is always more stable than a dependent component
- The stability and abstraction of components should be consistent; the more stable the organization, the higher the abstraction