Java DI 依赖注入示例

且听风铃 2021-09-04 16:21:03 浏览数 (2622)
反馈

依赖注入 (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 解耦类依赖并使单元测试更容易。


0 人点赞