Law of Demeter for Gradle modules
Once you decide to make a reference from a module which someone (co-worker) created, you may meet an error similar to the following.
Cannot access class 'retrofit2.Response'. Check your module classpath
for missing or conflicting dependencies
Why?
The reason it cannot find related dependencies is that the dependency as reference of our project exposes third-party dependency onto its API surfaces. In other words, once any API surfaces contain another dependencies as implementation
dependency and at least one calls to the API surfaces exist, dependency problem occurs like the above.
Let’s say our sample project has a controller to use a repository service in a library. The method we want to use is dependent to Response
class in retofit.
fun findRepositories(username: String): retrofit2.Response<List<Repo>>
Attractive solution
First, as an attractive solution, the problem can be resolved by adding the transitive dependencies if we don’t have modification permission on direct dependency.
// your_project/build.gradle
dependencies {
// ...
implementation "com.squareup.retrofit2:retrofit:${retrofitVersion}"
}
But there are several problems. This is brute-force solution. Until no more dependency problem, we need to add dependencies one by one recursively because we don’t know exactly what dependencies are needed on the target dependency. Moreover, this is bad module design because the client needs to know all the transitive dependencies. Finally the more dependencies to manage, the hard we control the project, which makes the resulting quality poor and the complexity higher.
Second, if we have full control of the dependency, the transitive dependencies can be added as api
in gradle. If you are curious about the difference between implementation
and api
, here is the best official guide to explain it.
// dependency/build.gradle
plugins {
id 'java-library'
}
dependencies {
// ...
api "com.squareup.retrofit2:retrofit:${retrofitVersion}"
}
Law of Demeter
There are tons of articles about Law of Demeter. Briefly explaining, this is a design guideline in order to achieve loose coupling among classes. This advocates
- Each unit should have only limited knowledge about other units: only units “closely” related to the current unit.
- Each unit should only talk to its friends; don’t talk to strangers.
- Only talk to your immediate friends. 1
Our module problem is said to be violation of Law of Demeter. It is decribed exactly in Clean Code2.
G36: Avoid Transitive Navigation
In general we don’t want a single module to know much about its collaborators. More specifically, if A collaborates with B, and B collaborates with C, we don’t want modules that use A to know about C.
Better solution
Again, if you have full control of the library, you can improve it with good class design. To follow the guideline, the client module doesn’t need to know any classes of retrofit. Rethink the purpose of the target method(API). Focus on the things the client really need.
In our sample project, the client only wants the content of the response, which is a list of repositories. It doesn’t care about response status. Moreover the client currently uses the response to determine whether the result is suceesfully fetched.
So the method can return the list of repositories or throw an exception. Simple.
fun findRepositories(username: String): List<Repo>
Now the client can more focus on the structure it is interested in. You can check out full source code of sample project on Github.
With another suggestion, you can create another class wrapped List<Repo>
instead of retrofit2.Response<List<Repo>>
. If the client want some of response information which the library is fine to expose, this approach is preferable. For example, with same objectives (= determines only whether the transaction is successful or not), you can use Optional<List<Repo>>
as the return type.