从零开始,通过编写 DI 容器理解依赖注入!(第一部分)DI 阶段 0:基本示例 DI 阶段 1:摆脱静态引用 DI 阶段 2:使用接口 DI 阶段 3:使用 setter 打破循环 下一步

2025-06-09

从零开始,通过编写 DI 容器理解依赖注入!(第一部分)

DI 阶段 0:基本示例

DI 阶段 1:摆脱静态引用

DI 阶段 2:使用接口

DI 阶段 3:使用 setter 打破循环

接下来

依赖注入 (DI) 可能是一个非常难以理解的话题,因为它似乎包含很多“魔法”。通常,它需要一堆散落在各处的注解,对象似乎凭空出现。我当然知道,我花了很长时间才真正理解这个概念。如果你觉得很难理解 Spring 和 Java EE 在幕后做了什么(以及为什么!),那就继续阅读吧!

在本教程中,我们将用 Java从头构建一个非常简单但功能齐全的依赖注入容器。以下是一些易于管理的规则:

  • 绝对不允许使用任何库,除了 JDK 本身。
  • 无需预先准备代码转储。我们将仔细研究所有内容并进行推理。
  • 没有任何花哨的东西,只有基本的东西。
  • 性能无关紧要,没有优化。
  • main()每一步都有可执行的方法。

与其他文章不同,我不会预先解释什么是 DI 或它为什么有用。相反,我们将从一个简单的示例开始,让它“进化”。

DI 阶段 0:基本示例

在github上找到本节的源代码

我们从一个非常基础的例子开始。我们需要两个类,我们分别称之为ServiceAServiceB,并ServiceA需要ServiceB完成它的工作。好吧,你可以用static方法来实现它,然后就大功告成了!所以,开始吧:

public class ServiceA {

    public static String jobA(){
        return "jobA(" + ServiceB.jobB() + ")";
    }

}
public class ServiceB {

    public static String jobB(){
        return "jobB()";
    }

}
public class Main {

    public static void main(String[] args) {
        System.out.println(ServiceA.jobA());
    }

}

如果我们运行此代码,它将打印:

jobA(jobB())

太棒了!那为什么还要费心去做更多呢?嗯……

  • 这段代码非常不符合面向对象原则。ServiceA 和 ServiceB 至少应该是对象
  • 代码紧密耦合,很难单独测试。
  • 我们绝对不可能将 ServiceA 和 ServiceB 替换成不同的实现。想象一下,其中一个正在处理信用卡账单;你肯定不希望这种情况在你的测试套件中发生。

DI 阶段 1:摆脱静态引用

在github上找到本节的源代码

我们在阶段 0 中发现的主要问题是,只有静态方法,导致耦合度极高。我们希望我们的服务是可以相互通信的对象,以便我们可以根据需要替换它们。现在问题来了:ServiceA 如何知道要与哪个ServiceB 通信?最基本的思路是,简单地将 ServiceB 的实例传递给 ServiceA 的构造函数:

public class ServiceA {

    private ServiceB serviceB;

    public ServiceA(ServiceB serviceB){
        this.serviceB = serviceB;
    }

    public String jobA(){
        return "jobA(" + this.serviceB.jobB() + ")";
    }

}

服务 B 没有太大变化:

public class ServiceB {

    public String jobB() {
        return "jobB()";
    }

}

...Main现在需要组装对象才能调用jobA方法:

   public static void main(String[] args) {
        ServiceB serviceB = new ServiceB();
        ServiceA serviceA = new ServiceA(serviceB);

        System.out.println(serviceA.jobA());
    }

没什么特别的,对吧?我们确实改进了一些地方:

  • 我们现在可以通过提供另一个对象(甚至可能是另一个子类)来替换ServiceB所使用的实现。ServiceA
  • 我们可以使用 ServiceA 的适当测试模拟来单独测试这两项服务。

这么酷吗?其实也不完全是:

  • 创建模拟很困难,因为我们需要一个
  • 如果我们只需要接口就好了。而且,这还能进一步降低耦合度。

DI 阶段 2:使用接口

在github上找到本节的源代码

因此,让我们为每个服务使用一个接口。它们尽可能简单(我将实际的类分别重命名为ServiceAImplServiceBImpl):

public interface ServiceA {

    public String jobA();

}
public interface ServiceB {

    public String jobB();

}

现在,在中ServiceAImpl我们实际上可以使用该界面:

public class ServiceAImpl implements ServiceA {

    private final ServiceB serviceB;

    public ServiceAImpl(ServiceB serviceB){
        this.serviceB = serviceB;
    }

    // jobA() is the same as before
}

这也使我们的main()方法更好一些:

  public static void main(String[] args) {
        ServiceB serviceB = new ServiceBImpl();
        ServiceA serviceA = new ServiceAImpl(serviceB);
        System.out.println(serviceA.jobA());
    }

从面向对象的角度来看,这对于简单的用例来说是完全可以接受的。如果你能做到这一点,请务必到此为止。但是,随着项目规模越来越大……

  • 在您的方法内部创建服务网络将变得越来越复杂。main()
  • 您将遇到服务依赖关系中的循环,而这些循环无法使用构造函数解决,如我们的示例所示。

DI 阶段 3:使用 setter 打破循环

在github上找到本节的源代码

假设不仅ServiceA需要ServiceB,而且反过来也需要——这样就形成了一个循环。当然,我们仍然可以在*Impl类的构造函数中声明一个参数,如下所示:


 // constructor examples
 public ServiceAImpl(ServiceB serviceB) { ... }
 public ServiceBImpl(ServiceA serviceA) { ... }

……但这对我们没有任何好处:我们将无法创建这两个类中的任何一个的实际实例。要创建 的实例ServiceAImpl,我们首先需要一个 的实例ServiceB,而要创建 的实例,ServiceBImpl我们首先需要一个 的实例ServiceA。我们陷入了僵局。

附注:其实可以通过使用代理来解决这个问题。不过,我们这里不打算采用这种方法。

那么我们该怎么做呢?因为我们要处理循环依赖,所以我们需要先创建服务,然后将它们连接在一起。我们用 setter 来实现:

public class ServiceAImpl implements ServiceA {

    private ServiceB serviceB;

    // no constructor anymore here!

    @Override // <- added getter to ServiceA interface
    public ServiceB getServiceB() { return serviceB; }

    @Override // <- added setter to ServiceA interface
    public void setServiceB(final ServiceB serviceB) { this.serviceB = serviceB; }

    // jobA() same as before
}

我们的main()方法是这样的:

    public static void main(String[] args) {
        // create instances
        ServiceB serviceB = new ServiceBImpl();
        ServiceA serviceA = new ServiceAImpl();

        // wire them together
        serviceA.setServiceB(serviceB);
        serviceB.setServiceA(serviceA);

        // call business logic
        System.out.println(serviceA.jobA());
    }

你能看出其中的规律吗?首先创建对象,然后将它们连接起来,形成服务图(即服务网络)。

那么为什么不在这里停下来呢?

  • 手动执行连接部分很容易出错。你可能会忘记调用 setter,然后错误就NullPointerException很多了。
  • 您可能会意外使用仍在构建中的服务实例,因此以某种方式封装网络构建将会很有益。

接下来

在下一篇博文中,我们将讨论如何自动化我们现在手动进行的布线。

鏂囩珷鏉ユ簮锛�https://dev.to/martinhaeusler/understanding-dependency-injection-by-writing-a-di-container-from-scratch-part-1-1hdf
PREV
Implementing 2D Physics in Javascript
NEXT
正合我意:2019 年使用 Jest、ESLint 和 Prettier 推出全新 TypeScript 项目