与Konsist重构多模块Kotlin项目
#kotlin #spring #android #konsist

与Konsist重构多模块Kotlin项目

重构不仅仅是更改代码;这是关于增强其结构,提高可读性,优化性能并保持一致的情况。在本文中,我们将重点关注一致性方面和重构一个简单的虚构项目,以统一其代码库。

我们还将实施一组警卫,以保持事物 k 将来。为了实现这一目标,我们将利用Konsist,Kotlin Architectural Linter。

阅读Introduction to Konsist

基本项目

典型的项目很复杂,它们包含许多类型的类/界面,这些类别/接口负责各种职责(视图,控制器,模型,用例,存储库等)。这些类/接口通常分布在不同的模块上,并放置在各种软件包中。重构这样的项目对于一篇文章来说将太多了,因此我们将使用一个启动项目重构3个模块和4个用例。

如果您喜欢学习,则可以遵循文章步骤。只需查看repository,打开InteliJ IDEAidea-mydiet-starter)或Android Studioandroid-studio-mydiet-starter)的入门项目即可。为了使事情变得简单,此项目包含一组要进行验证和重构的类,而不是完整的应用程序。

Mydiet应用程序具有3个模块:

  • featurecalorycalculator

  • featuregrocerylistgenerator

  • featuremealplanner

每个功能模块都有一个或多个用例。让我们看一下IntelliJ IDEA熟悉的项目视图以获取完整的项目结构:

让我们查看所有功能模块中每个用例类的内容:

    // 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
        }
    }

用例持有业务逻辑(为简单起见,此处表示是代码中的评论)。乍一看,这些用例看起来相似,但是经过仔细检查后,您会注意到用例类声明在方法名称,公共方法数量和软件包方面是不一致的。很可能是因为这些用例是由不同开发人员优先考虑个人意见而不是特定项目标准的不同开发人员撰写的。

确切的规则因项目而异,但Konsist API仍然可以用来定义针对特定项目量身定制的支票。

让我们写一些Konsist测试以统一代码库。

后卫1:统一用户酶方法名称

让我们想象这是一个大规模项目,每个模块中包含许多类别,并且由于每个模块很大,因此我们希望隔离每个模块。每个模块,重构将限制更改的范围,并有助于保持拉动请求较小。我们将仅专注于统一用例。

使用Konsist的第一步是在给定模块中创建存在的范围(包含Kotlin文件列表):

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

现在我们需要选择代表用例的所有类。在此项目中,用例是带有用户酶名称后缀的类(.withNameDendingWith(usecase))。

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

在其他项目中,用例可以由the类扩展基本usecase类(.withAllParentsOf*(BaseUseCase::class))或用@usecase注释注释的每个类(.withAllAnnotationsOf(BaseUseCase::class))。

现在定义包含所需检查的断言(断言块的最后一行总是必须返回布尔值)。我们将确保每个用例都有一个带有统一名称的公共方法。我们将选择调用作为所需的方法名称:

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

请注意,我们的警卫将缺乏可见性修饰符视为public,因为它是默认的Kotlin可见性。

如果您希望始终增加明确的公共可见性修饰符,则可以使用hasPublicModifier属性。

要使上述检查工作我们需要将其包装在JUnit测试中:

    @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
                }
            }
    }

如果您正在关注该项目,则将此测试添加到应用程序/src/test/kotlin/usecasekonsisttest.kt文件。要运行Konsist测试click on the green arrow(剩余到测试方法名称)。

在运行Konsist测试后,它将抱怨缺少AdjustCaloricGoalUseCaseCalculateDailyIntakeUseCase类(featureCaloryCalculator模块)中名称Invoke的方法。让我们在这些类中更新方法名称进行测试通过:

    // 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
        }
    }

下一个重构模块是featureGroceryListGenerator模块。同样,我们将假设这是一个非常大的模块,其中包含许多类和干扰。我们可以简单地复制测试并更新模块名称:

    @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
                }
            }
    }

上述方法有效,但是它导致不必要的代码重复。我们可以通过为每个模块创建两个示波器并添加它们来做得更好:

    @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
                }
            }
    }

可以添加示波器,因为Koscope覆盖了Kotlin plusplusAssign操作员。有关更多信息,请参见Create The Scope

