浅谈 IoC / DI

2017-10-24  本文已影响0人  李眼镜

1.背景

刚开始接触 spring ,一定不会少看到 IoC、DI 这两个单词。

先把 spring 撇开,单独看看 IoC 和 DI 是什么意思呢?

找两个定义:

呃,看完了,似懂非懂。。。

2.举个“吃饭”的例子

场景:在沙县小吃/学校食堂吃饭。

沙县小吃:

  1. 顾客到店:老板,我要一份炒米线。
  2. 老板:好嘞,这就给你做。

学校食堂:

  1. 学生到食堂:阿姨,我要这个西红柿炒蛋,加4两米饭。
  2. 食堂阿姨:好嘞,这就给你装盘。

区别在哪呢?

3.再举个例子

场景:接口参数检查

假设,我们的后台服务拆两个类:MyService 和 Validator 。很明显,MyService 依赖 Validator。

考虑一下, 最简单的实现方式是什么呢?

3.1 传统实现,new

简单介绍一下用到的类和接口:

代码示例:

// IValidator.java
interface IValidator.java {
    public void validate(String param);
}

// NotnullValidator.java
class NotnullValidator implements IValidator{
    @Override
    public void validate(String param){
        System.out.println(this.getClass().getSimpleName()+ " validating param {"+param+"}");
        if(null == param || 0 == param.length()){
            System.out.println(this.getClass().getSimpleName() + " param can not be null or empty");
        }
    }
}

// MyService.java
class MyService.java {
    void Serve(String param){
        System.out.println(this.getClass().getSimpleName() + " param = " + param);
        NotnullValidator notnullValidator = new NotnullValidator();
        notnullValidator.validate(param);
    }
}

// Client.java
public class Client {
    public static void main(String[] args){
        MyService myService = new MyService();
        myService.Serve("");
        myService.Serve("123");
        /**
         * 这样做的弊端是,依赖隐藏在函数内部,模块化、可用性、扩展性、易测性都不好。
         */
    }
}

在这个例子中,MyService 直接在 Serve() 方法内部 new 了一个 NotNullValidator 实例,进行参数检查。

这样做的问题在于:

  1. 扩展性差,如果需要其他校验器,就要在 MyService 内部做大调整了。
  2. 依赖管理困难,IValidator 实例的生命周期在 MyService 之内,无法管理。
  3. 可测性差,耦合太严重,依赖和行为完全堆到一起去了,没法做单元测试。

那么,有什么解决方法呢?
很简单,把 new NotNullValidator 这个操作拿出来就可以了。

3.2 构造子注入

同样的例子,我们可以在 MyService 内部定义一个 IValidator 成员变量,然后再 定义一个 MyService 的带参构造函数。

代码示例:

// IValidator.java
interface IValidator {
    public void validate(String param);
}

// NotnullValidator.java
class NotnullValidator implements IValidator{
    @Override
    public void validate(String param){
        System.out.println(this.getClass().getSimpleName()+ " validating param {"+param+"}");
        if(null == param || 0 == param.length()){
            System.out.println(this.getClass().getSimpleName() + " param can not be null or empty");
        }
    }
}

// MyService.java
class MyService {
    private IValidator validator;

    MyService(IValidator validator) {
        System.out.println("构造子注入");
        this.validator = validator;
    }

    void Serve(String param){
        System.out.println(this.getClass().getSimpleName() + " param = " + param);
        validator.validate(param);
    }
}

// Client.java
public class Client {
    public static void main(String[] args){
        NotnullValidator notnullValidator = new NotnullValidator();
        MyService myService = new MyService(notnullValidator);
        myService.Serve("");
        myService.Serve("123");
        /**
         * 依赖由外部管理,可测性变强了。
         * 这其实就是一种依赖注入方法。
         * 通过构造方法注入依赖,叫做"构造子注入"。
         */
    }
}

在这里,Client 自己 new 了一个 NotNullValidator 实例,通过构造方法传给新的 MyService 实例。

这样,类与类之间的依赖关系由外部管理,可测试性、可复用性都得到了很大的提升。

从应用程序的角度描述,依赖关系不再是 MyService 正向获得一个 IValidator 实现的实例,而是 Client 先实例化一个自己想要的 IValidator 实现,然后注入到 MyService,这就是“控制反转”(IoC)。

从 Client 的角度描述,Client 负责 IValidator 的实例化,并将其注入到 MyService,这就是“依赖注入”(DI)。

IoC其实就是把主动变被动,并有效的分离了对象和它所需要的外部资源,使得它们松散耦合,有利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。

除了通过构造器注入外,还有另外两种依赖注入方法。

