美文网首页
浅谈 IoC / DI

浅谈 IoC / DI

作者: 李眼镜 | 来源:发表于2017-10-24 14:42 被阅读0次

    1.背景

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

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

    找两个定义:

    • IoC,全称 Inverse of Control,控制反转。是指,由容器/第三方系统(例如 spring )来控制业务对象之间的依赖关系,而不是像传统方式中由代码来直接控制。

    • DI,全称 Dependency Injection,依赖注入。是指,自身对象中的内置对象是通过注入的方式进行创建。

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

    2.举个“吃饭”的例子

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

    沙县小吃:

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

    学校食堂:

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

    区别在哪呢?

    • “沙县小吃”是由顾客到店后主动要求“我要吃什么”,然后餐馆才去 new 一个你要吃的菜。
    • “学校食堂”是由食堂提前 new 很多可以提供给顾客吃的菜,顾客来了之后直接挑选装盘就可以了。

    3.再举个例子

    场景:接口参数检查

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

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

    3.1 传统实现,new

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

    • IValidate.java,接口定义了 validate() 方法,对一个 String 类型参数做检查。
    • NotnullValidator.java 实现了 IValidate 接口的validate()方法,要求参数不能为空。
    • MyService.java,定义了一个Serve() 方法,用来接收外部参数,然后对参数做非空检查。
    • Client.java,模拟调用方,请求MyService。

    代码示例:

    // 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实例。

    其他的请看注释说明~

    相关文章

      网友评论

          本文标题:浅谈 IoC / DI

          本文链接:https://www.haomeiwen.com/subject/yiktpxtx.html