Skip to content

13 -DI Container(1):如何实现基本的DI容器结构?

你好,我是徐昊。从今天开始,我们就来使用TDD的方式实现注入依赖容器。

在上节课,我们参照其他依赖注入容器,介绍了依赖注入容器的大致功能。现在请回想一下TDD的流程(参看第11讲):


现在我们需要将需求分解为功能点,并构想架构愿景。

将需求分解为功能点

对于组件构造部分,我分解的任务如下:

  • 无需构造的组件——组件实例

  • 如果注册的组件不可实例化,则抛出异常

    • 抽象类
    • 接口
  • 构造函数注入

    • 无依赖的组件应该通过默认构造函数生成组件实例
    • 有依赖的组件,通过Inject标注的构造函数生成组件实例
    • 如果所依赖的组件也存在依赖,那么需要对所依赖的组件也完成依赖注入
    • 如果组件有多于一个Inject标注的构造函数,则抛出异常
    • 如果组件需要的依赖不存在,则抛出异常
    • 如果组件间存在循环依赖,则抛出异常
  • 字段注入

    • 通过Inject标注将字段声明为依赖组件
    • 如果组件需要的依赖不存在,则抛出异常
    • 如果字段为final则抛出异常
    • 如果组件间存在循环依赖,则抛出异常
  • 方法注入

    • 通过Inject标注的方法,其参数为依赖组件
    • 通过Inject标注的无参数方法,会被调用
    • 按照子类中的规则,覆盖父类中的Inject方法
    • 如果组件需要的依赖不存在,则抛出异常
    • 如果方法定义类型参数,则抛出异常
    • 如果组件间存在循环依赖,则抛出异常

对于依赖选择部分,我分解的任务列表如下:

  • 对Provider类型的依赖

    • 注入构造函数中可以声明对于Provider的依赖
    • 注入字段中可以声明对于Provider的依赖
    • 注入方法中可声明对于Provider的依赖
  • 自定义Qualifier的依赖

    • 注册组件时,可额外指定Qualifier
    • 注册组件时,可从类对象上提取Qualifier
    • 寻找依赖时,需同时满足类型与自定义Qualifier标注
    • 支持默认Qualifier——Named

对于生命周期管理部分,我分解的任务列表如下:

  • Singleton生命周期

    • 注册组件时,可额外指定是否为Singleton
    • 注册组件时,可从类对象上提取Singleton标注
    • 对于包含Singleton标注的组件,在容器范围内提供唯一实例
    • 容器组件默认不是Single生命周期
  • 自定义Scope标注

    • 可向容器注册自定义Scope标注的回调

对于架构构想,我倾向于使用类似Guice、PicoContainer等框架,以代码方式来配置容器。而不是像Spring一样,主要依赖配置文件。

由于依赖注入容器的问题规模较小,我将采用经典TDD模式进行开发,因而也不需要对功能点进行进一步的分解了。

在进入红/绿/重构循环之前,我的build.gradle.kts文件如下所示:

plugins {
    `java-library`
    "jacoco"
}
repositories {
    mavenCentral()
}
dependencies {
    implementation("jakarta.inject:jakarta.inject-api:2.0.1")
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2")
    testImplementation("org.junit.jupiter:junit-jupiter-params:5.8.2")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.2")
    testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.8.2")
    testRuntimeOnly("org.junit.platform:junit-platform-runner:1.8.2")
    testImplementation("org.mockito:mockito-core:4.3.1")
    testImplementation("jakarta.inject:jakarta.inject-tck:2.0.1")
}
tasks.withType<Test>() {
    useJUnitPlatform()
}
java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

进入红/绿/重构循环

下面让我们进入TDD的红/绿/重构循环:

到这里,我们引入了Provider这样一个类似于Factory工厂方法的结构,得到了整个依赖注入容器的基本结构。

那么剩下的事情就是,我们要围绕DI容器的基本结构,对其进行更多功能上的完善。

思考题

在重构的时候,我采用的是增加一个平行实现(Parallel Implementation)。用平行实现替换原有功能,然后再删除原有实现的做法。你有没有不一样的做法?

欢迎把你的思考和想法分享在留言区,也欢迎你扫描详情页的二维码加入读者交流群。我们下节课再见!

最近更新