3.3 设值注入

设值注入是利用类成员变量的 setter 方法进行依赖注入的一种方式。

代码示例:

// IValidator.java
interface IValidator {
    public void validate(String param);
}

// NotnullValidator.java
class NotnullValidator implements IValidator{
    @Override
    public void validate(String param){
        System.out.println(this.getClass().getSimpleName()+ " validating param {"+param+"}");
        if(null == param || 0 == param.length()){
            System.out.println(this.getClass().getSimpleName() + " param can not be null or empty");
        }
    }
}

// MyService.java
class MyService {
    private IValidator validator;

    void setValidator(IValidator validator) {
        System.out.println("设值注入");
        this.validator = validator;
    }

    void Serve(String param){
        System.out.println(this.getClass().getSimpleName() + " param = " + param);
        validator.validate(param);
    }
}

// Client.java
public class Client {
    public static void main(String[] args){
        NotnullValidator notnullValidator = new NotnullValidator();
        MyService myService = new MyService();
        myService.setValidator(notnullValidator);
        myService.Serve("");
        myService.Serve("123");
        /**
         * 通过成员变量的setter方法注入依赖,叫做"设值注入"
         */
    }
}

这里,为 MyService 的成员变量 validator 定义了setValidator() 方法,从而使得外部可以通过这个 setValidator() 方法注入 IValidator 依赖。

3.4 接口注入

接口注入是指,通过实现特定接口的依赖注入方法实现依赖注入的一种方式。

代码示例:

// IValidator.java
interface IValidator {
    public void validate(String param);
}

// MyInject.java
interface MyInject {
    public void inject(IValidator validator);
}

// NotnullValidator.java
class NotnullValidator implements IValidator{
    @Override
    public void validate(String param){
        System.out.println(this.getClass().getSimpleName()+ " validating param {"+param+"}");
        if(null == param || 0 == param.length()){
            System.out.println(this.getClass().getSimpleName() + " param can not be null or empty");
        }
    }
}

// MyService.java
class MyService implements MyInject {
    private IValidator validator;

    void Serve(String param){
        System.out.println(this.getClass().getSimpleName() + " param = " + param);
        validator.validate(param);
    }

    @Override
    public void inject(IValidator validator) {
        System.out.println("接口注入");
        this.validator = validator;
    }
}

// Client.java
public class Client {
    public static void main(String[] args){
        NotnullValidator notnullValidator = new NotnullValidator();
        MyService myService = new MyService();
        myService.inject(notnullValidator);
        myService.Serve("");
        myService.Serve("123");
        /**
         * 通过实现一个单独的依赖注入接口,实现依赖注入,叫做"接口注入"。
         * 不常见的用法,构造函数和setter就够用了。
         *
         * 通过构造函数、setter、接口注入依赖,仍然存在问题。
         * 加入一个类A依赖非常多的其他类,每一个都要手动new,然后set,太麻烦了。
         * 怎么办呢?上框架。
         */
    }
}

这里定义了一个 MyInject 接口,接口内部参数 inject() 用于注入 IValidator 。
MyService 实现了这个接口,Client 可以通过 inject() 注入IValidator 实例。

上面提到的三种注入方式:构造子注入、设值注入、接口注入,实际上都是提供一个“入口”函数,允许外部调用方向内注入 IValidator 依赖,只是形式不同罢了。

但是,外部系统管理这些 “new” 操作的话,如果接口内有多层传递依赖,或者依赖项比较多的时候,Client 负担就太重了。

怎么办呢?上框架。

IoC/DI 框架要做的事情,就是:new 什么,什么时候 new ,new 完了要给谁。。。

IoC/DI 框架有很多,这里主要介绍两个,Spring 和 Guice。

4. IoC/DI 框架

4.1 spring

IoC是spring的核心,对于spring框架来说,就是由 spring 来负责控制对象的生命周期和对象间的关系。

想要使用 spring ,至少需要两个核心包:group: 'org.springframework', name: 'spring-core'group: 'org.springframework', name: 'spring-context'

spring 通过一个叫做“容器”的东西来管理所有的 bean 对象。

当 Spring 应用程序被加载到内存中时,spring 框架通过读取 “配置元数据”,来完成对象的实例化、配置和组装。应用程序可以从容器中取bean,方法是getBean()

在这里,接口和类定义同上一章。
只是,额外增加 xml 配置文件用于定义 bean 的生命周期及依赖关系。

PS:“配置元数据” 可以通过 XML、Java注解、Java代码来标识,代码示例只给出xml配置。

