实战Arch Unit

实战Arch Unit

在以前的文章中介绍了通过 《实战PMD》《实战Checkstyle》在代码级守护我们的代码,比通过《实战Jacoco》来了解当前项目的测试覆盖情况。通过得到数据了解我们的项目质量,进行定向的改进。

使用这些简单方面的自动化工具比凭空猜想或者全靠人力来接发现代码上的问题,效率高多了。

这篇文章将聚焦在Arch Unit上,Arch Unit能通过为我们提供架构的守护。

  1. 开发前的准备
  2. 项目分层检测
  3. 循环依赖检测(同一个package下,不同package下的循环依赖)
  4. Package依赖检测
  5. Package和Class的包含关系检测
  6. 忽略某些违规行为的三种凡是
  7. 如何组织Arch Unit的测试

先来看一下Arch Unit的相关功能介绍。

Arch Unit

这些功能很好,但是要是面面俱到,那么维护、查看规则也是一件麻烦事,所以针对项目情况,有选择定制,才能更好的展现器价值。

通过自己坐在项目的情况,可以通过金字塔来罗列:哪些行为做了价值大,哪些事情做了价值小。


1,开发前的准备

Arch Unit集成了Junit4Junit5,在它的模块中包含:archunitarchunit-junit4archunit-junit5-apiarchunit-junit5-enginearchunit-junit5-engine-api

在项目中只需要引入测试相关的JUnit5相关的依赖。

dependencies {
    testCompile 'com.tngtech.archunit:archunit-junit5:0.13.1'
}

test {
    useJUnitPlatform()
}

实践过程中有可能遇到的情况:

Tips 01: 当 @Analysis 中配置的 package 目录写错时,并不会报错package不存在,而是会让全部测试通过。

Tips 02: *在 layer 验证的时候,定义 layer的时候,package 名称需要根据需要在包名后添加 "..",例如:

layeredArchecture()
.layer("Representation").definedBy("com.page.practice.representation..")
.layer("Domain").definedBy("com.page.practice.domain..")
...

.whereLayer("Domain").mayOnlyAccessedByLayers("Representation")
...

其中的 com.page.practice.representation.. 结尾使用 ..,原因是 representation 中如果包含了其他两个package 例如:requestresponse,那么当 request 中调用到了 domain 中类后,上面的代码是可以检测通过。

如果去掉 com.page.practice.representation.. 结尾的 ..,那么当request 中调用到了 domain 时,检测是不过的。


2, 项目分层检测

在做DDD的一些落地项目中我们会使用四层架构,即RepresentationApplicationDomainInfrastructure四层,这四层的调用关系如下图所示。

下面通过一个例子我们来约束这几层的调动关系。

package com.page.practice.archunit;

import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import org.assertj.core.presentation.Representation;

import static com.tngtech.archunit.library.Architectures.layeredArchitecture;

@AnalyzeClasses(packages = "com.page.practice")
public class LayeredArchitectureTest {

    @ArchTest
    static ArchRule layer_dependencies_are_respected = layeredArchitecture()
            .layer("Representation").definedBy("com.page.practice.representation..")
            .layer("Application").definedBy("com.page.practice.application")
            .layer("Domain").definedBy("com.page.practice.domain..")
            .layer("Infrastructure").definedBy("com.page.practice.infrastructure")

            .whereLayer("Representation").mayNotBeAccessedByAnyLayer()
            .whereLayer("Application").mayOnlyBeAccessedByLayers("Representation")
            .whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Representation");
}

如上代码,定义了四层 layer,然后定义了: 1. Representation 不能被其他 Layer 调用, 2. Application 能够被 Represenatation 调用 3. Domain 能够被 RepresentationApplication 调用。

TIPS:

其中的 com.page.practice.representation.. 结尾使用 ..,原因是 representation 中如果包含了其他两个package 例如:requestresponse,那么当 request 中调用到了 domain 中类后,上面的代码是可以检测通过。

如果去掉 com.page.practice.representation.. 结尾的 ..,那么当request 中调用到了 domain 时,检测是不过的。


3, 循环依赖检测

对于一些项目仍旧在使用MVC三层的结构,当项目进行一段时间后,经常会遇到的循环依赖的问题。

而解决这类问题除了参考DDD等实践还有可能会根据团队对项目的理解,而添加团队规范,形成约束,如果只是口头约束,那么接下来开发过程中还会遇到多个上下的循环依赖问题,而如何通过测试代码约束住循环依赖,并在运行测试时第一时间得到反馈,这是效率较高的做法(Tips:使用 自动化测试 比完全启动项目 人肉点击测试 效率要高很多)。

