Enforcing Dependency Rule With Custom Detekt Rules

Jan Mottl
Ackee
Published in
5 min readJun 5, 2020

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:

Sample 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:

Reported issue by detekt

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.

--

--