这次Konsist测试将失败,因为featureGroceryListGenerator模块中存在的CategorizeGroceryItemsUseCase类具有不正确的名称。让我们解决这个问题:

    // featureGroceryListGenerator module
    // BEFORE
    class CategorizeGroceryItemsUseCase {
        fun categorizeGroceryItemsUseCase() {
            // business logic
        }
    }

    // AFTER
    class CategorizeGroceryItemsUseCase {
        fun invoke() { // CHANGE: Name updated
            // business logic
        }
    }

测试正在通过。现在,我们有了最后一个用于重构的模块。我们可以在featureMealPlanner模块中添加代表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
                }
            }
    }

请注意,featureMealPlanner模块是此特定重构的最后一个模块,因此我们可以简化上述代码。我们可以使用Konsist.scopeFromProject()
而不是创建3个单独的范围(对于每个模块)并添加它们,而是可以验证生产源集(main)中的所有类

    @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
                }
            }
    }

这次测试将成功,因为PlanWeeklyMealsUseCase类中存在的PlanWeeklyMealsUseCase类已经具有名为invoke的方法:

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

让我们改善我们的规则。

后卫2:用例只有一种公共方法

要验证项目中存在的每个用例是否都有一种公共方法,我们可以使用it.numPublicOrDefaultDeclarations() == 1检查类中公共声明的数量(或默认)声明的数量。而不是编写新测试,我们可以改善现有测试:

    @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
          }
    }

运行此Konsist测试后,我们将意识到AdjustCaloricGoalUseCase类具有两种public方法。要解决,我们将将calculateCalorieS方法的可见性更改为private(我们假设它是意外暴露的):

    // 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
        }
    }

后卫3:每个用例都位于域中。

您可能还没有注意到,但是用例包装结构有点关闭。两个用例AdjustCaloricGoalUseCaseCalculateDailyIntakeUseCase类位于com.mydiet软件包中,CategorizeGroceryItemsUseCase类位于com.mydiet.usecase软件包中(末尾没有s),而PlanWeeklyMealsUseCase类则位于com.mydiet.usecases软件包中(末端):

我们将首先验证每个用例的所需软件包是否为domain.usecase package(前缀并随后是许多软件包)。更新软件包名称非常简单,因此这次我们将为所有模块定义后卫,并一口气修复所有违规行为。让我们写一个新的Konsist测试以守护此标准:

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

两个点..表示零或更多软件包。

现在,上面突出显示的测试将在所有用例中都失败,因为它们都不位于正确的软件包中(它们都不位于domain软件包中)。要解决此问题,我们必须只需更新软件包(省略了类内容以清楚):

    // 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 { /* .. */ }

现在Konsist测试将成功。我们可以改进命名的包装。在一个典型的项目中,功能模块中的每个类都将具有特征名称前缀的软件包,以避免在不同模块上进行类重新分配。我们可以检索模块名称(moduleName),然后删除feature前缀以获取软件包的名称。让我们改善现有测试:

    @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..")
            }
    }

和最终修复程序再次更新这些软件包:

    // 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 { /* .. */ }

所有用途案例均由Konsist测试集守护,这意味着实施了项目编码标准。

请参阅mydiet-complete项目,其中包含GitHub repository中的所有测试和更新的代码。

Konsist测试正在验证项目范围创建时间中存在的所有类,这意味着将来添加的每个用例都将由上述警卫验证。

Konsist可以帮助您保护用例的更多方面。也许您是从RxJava迁移到Kotlin Flow,您想验证通过Invoke方法返回的类型或验证每个invoke方法是否具有operator修饰符。还可以确保每个用例构造函数参数都有从类型派生的名称,或确保以所需顺序排序参数(例如,字母内)。

Konists测试旨在作为拉请求代码验证的一部分运行,类似于经典的单元测试。

概括

这是一个非常简单但全面的示例,演示了Konsist如何帮助代码基础统一和针对项目特定规则的执行。经检查后,我们发现方法名称和声明的不一致可能是由于多个开发人员输入所致。为了解决这个问题,我们雇用了Konsist,Kotlin Architectural Linter。

在现实世界中,项目将变得更加复杂,增加了更多的类,接口,更多的模块,并且需要更多的Konsist测试。每个项目都会有所不同,但幸运的是,它们可以被Konsist Fixible API捕获。借助Konsist测试,我们确保将来的添加保持代码一致性,从而使代码库导航和可理解。该代码将为 konsistant

ð在Twitter上关注我。

链接