跳转至

15 DI Container(3):如何重构已有的代码?

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

回顾代码与任务列表

到目前为止,我们的代码是这样的:

package geektime.tdd.di;

import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

import static java.util.Arrays.stream;
public class Context {
    private Map<Class<?>, Provider<?>> providers = new HashMap<>();

    public <Type> void bind(Class<Type> type, Type instance) {
        providers.put(type, (Provider<Type>) () -> instance);
    }

    public <Type, Implementation extends Type>
    void bind(Class<Type> type, Class<Implementation> implementation) {
        Constructor<?>[] injectConstructors = stream(implementation.getConstructors()).filter(c -> c.isAnnotationPresent(Inject.class))
                .toArray(Constructor<?>[]::new);
        if (injectConstructors.length > 1) throw new IllegalComponentException();
        if (injectConstructors.length == 0 && stream(implementation.getConstructors())
                .filter(c -> c.getParameters().length == 0).findFirst().map(c -> false).orElse(true))
            throw new IllegalComponentException();
        providers.put(type, (Provider<Type>) () -> {
            try {
                Constructor<Implementation> injectConstructor = getInjectConstructor(implementation);
                Object[] dependencies = stream(injectConstructor.getParameters())
                        .map(p -> get(p.getType()))
                        .toArray(Object[]::new);
                return injectConstructor.newInstance(dependencies);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
    }

    private <Type> Constructor<Type> getInjectConstructor(Class<Type> implementation) {
        return (Constructor<Type>) stream(implementation.getConstructors())
                .filter(c -> c.isAnnotationPresent(Inject.class)).findFirst().orElseGet(() -> {
                    try {
                        return implementation.getConstructor();
                    } catch (NoSuchMethodException e) {
                        throw new RuntimeException(e);
                    }
                });
    }

    public <Type> Type get(Class<Type> type) {
        return (Type) providers.get(type).get();
    }
}

任务列表状态为:

  • 无需构造的组件——组件实例
  • 如果注册的组件不可实例化,则抛出异常

  • 抽象类

  • 接口
  • 构造函数注入

  • 无依赖的组件应该通过默认构造函数生成组件实例

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

  • 通过Inject标注将字段声明为依赖组件

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

  • 通过Inject标注的方法,其参数为依赖组件

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

  • 注入构造函数中可以声明对于Provider的依赖

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

  • 注册组件时,可额外指定Qualifier

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

  • 注册组件时,可额外指定是否为Singleton

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

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

视频演示

让我们进入今天的部分:

思考题

如何实现循环依赖的检测?

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

精选留言(7)
  • 小5 👍(4) 💬(0)

    学习了这节课的内容,想到了之前章节的一段话: Kent Beck 作为极限编程(Exetreme Programming)的创始人,将勇气(Courage)作为极限编程的第一原则,提出编程的第一大敌是恐惧(Fear),实在是有非凡的洞见。同时,他也花了极大的篇幅,说明为什么 TDD 可以让我们免于恐惧:重构使得我们在实现功能时,不恐惧于烂代码;测试使得我们在重构时,不恐惧于功能破坏。 我们平时常说:代码看不懂、改不动、不敢改。 看不懂是指认知负载太高,导致代码很难改,改了一个地方影响很多地方,甚至不知道影响多少地方,也就自然不敢改了,改出问题了谁负责,改一个bug改出多了几个新bug,以前公司老人常说千万不要在现有的代码上改,要把方法copy一份,然后再改,调用新的方法😂,多么痛的领悟。 在做练习的重构过程中明白了因为对所有的代码都有测试,改了代码跑一下如果有错误就很容易发现,所以给了我们改代码的勇气。

    2022-09-25

  • escray 👍(4) 💬(0)

    进入到第二阶段的课程之后,给出的代码相对比较详细,虽然实现的功能比起前面要复杂一些,但是跟上节奏就不那么困难了。 这个专栏的门槛还是挺高的。 实现循环依赖检测,比较容易想到的就是在每个依赖添加一个标志位,但是这个似乎不那么容易实现;那么另一个方式,就是增加一个记录依赖的列表,每次都去判断是否存在循环依赖。 代码不太会写,只能先想一下。 本课的代码链接:https://github.com/escray/TDDCourse/tree/ch15

    2022-04-28

  • 汗香 👍(3) 💬(0)

    获取一个依赖前,先从已有依赖获取, 若获取结果不为空则获取成功, 若获取结果为空,先将被获取依赖类型放入一个待创建集合中,并判断当前组件本身是否在集合中,若在,说明有循环依赖

    2022-04-25

  • 努力努力再努力 👍(1) 💬(0)

    哭了,上一个留言发现有问题,自己实现的时候才发现单纯一个 set 集合记录正在创建的bean还不行,得利用 TLS(https://time.geekbang.org/column/article/93745) 模式做线程隔离,否则多线程的测试用例下,就会出现明明没有循环依赖而报错的问题。

    2022-09-23

  • 努力努力再努力 👍(1) 💬(0)

    循环依赖,可以加一个 bean 在创建中的标识,当 bean 被创建完之后,就把标识去掉,这样如果创建过程中发现这个 bean 正在创建中,就可以认为是发生循环依赖了

    2022-09-23

  • 蝴蝶 👍(0) 💬(0)

    @Inject注解的构造方法如果没有参数,那肯定是有问题的,这次看明白了。

    2022-08-13

  • davix 👍(0) 💬(0)

    get() 重構受啟發!雖然看過《重構》了解其中的方法,但平時如果get()設計變化我通常還是直接改。這回直觀感受引入get_()的好處。

    2022-05-25