从零开始,通过编写 DI 容器理解依赖注入!(第一部分)
DI 阶段 0:基本示例
DI 阶段 1:摆脱静态引用
DI 阶段 2:使用接口
DI 阶段 3:使用 setter 打破循环
接下来
依赖注入 (DI) 可能是一个非常难以理解的话题,因为它似乎包含很多“魔法”。通常,它需要一堆散落在各处的注解,对象似乎凭空出现。我当然知道,我花了很长时间才真正理解这个概念。如果你觉得很难理解 Spring 和 Java EE 在幕后做了什么(以及为什么!),那就继续阅读吧!
在本教程中,我们将用 Java从头构建一个非常简单但功能齐全的依赖注入容器。以下是一些易于管理的规则:
- 绝对不允许使用任何库,除了 JDK 本身。
- 无需预先准备代码转储。我们将仔细研究所有内容并进行推理。
- 没有任何花哨的东西,只有基本的东西。
- 性能无关紧要,没有优化。
main()
每一步都有可执行的方法。
与其他文章不同,我不会预先解释什么是 DI 或它为什么有用。相反,我们将从一个简单的示例开始,让它“进化”。
DI 阶段 0:基本示例
在github上找到本节的源代码
我们从一个非常基础的例子开始。我们需要两个类,我们分别称之为ServiceA
和ServiceB
,并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上找到本节的源代码
因此,让我们为每个服务使用一个接口。它们尽可能简单(我将实际的类分别重命名为ServiceAImpl
和ServiceBImpl
):
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