代码有点多,我分开说哈。
记得以下几步:

  1. 引入上面说的两个包。
  2. 写 java 代码 定义 bean。
  3. 写 xml 配置文件。
  4. Client。

构建工具用的是gradle,所以这么引包:

// build.gradle
group 'com.ann.javas.topics'
version '1.0-SNAPSHOT'

apply plugin: 'java'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile group: 'org.springframework', name: 'spring-core', version: '4.3.11.RELEASE'
    compile group: 'org.springframework', name: 'spring-context', version: '4.3.11.RELEASE'
}

本例中,给出了5种xml配置方法:构造子注入、设值注入、自动装载byName,自动装载byType,自动装载constructor。

先列下java代码,bean 定义没变,就是Client.java里面的内容多了点:

// IValidator.java
public interface IValidator {
    public void validate(String param);
}

// NotnullValidator.java
public class NotnullValidator implements IValidator{
    @Override
    public void validate(String param) {
        System.out.println(this.getClass().getSimpleName()+ " validating param {"+param+"}");
        if(null == param || 0 == param.length()){
            System.out.println(this.getClass().getSimpleName() + " param can not be null or empty");
        }
    }
}

// MyService.java
public class MyService {
    private IValidator validator;

    public MyService(IValidator validator) {
        this.validator = validator;
    }

    public void setValidator(IValidator validator) {
        this.validator = validator;
    }

    public MyService() {
    }

    void Serve(String param){
        System.out.println(this.getClass().getSimpleName() + " param = " + param);
        validator.validate(param);
    }
}


// Client.java
public class Client {
    /**
     * 构造子注入 和 设值注入,唯一的区别就是标签中定义依赖的元素不同。
     * 构造子注入使用:constructor-arg,设值注入使用:property。
     *
     * @param args
     */
    public static void main(String[] args) {
        System.out.println("case1:测试spring容器构造子注入(基于XML配置)...");
        demo1();
        System.out.println("..................................................");

        System.out.println("case2:测试spring容器设值注入(基于XML配置)...");
        demo2();
        System.out.println("..................................................");

        System.out.println("case3:测试spring容器自动装载byName(基于XML配置)...");
        demo3();
        System.out.println("..................................................");

        System.out.println("case4:测试spring容器自动装载byType(基于XML配置)...");
        demo4();
        System.out.println("..................................................");

        System.out.println("case5:测试spring容器自动装载constructor(基于XML配置)...");
        demo5();
        System.out.println("..................................................");

    }

    private static void demo1(){
        // 读取配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("demo5_configioc-byxml-case1.xml");
        // 构造子注入实例
        MyService service = (MyService)context.getBean("s");
        service.Serve("");
        service.Serve("123");
    }

    private static void demo2(){
        // 读取配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("demo5_configioc-byxml-case2.xml");
        // 设值注入实例
        MyService service = (MyService)context.getBean("s");
        service.Serve("");
        service.Serve("123");
    }

    private static void demo3(){
        // 读取配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("demo5_configioc-byxml-case3.xml");
        // 自动装载,byName
        MyService service = (MyService)context.getBean("s");
        service.Serve("");
        service.Serve("123");
    }

    private static void demo4(){
        // 读取配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("demo5_configioc-byxml-case4.xml");
        // 自动装载,byType
        MyService service = (MyService)context.getBean("s");
        service.Serve("");
        service.Serve("123");
    }

    private static void demo5(){
        // 读取配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("demo5_configioc-byxml-case5.xml");
        // 自动装载,constructor
        MyService service = (MyService)context.getBean("s");
        service.Serve("");
        service.Serve("123");
    }
}

然后是xml配置文件,有5个:

// demo5_configioc-byxml-case1.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 配置两个 bean,并指明他们的依赖关系: MyService 依赖 NotnullValidator -->
    <!-- 被依赖的 NotnullValidator 会在 MyService 构造之前构造 -->
    <!-- 用 constructor-arg 配置,就是构造子注入 -->
    <bean id="s" class="com.ann.javas.topics.iocdi.demo5.usespring.MyService">
        <constructor-arg ref="p"/>
    </bean>
    <bean id="p" class="com.ann.javas.topics.iocdi.demo5.usespring.NotnullValidator">
    </bean>
</beans>

// demo5_configioc-byxml-case2.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 配置两个 bean,并指明他们的依赖关系: MyService 依赖 NotnullValidator -->
    <!-- 被依赖的 NotnullValidator 会在 MyService 构造之前构造 -->
    <!-- 用 property 配置,就是设值注入 -->
    <bean id="s" class="com.ann.javas.topics.iocdi.demo5.usespring.MyService">
        <property name="validator" ref="p"/>
    </bean>
    <bean id="p" class="com.ann.javas.topics.iocdi.demo5.usespring.NotnullValidator">
    </bean>
