< Back to articles

Enforcing Dependency Rule With Custom Detekt Rules

Clean Architecture is 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 (https://github.com/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:

Package structure
Use case 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:

apply plugin: 'java-library'  
apply plugin: 'kotlin'  
  
dependencies {  
   implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"  
   compileOnly "io.gitlab.arturbosch.detekt:detekt-api:$detektVersion"  
   implementation "io.gitlab.arturbosch.detekt:detekt-cli:$detektVersion"  
  
   testImplementation 'junit:junit:4.13'  
   testImplementation "io.gitlab.arturbosch.detekt:detekt-test:$detektVersion"  
   // Needed because of detekt-test  
   testImplementation "org.assertj:assertj-core:3.15.0"  
}

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:

class DatabaseImplLeakRule(config: Config = Config.empty) : Rule(config) {  
  
...  
  
   private val KtImportDirective.import: String?  
       get() = importPath?.pathStr  
  
   override val issue = Issue(  
       id = "DatabaseImplLeak",  
       description = "Database implementation leaks outside of the '$DATABASE_LAYER_PACKAGE' package. This violates dependency rule.",  
       severity = Severity.CodeSmell,  
       debt = Debt.FIVE_MINS  
   )  
  
   override fun visitImportDirective(importDirective: KtImportDirective) {  
       if (shouldReport(importDirective)) {  
           report(  
               CodeSmell(  
                   issue = issue,  
                   entity = Entity.from(importDirective),  
                   message = "Importing '${importDirective.import}' which is part of the database implementation."  
               )  
           )  
       }  
   }}

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:

private fun shouldReport(importDirective: KtImportDirective): Boolean {  
  
   return shouldReportDbLibraryDependency(importDirective) || shouldReportDbLayerDependency(importDirective)  
}  
  
private fun shouldReportDbLibraryDependency(importDirective: KtImportDirective): Boolean {  
   return importDirective.import.containsDbLibraryPackagePath()  
           && importDirective.meetOtherRequirementsForReporting()  
}  
  
private fun String?.containsDbLibraryPackagePath() = this?.contains(DATABASE_LIBRARY_PACKAGE_PATH) == true  
  
private fun KtImportDirective.meetOtherRequirementsForReporting(): Boolean {  
   return containingFileIsNotInDbLayer() && containingFileDoesNotContainKoinModules()  
}  
private fun KtImportDirective.containingFileIsNotInDbLayer(): Boolean {  
   return containingKtFile.packageDirective?.qualifiedName?.contains(DATABASE_LAYER_PACKAGE) == false  
}  
private fun KtImportDirective.containingFileDoesNotContainKoinModules(): Boolean {  
   return !containingKtFile.importDirectives.map { it.import }.contains(KOIN_MODULE_PACKAGE_PATH)  
}

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.

class CleanArchRuleSetProvider : RuleSetProvider {  
  
   override val ruleSetId = "clean-arch-rules"  
  
   override fun instance(config: Config) = RuleSet(ruleSetId, listOf(DatabaseImplLeakRule(config)))  
}

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

clean-arch-rules:  
 DatabaseImplLeak:  
   active: true

and add the following configuration:

config:  
 validation: true  
 # when writing own rules with new properties, exclude the property path e.g.: "my_rule_set,.*>.*>[my_property]"  
 excludes: "clean-arch-rules"

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:

dependencies {  
  
   ...  
  
   detekt project(":custom-rules")  
}

Now when we violate our rule and run detekt Gradle task we should see this:

detekt rule violation
detekt detects an error.

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 https://github.com/mottl-jan/clean-arch-detekt.

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! :)

Jan Mottl
Jan Mottl
Android Tech Lead

Are you interested in working together? Let’s discuss it in person!