Bootstrap

Java 基础知识之 依赖注入(Dependency Injection)

概念

虽然标题为 Java 基础知识,但是依赖注入这个词是不限于 Java 的,而是存在于软件工程中的一个技术名词。

在软件工程中,依赖注入(dependency injection,缩写为 DI) 是一种软件设计模式,也是实现 控制反转(Inversion of Control,缩写为 IoC) 其中一种技术。这种模式能让一个对象接收它所依赖的其他对象。依赖 是指接收方所需的对象;注入 是指将 依赖 传递给接收方的过程。在 注入 之后,接收方才能够调用该 依赖

再说细一点:

  • 依赖(Dependency):一个类使用另一个类的实例来完成某些任务,称为依赖。例如,类A依赖于类B来完成某些功能,类B就是类A的依赖。
  • 注入(Injection):将依赖传递给使用它的对象的过程。依赖注入可以通过构造函数、属性(setter)或接口实现。

那么这种设计模式有什么好处呢?主要是以下三点:

  • 提高代码的可维护性:通过将依赖关系的创建和管理工作交给外部容器,代码变得更加清晰易懂,更容易维护。
  • 提高代码的可测试性:通过将依赖关系注入到代码中,可以更容易地创建测试用例来测试代码的各个部分。
  • 提高代码的松耦合性:代码不再需要紧密耦合到其依赖关系的具体实现上,从而更容易地进行重构和扩展。

没有依赖注入的传统编程方式

我们举一个例子:如果一个人(Person)想要玩电脑(Computer)游戏的话,那么他就必须依赖于电脑,也就是 Person 类需要依赖于 Computer 类。

在没有使用依赖注入的传统编程方式中,接收方(Person)就必须自己创建一个依赖的对象 Computer

class Computer {
    public void play(String gameName) {
        System.out.println("playing "+gameName);
    }
}

class Person {
    private final Computer computer = new Computer();
    public void playComputerGame(String whichGame) {
        computer.play(whichGame);
    }
}

public class Main {
    public static void main(String[] args) {
        Person Person = new Person();
        Person.playComputerGame("赛博朋克2077");
    }
}

这看起来没问题,也能工作,但是这种写法会带来一系列问题:包括代码的可维护性、可测试性和松耦合性降低,以及代码的复杂性增加。例如,当需要更改依赖项的实现时,需要手动修改所有使用该依赖项的代码,因为依赖项是硬编码在代码中的。

我们将上面的例子具体化到现实生活中,一个人如果要用电脑的话,可以选择去网吧,用网吧的电脑,也可以选择去公司,用公司的电脑。这个人虽然依赖电脑,但是这个电脑是由外部提供的,而不是这个人随身携带一台台式机,走到哪儿搬到哪儿。

不用依赖注入时,就属于这个人随身携带电脑,而依赖注入,就变成了这个人使用网吧的电脑,使用公司的电脑,只不过在使用时,需要把电脑给他而已。

下面就看一看使用了依赖注入,这个例子该怎么改写。

依赖注入的方式

使用依赖注入时,依赖的对象不会和接收对象放到一起,而是通过某种方式注入到接收方中。也就是依赖注入的常见实现方式:

  • 构造函数注入:通过构造函数将依赖项注入到类中。
  • 方法注入:通过方法将依赖项注入到类中,函数的参数传入。
  • 属性注入:通过属性将依赖项注入到类中,set 方法。
  • 接口注入:使用接口来定义依赖项,然后将实现该接口的类注入到代码中。

现在我们改写上面的代码,分别使用这四种依赖注入方式。

构造函数注入

class Person {
    private final Computer computer;

    public Person(Computer computer) {        //通过构造函数将依赖项 computer 注入
        this.computer = computer;
    }

    public void playComputerGame(String whichGame) {
        computer.play(whichGame);
    }
}

public class Main {
    public static void main(String[] args) {
        Computer computer = new Computer();
        Person Person = new Person(computer);
        Person.playComputerGame("赛博朋克2077");
    }
}

方法注入

class Person {
    public void playComputerGame(Computer computer, String whichGame) {
        computer.play(whichGame);
    }
}

public class Main {
    public static void main(String[] args) {
        Computer computer = new Computer();
        Person Person = new Person();
        Person.playComputerGame(computer, "赛博朋克2077");
    }
}

属性注入

class Person {
    private Computer computer;

    public setComputer(Computer computer) {        //通过此 set 方法注入依赖
        this.computer = computer;
    }

    public void playComputerGame(String whichGame) {
        computer.play(whichGame);
    }
}

public static void main(String[] args) {
    Computer computer = new Computer();
    Person person = new Person();
    person.setComputer(computer);
    person.playComputerGame("赛博朋克2077");
}

接口注入

接口注入在实际应用中较为少见,但它可以为某些特定场景提供灵活性,特别是当类需要实现某个接口并且依赖注入的对象也需要通过接口方法传递时。

这个注入方式需要多说一些。因为这种方式中,依赖项是通过接口方法注入到目标类中的,那么关键点就在于 接收注入的目标类需要实现一个特定的接口,该接口包含用于注入依赖的方法

对于上面的例子,就不能直接使用 Person 类了,而是需要为其制定一个接口:

//定义 Gamer 接口
interface Gamer {
    public setComputer(Computer computer);
    public void playComputerGame(String whichGame);
}

class Person implements Gamer {
    private Computer computer;

    public setComputer(Computer computer) {
        this.computer = computer;
    }

    public void playComputerGame(String whichGame) {
        computer.play(whichGame);
    }
}

interface Computer {
    public void play(String gameName);
}

class WindowsComputer implements Computer {
    public void play(String gameName) {
        System.out.println("Windows Computer playing " + gameName);
    }
}

class LinuxComputer implements Computer {
    public void play(String gameName) {
        System.out.println("Linux Computer playing " + gameName);
    }
}

public class Main {
    public static void main(String[] args) {
        Gamer gamer = new Person();
        
        Computer windowsComputer = new WindowsComputer();
        gamer.setComputer(windowsComputer);
        gamer.playComputerGame("赛博朋克2077");
        
        Computer linuxComputer = new LinuxComputer();
        gamer.setComputer(linuxComputer);
        gamer.playComputerGame("赛博朋克2077");
    }
}

使用框架进行依赖注入

以上是传统的依赖注入的实现方式,但在实际应用中,依赖注入通常通过依赖注入框架来实现。这些框架可以自动管理对象的创建和依赖关系,从而简化开发工作。常见的依赖注入框架包括:

  • Spring(Java):一个流行的依赖注入和应用程序框架。
  • Google Guice(Java):一个轻量级的依赖注入框架。
  • Dagger(Java/Android):一个适用于Android和Java的依赖注入框架。
;