</beans>

// demo5_configioc-byxml-case3.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 配置两个 bean,并指明他们的依赖关系: MyService 依赖 NotnullValidator -->
    <!-- 被依赖的 NotnullValidator 会在 MyService 构造之前构造 -->
    <!-- 自动装载 byName,根据 id 或 name 查找 bean 并自动装配 -->
    <bean id="s" class="com.ann.javas.topics.iocdi.demo5.usespring.MyService" autowire="byName">
    </bean>
    <!-- 这里的 id 必须和 MyService 中定义的IValidator 变量名一致,否则 byName 无法识别 -->
    <bean id="validator" class="com.ann.javas.topics.iocdi.demo5.usespring.NotnullValidator">
    </bean>
</beans>

// demo5_configioc-byxml-case4.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 配置两个 bean,并指明他们的依赖关系: MyService 依赖 NotnullValidator -->
    <!-- 被依赖的 NotnullValidator 会在 MyService 构造之前构造 -->
    <!-- 自动装载byType,根据 class 查找并自动装配 bean -->
    <bean id="s" class="com.ann.javas.topics.iocdi.demo5.usespring.MyService" autowire="byType">
    </bean>
    <bean id="b" class="com.ann.javas.topics.iocdi.demo5.usespring.NotnullValidator">
    </bean>
</beans>

// demo5_configioc-byxml-case5.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 配置两个 bean,并指明他们的依赖关系: MyService 依赖 NotnullValidator -->
    <!-- 被依赖的 NotnullValidator 会在 MyService 构造之前构造 -->
    <!-- 自动装载 constructor,根据 bean 的构造函数定义进行查找并自动装配 -->
    <bean id="s" class="com.ann.javas.topics.iocdi.demo5.usespring.MyService" autowire="constructor">
    </bean>
    <bean id="b" class="com.ann.javas.topics.iocdi.demo5.usespring.NotnullValidator">
    </bean>
</beans>

代码中的注释有简单介绍,详细的内容就不再赘述~

4.2 guice

guice 是谷歌开发的轻量级 DI 框架,号称比 spring 快10倍,虽然我没测过,但确实蛮好用的就是了。

想要使用guice,需要引包:group: 'com.google.inject', name: 'guice'

guice 允许你使用 @Inject 进行注解构造函数的方式,来进行自动实例化构造参数。也可以通过实现 Module 的 configure(Binder binder) 方法定义依赖注入规则。

在程序启动时,你可以使用 Guice.createInjector(自定义Module) 来生成一个injector,之后就可以从 injector 中获取你需要 guice 为你实例化的类,方法是 injector.getInstance(你想要的类.class)

用guice非常简单:

  1. 引包。
  2. java代码。

同样gradle构建:

// build.gradle
group 'com.ann.javas.topics'
version '1.0-SNAPSHOT'

apply plugin: 'java'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile group: 'com.google.inject', name: 'guice', version: '4.0'
}

java代码:

// IValidator.java
interface IValidator {
    public void validate(String param);
}

// NotnullValidator.java
class NotnullValidator implements IValidator {
    @Override
    public void validate(String param){
        System.out.println(this.getClass().getSimpleName()+ " validating param {"+param+"}");
        if(null == param || 0 == param.length()){
            System.out.println(this.getClass().getSimpleName() + " param can not be null or empty");
        }
    }
}


// MyService.java
class MyService  {
    @Inject
    private IValidator validator;

    void Serve(String param){
        System.out.println(this.getClass().getSimpleName() + " param = " + param);
        validator.validate(param);
    }
}


// MyModule.java
public class MyModule implements Module{
    @Override
    public void configure(Binder binder) {
        System.out.println("guice注入规则,绑定 IValidator 到 NotnullValidator");
        binder.bind(IValidator.class).to(NotnullValidator.class);
    }
}


// Client.java
public class Client {
    public static void main(String[] args){
        // 定义依赖注入规则
        MyModule module = new MyModule();

        // 根据注入规则,生成 injector
        Injector injector = Guice.createInjector(module);

        // 获取 MyService 实例
        MyService myService = injector.getInstance(MyService.class);

        // 来两个case
        myService.Serve("");
        myService.Serve("123");
    }
}

如你所见,MyModule中定义了依赖注入规则,将 IValidator 绑定到 NotnullValidator。这样,当某个类需要一个IValidator的时候,guice就会给它一个NotNullValidator实例。

其他的请看注释说明~

上一篇 下一篇

猜你喜欢

热点阅读