依赖注入 (DI) 是一种让类从外部接收其依赖的技术。如果类 A 使用类 B,则类 A 依赖于类 B,而 B 是 A 的依赖项。
以下示例显示了 Java 中的依赖项和 DI 是什么。在第一个示例中,A 类依赖于 B 类,因为 B 是 A 的成员。A 和 B 是紧密耦合的。每当 B 改变时,A 就必须改变。这种情况称为硬依赖。
// hard dependency
class A{
private B b;
public A(){
this.b = new B();
}
...
}
在第二个例子中,A 仍然依赖于 B,但依赖不是硬编码的。它通过在构造函数中使用参数来解耦。如果 A 需要 B 的不同实现,A 可以使用 B 的不同实现来构造实例。这导致了 DI 的一个关键特性:被注入的类应该是一个抽象接口,以便可以将不同的实现注入到 A。如果 B 的实现只有一个,则不需要进行 DI。
// dependency injection through constructor
class A{
private B b;
public A(B b){
this.b = b;
}
...
}
使用依赖注入的好处
DI 的一个示例用途是数据访问对象 (DAO)。执行 CRUD 操作的类通常需要访问数据库。使用 DI 向应用程序注入 DAO 将应用程序层与数据持久层解耦。如果底层数据库发生变化,只要这些 DAO 实现相同的接口,应用程序类就可以更改为不同的 DAO。另一个好处是使单元测试更容易。单元测试可以使用伪造的(硬编码或内存中的)DAO 来测试应用程序逻辑,而无需担心底层数据库访问。
DI 是流行的 Java 框架(如 Spring 和 Hibernate)中使用的一项关键技术。框架不是手动创建 B 对象并将其传递给 A 的构造函数,而是使用反射来创建依赖对象并根据配置将它们注入到适当的位置。
一个展示依赖注入的简单例子
下面是一个简单的例子来说明使用框架时 DI 的样子以及使用 DI 的两个好处。我使用 Guice 框架,但其他框架在幕后以相同的方式工作。
假设我们有一台计算机,它有很多部分协同工作,例如 CPU、内存等。 CPU 中有两种方法。
public interface CPU {
public void start();
public int getUsage();
}
CPU 可以是 Intel,
public class Intel implements CPU{
@Override
public void start() {
System.out.println("Intel is started.");
}
@Override
public int getUsage() {
return new Random().nextInt(100);
}
}
或 AMD。
public class Amd implements CPU {
@Override
public void start() {
System.out.print("Amd is started");
}
@Override
public int getUsage() {
return new Random().nextInt(100);
}
}
在 Guice 中,通过构造函数注入依赖就像添加 @Inject 注释一样简单。
public class Computer {
private CPU cpu;
@Inject
Computer(CPU cpu) {
this.cpu = cpu;
}
public void start() {
cpu.start();
// start other parts
}
public boolean isStatusOk() {
//assuming this random
if (cpu.getUsage() > 50) {
return false;
}
// check other things, such as memory, hard drives.
return true;
}
public static void main(String[] args) {
Injector injector = Guice.createInjector(new BasicModule());
Computer computer = injector.getInstance(Computer.class);
computer.start();
System.out.println("Status:" + (computer.isStatusOk() ? "OK" : "Not OK"));
}
}
Guice 使用模块来配置注入。在此示例中,当请求 CPU 时,模块将具体的 Intel 绑定到 CPU。
public class BasicModule extends AbstractModule {
@Override
protected void configure() {
bind(CPU.class).to(Intel.class);
}
}
这样做的好处是显而易见的。计算机可以在需要时灵活地使用其他类型的 CPU。此外,如果 CPU 依赖于另一个类,例如 Cache 或 Clock,我们可以使用相同的方式注入依赖项,而无需耦合这些类。
关于第二个好处——让单元测试更简单,我们可以做一个简单的单元测试来测试 isStatusOk() 方法。在实际情况下,CPU 使用率可以是基于实际使用情况的随机数。如果我们想把测试的重点放在方法的其他部分,我们可以模拟 CPU 的使用情况,假设 CPU 使用率没问题,然后测试其他部分。
public class ComputerTest {
@Test
public void testIsStatusOk(){
CPU cpu = mock(CPU.class);
// mock cpu usage, so we can focus on testing other part
when(cpu.getUsage()).thenReturn(10);
assertTrue(new Computer(cpu).isStatusOk());
}
}
总之,DI 是为了分离对象创建和使用的关注点。DI 解耦类依赖并使单元测试更容易。