【Engineering】依赖注入(Dependency Injection)

Posted by 西维蜀黍 on 2021-08-17, Last Modified on 2021-09-21

依赖注入 (Dependency Injection)

In software engineering, dependency injection is a technique in which an object receives other objects that it depends on, called dependencies.

Typically, the receiving object is called a client and the passed-in (‘injected’) object is called a service.

The code that passes the service to the client is called the injector. Instead of the client specifying which service it will use, the injector tells the client what service to use. The ‘injection’ refers to the passing of a dependency (a service) into the client that uses it.

UML

In the above UML class diagram, the Client class requires ServiceA and ServiceB, but does not instantiate the ServiceA1 and ServiceB1 objects directly.

Instead, an Injector class creates the objects and injects them into the Client. The Client is independent of which classes are instantiated. The UML sequence diagram on the right shows the run-time interactions:

  1. The Injector object creates the ServiceA1 and ServiceB1 objects.
  2. The Injector creates the Client object.
  3. The Injector injects the ServiceA1 and ServiceB1 objects into the Client object.

Why DI

The intent behind dependency injection is to achieve separation of concerns of construction and use of objects, which promotes loosely coupled programs and the dependency inversion and single responsibility principles.

As a result, DIhis can increase readability and code reuse.

Dependency injection is an example of the more general concept of inversion of control.

A client who wants to call some services should not have to know how to construct those services. Instead, the client delegates to external code (the injector). The client is not aware of the injector. The injector passes the services, which might exist or be constructed by the injector itself, to the client. The client then uses the services.

This means the client does not need to know about the injector, how to construct the services, or even which services it is actually using. The client only needs to know the interfaces of the services, because these define how the client may use the services. This separates the responsibility of ‘use’ from the responsibility of ‘construction’.

Analysis

Advantages

Decreased Coupling

A basic benefit of dependency injection is decreased coupling between classes and their dependencies. By removing a client’s knowledge of how its dependencies are implemented, programs become more reusable, testable and maintainable.

This also results in increased flexibility: a client may act on anything that supports the intrinsic interface the client expects.

Many of dependency injection’s benefits are particularly relevant to unit-testing.

For example, dependency injection can be used to externalize a system’s configuration details into configuration files, allowing the system to be reconfigured without recompilation. Separate configurations can be written for different situations that require different implementations of components. This includes testing.

Similarly, because dependency injection does not require any change in code behavior it can be applied to legacy code as a refactoring. The result is clients that are more independent and that are easier to unit test in isolation using stubs or mock objects that simulate other objects not under test. This ease of testing is often the first benefit noticed when using dependency injection.

Reduces Boilerplate Code

More generally, dependency injection reduces boilerplate code, since all dependency creation is handled by a singular component.

Allows Concurrent Development

Finally, dependency injection allows concurrent development.

Two developers can independently develop classes that use each other, while only needing to know the interface the classes will communicate through. Plugins are often developed by third party shops that never even talk to the developers who created the product that uses the plugins.

Disadvantages

Criticisms of dependency injection argue that it:

  • Creates clients that demand configuration details, which can be onerous when obvious defaults are available.
  • Make code difficult to trace because it separates behavior from construction.
  • Is typically implemented with reflection or dynamic programming. This can hinder IDE automation.
  • Typically requires more upfront development effort.
  • Forces complexity out of classes and into the links between classes which might be harder to manage.
  • Encourage dependence on a framework

实现依赖注入的方式

  1. 构造器中注入(Constructor injection)
  2. setter 方式注入(Setter injection)
  3. 接口注入(Interface injection)

Constructor injection

**构造器注入(Constructor injection)**是将需要的依赖作为构造函数的参数传递,已完成依赖注入。

方案1

interface Energy {
}
  
class GasEnergy implements Energy {
}

public class Car {
    private Energy energy;

    public Car(Energy energy) {
        this.energy = energy;
    }
}

在上面的代码中,Car不承担将Energy对象实例化的职能,而是将 energy 对象作为构造函数的一个参数传入。在调用Car的构造函数前就已经初始化好了一个Energy 对象。

方案2

除此之外,还可以通过Java中注解的方式(且依赖一个依赖注入框架,如 Spring)以实现基于构造器的依赖注入。即,通过在字段声明前添加@Inject以实现依赖对象的自动注入。

interface Energy {
}
  
class GasEnergy implements Energy {
}

public class Car {
    @Inject Energy energy;
    public Car() {
    }
}

好处

  • 强依赖解耦:将强依赖之间解耦;
  • 可移植性:在减轻了组件之间依赖关系的同时,也大大提高了组件的可移植性(同时,组件得到重用的机会更多);
  • 便于测试:便于做Mock测试;
  • 在一个 Person 对象实例化时的一开始,就创建好了依赖。

缺点

  • 后期无法更改依赖。

Setter injection

setter方法注入(setter injection)是指增加setter方法,其参数为需要被注入的依赖对象。

interface Energy {
}
  
class GasEnergy implements Energy {
}

public class Car {
    Energy energy;
    
    public void setEnergy(Energy energy) {
      this.Energy  = energy;
  }
}

优点

Person 对象在运行过程中可以灵活地更改依赖。

缺点

Person 对象运行时,可能会存在依赖项为 null 的情况,所以需要检测依赖项的状态。

Interface injection

接口注入(Interface injection)是指为完成依赖注入创建一个接口,且接口中包含实现依赖注入的方法声明,依赖作为该方法的参数传入。

interface EnergyConsumerInterface {
    public void setEnergy(Energy energy);
}
  
class Car implements EnergyConsumerInterface {
    Energy Energy;
      
    public void setEnergy(Energy energy) {
        Energy  = energy;
  }
}

Spring中的依赖注入

下面我们结合Spring的控制反转容器,简单描述一下这个过程。

class MovieLister{
    private MovieFinder finder;
    public void setFinder(MovieFinder finder) {
        this.finder = finder;
    }
}
class ColonMovieFinder extends MovieFinder{
    public void setFilename(String filename) {
        this.filename = filename;
    }
}

我们先定义两个类,可以看到都使用了依赖注入的方式,通过外部传入依赖,而不是自己创建依赖。那么问题来了,谁把依赖传给他们,也就是说谁负责创建finder引用指向的对象,并且把这个具体的MovieFinder对象传给MovieLister对象。

答案是Spring的控制反转容器。

要使用控制反转容器,首先要进行配置。这里我们使用XML的配置,也可以通过代码注解(Anotation)方式配置。

下面是spring.xml的内容:

<beans>
    <bean id="MovieLister" class="spring.MovieLister">
        <property name="finder">
            <ref local="MovieFinder"/>
        </property>
    </bean>
    <bean id="MovieFinder" class="spring.ColonMovieFinder">
        <property name="filename">
            <value>movies1.txt</value>
        </property>
    </bean>
</beans>

在Spring中,每个bean代表一个对象的实例,默认是单例模式,即在程序的生命周期内,所有的对象都只有一个实例,进行重复使用。通过配置bean,控制反转容器在启动的时候会根据配置生成bean实例。

这里,我们只需要知道,控制反转容器会根据XML中的配置,在运行时(runtime)创建一个特定的MovieFinder对象,并把这个特定的MovieFinder对象赋值给一个MovieLister对象的finder字段,最终完成依赖注入的过程。

测试代码

下面给出测试代码:

public void testWithSpring() throws Exception {
    ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");//1
    MovieLister lister = (MovieLister) ctx.getBean("MovieLister");//2
    Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
    assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}

分析

  • 根据配置生成ApplicationContext,即IoC容器;
  • 从容器中获取MovieLister的实例。

Reference