Refactoring Multi-Module Kotlin Project With Konsist
Tags: konsist, android, spring, kotlin
Published: 2023-09-04 15:22:56


## Refactoring Multi-Module Kotlin Project With Konsist

![](https://cdn-images-1.medium.com/max/6816/1*BaZpGNdB3_NCzLMtdnS3MA.png)

Refactoring is not just about altering code; it’s about enhancing its structure, improving readability, optimizing performance, and keeping things consistent. In this article, we’ll focus on the consistency aspect and refactor a simple imaginary project to unify its codebase.

We will also implement a set of guards to keep things **K**onsistent in the future. To achieve this we will utilise the [Konsist](https://github.com/LemonAppDev/konsist), the Kotlin architectural linter.
>  Read [Introduction to Konsist](https://blog.kotlin-academy.com/introducing-konsist-a-cutting-edge-kotlin-linter-d3ab916a5461).

## Base Project

Typical projects are complex, they contain many types of classes/interfaces heaving various responsibilities (views, controllers, models, use cases, repositories, etc.). These classes/interfaces are usually spread across different modules and placed in various packages. Refactoring such a project would be too much for a single article, so we will refactor with a starter project heaving 3 modules and 4 use cases — the imaginary MyDiet application.

>  If you prefer learning by doing you can follow the article steps. Just check out the [repository](https://github.com/LemonAppDev/konsist-article-refactoring-multi-module-project), Open the starter project in the [InteliJ IDEA](https://www.jetbrains.com/idea/) (`idea-mydiet-starter`) or [Android Studio](https://developer.android.com/studio) (`android-studio-mydiet-starter`). To keep things simple this project contains a set of classes to be verified and refactored, not the full-fledge app.

The MyDiet application has feature 3 modules:

* featureCaloryCalculator

* featureGroceryListGenerator

* featureMealPlanner

![](https://cdn-images-1.medium.com/max/2000/1*9LOlQ2qq3Ca6PFDv8s8adw.png)

Each feature module has one or more use cases. Let’s look at the familiar *project view* from [IntelliJ IDEA](https://www.jetbrains.com/idea/) to get the full project structure:

![](https://cdn-images-1.medium.com/max/2000/1*vl7ZFq5V_3cOFhkc_4zk9A.png)

Let’s look at the content of each use case classes across all feature modules:

```kotlin
    // featureCaloryCalculator module
    class AdjustCaloricGoalUseCase {
        fun run() {
            // business logic
        }
    
        fun calculateCalories() {
            // business logic
        }
    }
    
    class CalculateDailyIntakeUseCase {
        fun execute() {
            // business logic
        }
    }
    
    // featureGroceryListGenerator module
    class CategorizeGroceryItemsUseCase {
        fun categorizeGroceryItemsUseCase() {
            // business logic
        }
    }
    
    // featureMealPlanner module
    class PlanWeeklyMealsUseCase {
        fun invoke() {
            // business logic
        }
    }
```

Use case holds the business logic (for simplicity represented here as a comment in the code). At first glance, these use cases look similar but after closer examination, you will notice that the use case class declarations are inconsistent when it comes to method names, number of public methods, and packages. Most likely because these use cases were written by different developers prioritizing developer personal opinions rather than project-specific standards.
>  Exact rules will vary from project to project, but Konsist API can still be used to define checks tailored for a specific project.

Let’s write a few Konsist tests to unify the codebase.

## Guard 1: Unify The UseCase Method Names

Let’s imagine that this is a large-scale project containing many classes in each module and because each module is large we want to refactor each module in isolation. Per module, refactoring will limit the scope of changes and will help with keeping Pull Request smaller. We will focus only on unifying use cases.

The first step of using Konsist is creation of the scope (containing a list of Kotlin files) present in a given module:

```kotlin
    Konsist
        .scopeFromModule("featureCaloryCalculator") // Kotlin files in featureCaloryCalculator module
```

Now we need to select all classes representing use cases. In this project use case is a class with UseCase name suffix ( .withNameEndingWith(“UseCase”)).

```kotlin
    Konsist
        .scopeFromModule("featureCaloryCalculator")
        .classes()
        .withNameEndingWith("UseCase")
```

>  In other projects use case could be represented by the class extending BaseUseCase class (`.withAllParentsOf*(BaseUseCase::class)`) or every class annotated with the @UseCase annotation (`.withAllAnnotationsOf(BaseUseCase::class)`).

Now define the assert containing desired checks (the last line of the assert block always has to return a boolean). We will make sure that every use case has a public method with a unified name. We will choose the invoke as a desired method name:

```kotlin
    Konsist
        .scopeFromModule("featureCaloryCalculator")
        .classes()
        .withNameEndingWith("UseCase")
        .assert {
            it.containsFunction { function ->
                function.name == "invoke" && function.hasPublicOrDefaultModifier
            }
        }
```

Notice that our guard treats the absence of visibility modifier as `public`, because it is a default Kotlin visibility.
>  If you would like always heave an explicit public visibility modifier you could use `hasPublicModifier` property instead.

To make the above check work we need to wrap it in [JUnit](https://junit.org/) test:

```kotlin
    @Test
    fun `featureCaloryCalculator classes with 'UseCase' suffix should have a public method named 'invoke'`() {
        Konsist
            .scopeFromModule("featureCaloryCalculator")
            .classes()
            .withNameEndingWith("UseCase")
            .assert {
                it.containsFunction { function ->
                    function.name == "invoke" && function.hasPublicOrDefaultModifier
                }
            }
    }
```

>  If you are following with the project add this test to app/src/test/kotlin/UseCaseKonsistTest.kt file. To run Konsist test [click on the green arrow](https://www.jetbrains.com/help/idea/performing-tests.html) (left to the test method name).

After running Konsist test it will complain about lack of the method named invoke in the `AdjustCaloricGoalUseCase` and `CalculateDailyIntakeUseCase` classes (`featureCaloryCalculator` module). Let’s update method names in these classes to make the test pass:

```kotlin
    // featureCaloryCalculator module
    // BEFORE
    class AdjustCaloricGoalUseCase {
        fun run() {
            // business logic
        }
    
        fun calculateCalories() {
            // business logic
        }
    }
    
    class CalculateDailyIntakeUseCase {
        fun execute() {
            // business logic
        }
    }
    
    // AFTER
    class AdjustCaloricGoalUseCase {
        fun invoke() { // CHANGE: Name updated
            // business logic
        }
    
        fun calculateCalories() {
            // business logic
        }
    }
    
    class CalculateDailyIntakeUseCase {
        fun invoke() { // CHANGE: Name updated
            // business logic
        }
    }
```

The next module to refactor is the `featureGroceryListGenerator` module. Again we will assume that this is a very large module containing many classes and interferes. We can simply copy the test and update the module names:

```kotlin
    @Test
    fun `featureCaloryCalculator classes with 'UseCase' suffix should have a public method named 'invoke'`() {
        Konsist
            .scopeFromModule("featureCaloryCalculator")
            .classes()
            .withNameEndingWith("UseCase")
            .assert {
                it.containsFunction { function ->
                    function.name == "invoke" && function.hasPublicOrDefaultModifier
                }
            }
    }
    
    @Test
    fun `featureGroceryListGenerator classes with 'UseCase' suffix should have a public method named 'invoke'`() {
        Konsist
            .scopeFromModule("featureGroceryListGenerator")
            .classes()
            .withNameEndingWith("UseCase")
            .assert {
                it.containsFunction { function ->
                    function.name == "invoke" && function.hasPublicOrDefaultModifier
                }
            }
    }
```

The above approach works, however it leads to unnecessary code duplication. We can do better by creating two scopes for each module and add them:

```kotlin
    @Test
    fun `classes with 'UseCase' suffix should have a public method named 'invoke'`() {
        val featureCaloryCalculatorScope = Konsist.scopeFromModule("featureCaloryCalculator")
        val featureGroceryListGeneratorScope = Konsist.scopeFromModule("featureGroceryListGenerator")
        
        val refactoredModules = featureCaloryCalculatorScope + featureGroceryListGeneratorScope
    
        refactoredModules
            .classes()
            .withNameEndingWith("UseCase")
            .assert {
                it.containsFunction { function ->
                    function.name == "invoke" && function.hasPublicOrDefaultModifier
                }
            }
    }
```

>  Addition of scopes is possible because KoScope overrides Kotlin `plus` and `plusAssign` operators. See [Create The Scope](https://docs.konsist.lemonappdev.com/writing-tests/koscope) for more information.

This time the Konsist test will fail because the `CategorizeGroceryItemsUseCase` class present in the `featureGroceryListGenerator` module has an incorrect name. Let’s fix that:

```kotlin
    // featureGroceryListGenerator module
    // BEFORE
    class CategorizeGroceryItemsUseCase {
        fun categorizeGroceryItemsUseCase() {
            // business logic
        }
    }
    
    // AFTER
    class CategorizeGroceryItemsUseCase {
        fun invoke() { // CHANGE: Name updated
            // business logic
        }
    }
```

The test is passing. Now we have the last module to refactor. We can add another scope representing Kotlin files in the `featureMealPlanner` module:

```kotlin
    @Test
    fun `classes with 'UseCase' suffix should have a public method named 'invoke'`() {
        val featureCaloryCalculatorScope = Konsist.scopeFromModule("featureCaloryCalculator")
        val featureGroceryListGeneratorScope = Konsist.scopeFromModule("featureGroceryListGenerator")
        val featureMealPlannerScope = Konsist.scopeFromModule("featureMealPlanner")
    
        val refactoredModules = 
                 featureCaloryCalculatorScope + 
                 featureGroceryListGeneratorScope +
                 featureMealPlannerScope
    
        refactoredModules
            .classes()
            .withNameEndingWith("UseCase")
            .assert {
                it.containsFunction { function ->
                    function.name == "invoke" && function.hasPublicOrDefaultModifier
                }
            }
    }
```

Notice that the `featureMealPlanner` module is the last module for this particular refactoring, so we can simplify the above code. Rather than creating 3 separate scopes (for each module) and adding them ,we can verify all classes present in the production source set (main) by using `Konsist.scopeFromProject()`:

```kotlin
    @Test
    fun `classes with 'UseCase' suffix should have a public method named 'invoke'`() {
        Konsist
            .scopeFromProject()
            .classes()
            .withNameEndingWith("UseCase")
            .assert {
                it.containsFunction { function ->
                    function.name == "invoke" && function.hasPublicOrDefaultModifier
                }
            }
    }
```

This time the test will succeed, because the `PlanWeeklyMealsUseCase` class present in the `featureMealPlanner` module already has a method named `invoke`:

```kotlin
    class PlanWeeklyMealsUseCase {
        fun invoke() { // INFO: Already had correct method name
            // business logic
        }
    }
```

Let’s improve our rule.

## Guard 2: Use Case Has Only One Public Method

To verify if every use case present in the project has a single public method we can check the number of public (or default) declarations in the class by using `it.numPublicOrDefaultDeclarations() == 1`. Instead of writing a new test we can just improve the existing one:

```kotlin
    @Test
    fun `classes with 'UseCase' suffix should have single public method named 'invoke'`() {
      Konsist.scopeFromProject()
          .classes()
          .withNameEndingWith("UseCase")
          .assert {
              val hasSingleInvokeMethod = it.containsFunction { function ->
                  function.name == "invoke" && function.hasPublicOrDefaultModifier
              }
    
              val hasSinglePublicDeclaration = it.numPublicOrDefaultDeclarations() == 1
    
              hasSingleInvokeMethod && hasSinglePublicDeclaration
          }
    }
```

After running this konsist test we will realise that the `AdjustCaloricGoalUseCase` class has two `public` methods. To fix we will change visibility of the `calculateCalorie`s method to `private` (we assume it was accidentally exposed):

```kotlin
    // featureCaloryCalculator module
    // BEFORE
    class AdjustCaloricGoalUseCase {
        fun run() {
            // business logic
        }
    
        fun calculateCalories() {
            // business logic
        }
    }
    
    // AFTER
    class AdjustCaloricGoalUseCase {
        fun invoke() {
            // business logic
        }
    
        private fun calculateCalories() { // CHANGE: Visibility updated
            // business logic
        }
    }
```

## Guard 3: Every Use Case Resides in “domain.usecase” package

You may not have noticed yet, but use case package structure is a bit off. Two use cases `AdjustCaloricGoalUseCase` and `CalculateDailyIntakeUseCase` classes resides in the `com.mydiet` package, `CategorizeGroceryItemsUseCase` class resides in the `com.mydiet.usecase` package(no `s` at the end) and `PlanWeeklyMealsUseCase` class resides in the `com.mydiet.usecases` package (s at the end):

![](https://cdn-images-1.medium.com/max/2000/1*3vjed1WSO3c7izV8ekFQrA.png)

We will start by verifying if the desired package for each use case is `domain.usecase package` (prefixed and followed by an number of packages). Updating package names is quite straight forward task so this time we will define guard for all modules and fix all violations in one go. Let’s write a new Konsist test to guard this standard:

```kotlin
    @Test
    fun `classes with 'UseCase' suffix should reside in 'domain', 'usecase' packages`() {
        Konsist.scopeFromProduction()
            .classes()
            .withNameEndingWith("UseCase")
            .assert { it.resideInPackage("..domain.usecase..") }
    }
```

>  Two dots `..` means zero or more packages.

The test highlighted above will now fail for all use cases because none of them reside in the correct package (none of them reside in the `domain` package). To fix this we have to simply update the packages (class content is omitted for clarity):

```kotlin
    // BEFORE
    // featureCaloryCalculator module
    package com.mydiet
    class AdjustCaloricGoalUseCase { /* .. */ }
    
    package com.mydiet
    class CalculateDailyIntakeUseCase{ /* .. */ }
    
    // featureGroceryListGenerator module
    package com.mydiet.usecase
    class CategorizeGroceryItemsUseCase { /* .. */ }
    
    // featureMealPlanner module
    package com.mydiet.usecases
    class PlanWeeklyMealsUseCase { /* .. */ }
    
    // AFTER
    // featureCaloryCalculator module
    package com.mydiet.domain.usecase // CHANGE: Package updated
    class AdjustCaloricGoalUseCase { /* .. */ }
    
    package com.mydiet.domain.usecase // CHANGE: Package updated
    class CalculateDailyIntakeUseCase{ /* .. */ }
    
    // featureGroceryListGenerator module
    package com.mydiet.domain.usecase // CHANGE: Package updated
    class CategorizeGroceryItemsUseCase { /* .. */ }
    
    // featureMealPlanner module
    package com.mydiet.domain.usecase // CHANGE: Package updated
    class PlanWeeklyMealsUseCase { /* .. */ }
```

Now Konsist tests will succeed. We can improve package naming even more. In a typical project every class present in a feature module would have a package prefixed with the feature name to avoid class redeclaration across different modules. We can retrieve module name (`moduleName`), and remove `feature` prefix to get the name of the package. Let’s improve existing test:

```kotlin
    @Test
    fun `classes with 'UseCase' suffix should reside in feature, domain and usecase packages`() {
        Konsist.scopeFromProduction()
            .classes()
            .withNameEndingWith("UseCase")
            .assert {
                /*
                module -> package name:
                featureMealPlanner -> mealplanner
                featureGroceryListGenerator -> grocerylistgenerator
                featureCaloryCalculator -> calorycalculator
                */
                val featurePackageName = it
                  .containingFile
                  .moduleName
                  .lowercase()
                  .removePrefix("feature")
    
                it.resideInPackage("..${featurePackageName}.domain.usecase..")
            }
    }
```

And the final fix to update these packages once again:

```kotlin
    // BEFORE
    // featureCaloryCalculator module
    package com.mydiet.domain.usecase
    class AdjustCaloricGoalUseCase { /* .. */ }
    
    package com.mydiet.domain.usecase
    class CalculateDailyIntakeUseCase{ /* .. */ }
    
    // featureGroceryListGenerator module
    package com.mydiet.domain.usecase
    class CategorizeGroceryItemsUseCase { /* .. */ }
    
    // featureMealPlanner module
    package com.mydiet.domain.usecase
    class PlanWeeklyMealsUseCase { /* .. */ }
    
    // AFTER
    // featureCaloryCalculator module
    package com.mydiet.calorycalculator.domain.usecase // CHANGE: Package updated
    class AdjustCaloricGoalUseCase { /* .. */ }
    
    package com.mydiet.calorycalculator.domain.usecase // CHANGE: Package updated
    class CalculateDailyIntakeUseCase{ /* .. */ }
    
    // featureGroceryListGenerator module
    package com.mydiet.grocerylistgenerator.domain.usecase // CHANGE: Package updated
    class CategorizeGroceryItemsUseCase { /* .. */ }
    
    // featureMealPlanner module
    package com.mydiet.mealplanner.domain.usecase // CHANGE: Package updated
    class PlanWeeklyMealsUseCase { /* .. */ }
```

All of the uses cases are guarded by set of Konsist tests meaning that project coding standards are enforced.
>  See [mydiet-complete](https://github.com/LemonAppDev/konsist-article-refactoring-multi-module-project) project containing all tests and updated code in the [GitHub repository](https://github.com/LemonAppDev/konsist-article-refactoring-multi-module-project) .

The Konsist tests are verifying all classes present in the project at scope creation time meaning that every use case added in the future will be verified by the above guards.

Konsist can help you with guarding even more aspects of the use case. Perhaps you are migrating from `RxJava` to Kotlin `Flow` and you would like to verify the type returned by invoke method or verify that every `invoke` method has an `operator` modifier. It is also possible to make sure that every use case constructor parameter has a name derived from the type or make sure that there parameters are ordered in desired order (e.g. alphabetically).
>  Konists tests are intended to run as part of Pull Request code verification, similar to classic unit tests.

## Summary

This was a very simple yet comprehensive example demonstrating how Konsist can help with code base unification and enforcement of project-specific rules. Upon inspection, we found inconsistencies in method names and declarations, likely due to multiple developers inputs. To address this, we employ Konsist, a Kotlin architectural linter.

In the real world, projects will be more complex, heaving more classes, interfaces, more modules, and will require more Konsist tests. These guards will slightly differ for every project, but fortunately, they can be captured by Konsist flexible API. With Konsist tests in place, we ensure future additions maintain code consistency, making the codebase more navigable and understandable. The code will be **Konsistant**.
>  👉 Follow me on [Twitter](https://twitter.com/igorwojda).

## Links

* [Introduction to Konsist](https://blog.kotlin-academy.com/introducing-konsist-a-cutting-edge-kotlin-linter-d3ab916a5461)

* [Konsist project repository](https://github.com/LemonAppDev/konsist)

* [Konsist documentation](https://docs.konsist.lemonappdev.com/)

* [Konsist Slack channel (at kotlinlang)](https://kotlinlang.slack.com/archives/C05QG9FD6KS)

* [Android-Showcase](https://github.com/igorwojda/android-showcase/) (Android project using Konsist)