在日常工作中经常出现两种循环依赖,一种是同一个package下出现循环依赖,另一种情况是不同package下的类出现了循环依赖。Arch Unit对这两的场景约束验证略有不同。

(1)场景一:相同package下的类出现了循环依赖

package com.page.practice.application;


import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.library.dependencies.SliceAssignment;
import com.tngtech.archunit.library.dependencies.SliceIdentifier;

import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;

@AnalyzeClasses(packages = "com.page.practice")
public class CycleDependencyRulesTest {


    private static SliceAssignment legacyPackageStructure = new SliceAssignment() {
        @Override
        public SliceIdentifier getIdentifierOf(JavaClass javaClass) {
            if (javaClass.getPackageName().startsWith("com.page.practice.application")) {
                return SliceIdentifier.of(javaClass.getName());
            }

            return SliceIdentifier.ignore();
        }

        @Override
        public String getDescription() {
            return "legacy package structure";
        }
    };

    @ArchTest
    static final ArchRule no_cycles_by_method_calls_in_same_slice = slices()
            .assignedFrom(legacyPackageStructure)
            .should()
            .beFreeOfCycles();
}

上面的代码关键部分在 getIdentifierOf()中,如果package名字是以javaClass.getPackageName().startsWith("com.page.practice.application")return SliceIdentifier.of(javaClass.getName()); 认为是不同slice,如果不是目标package则忽略。

**(2)不同package下的类出现循环依赖

使用Arch Unit对不同package下的循环依赖验证条件要相对简单,只需要在matching()方法中,指定匹配的package规则就可以了。

package com.page.practice.application;


import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;

@AnalyzeClasses(packages = "com.page.practice")
public class CycleDependencyRulesTest {

    @ArchTest
    static final ArchRule no_cycles_by_method_calls_between_slices = slices()
            .matching("application.(*)..")
            .should()
            .beFreeOfCycles();
}

即:在包 com.page.practice.infrastructure 中的内类不能够调用 com.page.practices 中的类。


4, Package依赖检测

在实现DDD的四层架构中,application可以依赖infrastructure,但是infrastructure 并不能依赖 application,所以通过Arch Unit建立规则

模拟场景如图,下图中的红线部分这个场景中出现 infrastructure 逆向调用了 application 层的代码

通过如下代码我们建立约束。

package com.page.practice.archunit;

import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

@AnalyzeClasses(packages = "com.page.practice")
public class PackageDependencyTest {

    @ArchTest
    static ArchRule infrastructure_should_no_dependecny_on_application = noClasses()
            .that()
            .resideInAPackage("..infrastructure")
            .should()
            .dependOnClassesThat()
            .resideInAPackage("..application");

}

即:在包 com.page.practice.infrastructure 中的内类不能够调用 com.page.practices 中的类。

注意这里使用的是 noClasses() 静态方法,表达的不能依赖。


5, Package和Class的包含关系

约束某个package下的类的命名规则也是非常重要的,例如之前有的项目在进行过程中,由于没有进行自动化约束,而是人为的传授约束,结果一段时间过去后,命名五花八门。

例如在微服务中使用到了 Feign,结果出现了如下命名方式:xxFeign,xxFeignClientxxClientxxService

预期较劲脑汁的沟通,不如建议一套自动化约束。了解自卸自动化约束是修改、添加代码的前提之一。

下面的例子是在**.application 包下的类都需要以Service结尾。

@ArchTest
static ArchRule = classes()
    .that()
    .resideInAPackage("..application")
    .should()
    .haveSimpleNameEndingWith("Service");

6, 忽略某些违规行为

如果你的项目已经开始,且代码质量不高,直接添加这些Arch Unit约束,很可能会遇到进退两难的问题,所以临时忽略一部分是架构向好的反向演进过程中一种需要使用的方法。

有两种行为能够忽略部分规则

(1) 使用@ArchIgnore注解

@ArchIgnore
@ArchTest
static ArchRule = classes()
    .that()
    .resideInAPackage("..application")
    .should()
    .haveSimpleNameEndingWith("Service");

(2) 通过制定更为具体的package名称来做局部限定,在对自己何时的时候在扩大范围

(3) 在classpath下使用arch_ignore_patterns.txt,并在文件中添加需要忽略的package或者class

.*some\.pkg\.ABC.*

所有符合some.pkg.ABC都会被忽略。

### 7, 如何组织Arch Unit的测试

为了方便维护,可以一个类型的规放置在一个单独的Test文件,当查找是能够方便的进行相关规则的查找。

编辑于 02-15

文章被以下专栏收录