Enforcing Dependency Rule With Custom Detekt Rules
Clean Architecture has been a much-discussed topic in recent years not only in the Android community. You can find many articles about it and most of them are devoted to an explanation and implementation of the Dependency Rule, which is the basis of the Clean Architecture. Implementing the Dependency Rule is quite easy but it is also really easy to violate it and it can be really hard to uncover that on the code review. It would be possible to control it better using package private
visibility or divide app layers properly to separate modules. The problem is that we do not have package private
visibility in Kotlin and layer modularization does not have to be a good or preferred architecture for our app. But don’t worry! We can use help from the static code analysis tool called detekt and I am going to show you how in this article. I expect that you already know and understand Clean Architecture including the Dependency Rule. If you don’t, I recommend this great article from Mario Sanoguera de Lorenzo.
detekt
detekt is a static code analysis tool for Kotlin. No, it’s not a mistake. It is detekt and not Detekt. With the help of detekt we can check compliance with the rules which usually don’t affect functionality but improve code quality. This includes rules for code formatting, naming conventions, performance and so on. You can also write your own custom rules and we will use that for a Dependency Rule check.
For setup of detekt in your project, refer to the official docs. It is not difficult but I want to focus mainly on the custom rule creation in this article.
Use case
In a lot of our apps we use a database as a local persistent storage. The app should only deal with the possibility to store and retrieve data but it should not deal with the specific implementation of the database. If we use Realm database and we later switch to SQLite with Room it should affect only the database layer but not the rest of the app. Let’s say we have the following package structure:
The whole implementation of the database layer of each feature is always in the room
package. ArticlesRepositoryImpl
accesses the database using ArticlesDatabaseSource
interface which declares methods for accepting and returning domain Article
objects. ArticlesRoomDataSource
implements the interface and maps DbArticle
entity to Article
. ArticlesRoomDataSource
is provided to the repository in a Koin module located in the ArticlesModule
. With this package structure we only have to check if we don’t use a class from the room
package or Room library in classes outside of the room
package, excluding ArticlesModule
where the usage is allowed.
Setup
It’s best to implement custom rules in a standalone Kotlin module with the following build.gradle
definition:
We need detekt-api
for writing rules and without detekt-cli
the rules can not be run. This applies for the version 1.5.0. For testing we can use detekt-test
which requires assertj-core
dependency.
Rule definition
For our custom rule definition we need to extend Rule
class like this:
We need to override issue
property which defines the issue. debt
represents the expected time to fix the issue.
Rules are implemented using the Visitor design pattern. There are many methods for different parts of the code which are called on the Rule
instance and enable to check the code. We need to override only the visitImportDirective
method which allows us to check imports in the file. This way we can easily find out if files outside of the room
package don’t contain forbidden database imports. If shouldReport
method returns true
, the file contains a forbidden database import and we report the issue using the report
method.
shouldReport
method looks like this:
It returns true
if shouldReportDbLibraryDependency
or shouldReportDbLayerDependency
returns also true
. shouldReportDbLibraryDependency
checks if the currently analyzed import is from a used database library (Room in our case). If it is then it also checks if the containing file isn’t in the database layer by checking its packageDirective
on presence of room
package and if the containing file doesn’t contain Koin module imports. If it does then it is most probably the file with Koin module declarations and we want to allow this import here. Implementation of shouldReportDbLayerDependency
is almost the same except it checks if the analyzed import is from the room
package or its subpackages.
Providing the rule
When the rule is done, we have to let detekt know about it. It’s done by implementing the RuleSetProvider
interface.
The implementation of this interface can provide detekt with RuleSet
containing our new custom rule. RuleSet
is just a set of related rules and we also have to provide its id. If we implemented another rule related to the Clean Architecture we would put it in this RuleSet
and then we would be able to configure them all at once.
Next we have to let detekt know about our CleanArchRuleSetProvider
. This is done by creating a file io.gitlab.arturbosch.detekt.api.RuleSetProvider
located in the src/main/resources/META-INF/services
folder and the file has to contain a fully qualified name of our provider cz.ackee.cleanarch.detekt.customrules.CleanArchRuleSetProvider
. Now detekt can find our class, instantiate it and retrieve our rule set.
We also need to add our rule to the .yml
configuration and activate it because it is disabled by default
and add the following configuration:
Using the rule
Last thing we need to do is apply our module with the custom rule to all modules in the app where we want to use it. This is done by declaring a dependency on it in the build.gradle
file like this:
Now when we violate our rule and run detekt Gradle task we should see this:
Conclusion
The rule is aimed only at the database layer but you can write rules for checking the networking API layer or other low-level dependencies in a similar way.
The implementation of the rule in this article isn’t 100% reliable of course. You must strictly adhere to the given package structure. The rule also doesn’t discover a violation if you use a fully qualified name of the class instead of the import. It means you still have to partly rely on the discipline of the developers and the code reviews. But even with these limitations detekt can probably do a better job on the code review than most of us. And I think you will probably notice a fully qualified name on the code review.
Rules can be also tested but I skipped this for the sake of simplicity. You can check the whole implementation of the rule including tests and a sample project here.
Thank you for reading this article! I hope you liked it and it will help you achieve a cleaner architecture of your apps! Happy coding! 🙂
Originally published at https://www.ackee.cz on June 5, 2020.