浅析IOC和DI

概念

  • IOC(Inversion of Control): 其思想是反转资源获取的方向.传统的资源查找方式要求组件向容器发起请求查找资源.作为回应,容器适时的返回资源。而应用了IOC之后,则是容器主动地将资源推送给它所管理的组件,组件所要做的仅是选择一种合适的方式来接受资源.这种形式也被称为查找的被动形式。

    • 控制什么? 控制对象的创建及销毁(生命周期)
    • 反转什么? 对象的控制权被反转了,将对象的控制权交给IOC容器
  • DI(Dependency Injection)——IOC的另一种表述方式:即组件以一些预先定义好的方式(如:setter方法)接受来自肉容器的资源注入.相对于IOC而言,这种表述更直接.

为何是反转,哪些方面反转了:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。

IoC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。

  • 依赖注入:是组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。

  • 理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:

    • 谁依赖于谁:当然是某个容器管理对象依赖于IoC容器;“被注入对象的对象”依赖于“依赖对象”;

    • 为什么需要依赖:容器管理对象需要IoC容器来提供对象需要的外部资源;

    • 谁注入谁:很明显是IoC容器注入某个对象,也就是注入“依赖对象”;

    • 注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

IoC和DI由什么关系呢?其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”,相对IoC 而言,“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。

分析实际

假设有个人叫张三,他有一辆奥迪车。张三下班的时候要开车回家,那么他对奥迪便产生了一种依赖关系。

Audi和ZhangSan的实现类:

package com.iocstudy;

public class Audi {
    
    public void start() {
        System.out.println("Audi.start");
    }
    
    public void turnLeft() {
        System.out.println("Audi.turnLeft");
    }
    
    public void turnRight() {
        System.out.println("Audi.turnRight");
    }
    
    public void stop() {
        System.out.println("Audi.stop");
    }   
}
package com.iocstudy;

public class ZhangSan {
    
    public void goHome(){
        Audi audi = new Audi();
        
        audi.start();
        audi.turnLeft();
        audi.turnRight();
        audi.turnRight();
        audi.stop();
    }
}

所以,张三要回家的话,需要

  • 创建一辆车
     Audi audi = new Audi();
    
  • 控制车辆启动、直行、左右转
    audi.start();
    audi.turnLeft();
    audi.turnRight();
    audi.turnRight();
    audi.stop();
    

那么,张三用车肯定不只是用来回家。还可以买东西、约会、飙车等等。 这时候,代码就该修改为:

package com.iocstudy;

public class ZhangSan {
    
    public void goHome(){
        Audi audi = new Audi();
        
        audi.start();
        audi.turnLeft();
        audi.turnRight();
        audi.turnRight();
        audi.stop();
    }

     public void goShop(){
        Audi audi = new Audi();
        
        audi.start();
        audi.turnLeft();
        audi.turnRight();
        audi.turnRight();
        audi.stop();
    }
}

这时候,张三换车了,换成一辆BMW

package com.iocstudy;

public class BMW {
    
    public void start() {
        System.out.println("Audi.start");
    }
    
    public void turnLeft() {
        System.out.println("Audi.turnLeft");
    }
    
    public void turnRight() {
        System.out.println("Audi.turnRight");
    }
    
    public void stop() {
        System.out.println("Audi.stop");
    }
}

那么这时候要把所有ZhangSan类中的引用Audi的代码都改为BMW。 这体现出来了车和张三之间的高耦合性。其次,这在设计时有一些问题:

  • 张三所有的行为都需要自己主动创建一辆车。
  • 更换车辆的代价是巨大的。要把所有奥迪车的引用都换为BMW。

这时候可以在张三类中进行一些改进,把他的车提到张三的属性域中,这样只需修改属性域中的车辆就可以了:

package com.iocstudy;

public class ZhangSan {
    
    private BMW bmw = new BMW();
    
    public void goHome(){
        audi.start();
        audi.turnLeft();
        audi.turnRight();
        audi.turnRight();
        audi.stop();
    }
    
    public void goShop(){
      audi.start();
      audi.turnLeft();
      audi.turnRight();
      audi.turnRight();
      audi.stop();
    }
}

那么接着思考:

  • 张三需要的事一辆奥迪车?还是一辆宝马车?还是只是一辆车? 张三只需要回家和买东西,只是需要一辆车而已。
  • 张三会制造(创建)车辆吗? 不会。车辆不应该由张三进行创建。

解决方法:抽象出一个车的接口,并且车辆不由张三创建,而是在构造函数中以参数的形式传入。

定义接口:

package com.iocstudy;

public interface Car {
    
    public void start();
    
    public void turnLeft();
    
    public void turnRight();
    
    public void stop();
}

让奥迪和宝马实现接口:

package com.iocstudy;

public class Audi implements Car{
    
    public void start() {
        System.out.println("Audi.start");
    }
    
    public void turnLeft() {
        System.out.println("Audi.turnLeft");
    }
    
    public void turnRight() {
        System.out.println("Audi.turnRight");
    }
    
    public void stop() {
        System.out.println("Audi.stop");
    }
    
}
package com.iocstudy;

public class BMW implements Car{
    
    public void start() {
        System.out.println("Audi.start");
    }
    
    public void turnLeft() {
        System.out.println("Audi.turnLeft");
    }
    
    public void turnRight() {
        System.out.println("Audi.turnRight");
    }
    
    public void stop() {
        System.out.println("Audi.stop");
    }
}

接着回到张三这个类,张三只是需要一辆车,车不应该由张三创建。

package com.iocstudy;

public class ZhangSan {
    
    private Car car;
    
    ZhangSan(Car car){
        this.car = car;
    }
    
    public void goHome(){
        audi.start();
        audi.turnLeft();
        audi.turnRight();
        audi.turnRight();
        audi.stop();
    }
    
     public void goShop(){
        audi.start();
        audi.turnLeft();
        audi.turnRight();
        audi.turnRight();
        audi.stop();
    }
}

那么接着思考一下:既然车不应该由张三创建,那么应该由谁创建呢? 那就是今天的主角:**IOC容器**

实现一个自己的IOC容器

场景模拟

延续上一节中场景的例子,新增一个Human接口,和HumanWithCar类(有车一族类),张三和李四继承自有车一族类。如下图:

在使用IOC管理这个场景之前,进行一些约定,以便简化IOC的业务逻辑:

  • 所有的Bean的声明周期交给IOC容器管理
  • 所有被依赖的Bean通过构造方法进行注入。(其实除了构造方法注入,还有setter方法注入)
  • 被依赖的Bean需要优先创建。(比如要创建张三,那么张三依赖的奥迪需要先创建了并且交由IOC容器管理了)

这是新的场景模拟的代码: Car接口

package com.iocstudy.car;

public interface Car {

    public void start();

    public void turnLeft();

    public void turnRight();

    public void stop();
}

奥迪实现类:

package com.iocstudy.car;

public class Audi implements Car{

    public void start() {
        System.out.println("Audi.start");
    }

    public void turnLeft() {
        System.out.println("Audi.turnLeft");
    }

    public void turnRight() {
        System.out.println("Audi.turnRight");
    }

    public void stop() {
        System.out.println("Audi.stop");
    }
}

宝马实现类:

package com.iocstudy.car;

public class BMW implements Car{

    public void start() {
        System.out.println("Audi.start");
    }

    public void turnLeft() {
        System.out.println("Audi.turnLeft");
    }

    public void turnRight() {
        System.out.println("Audi.turnRight");
    }

    public void stop() {
        System.out.println("Audi.stop");
    }
}

Human接口:

package com.iocstudy.Human;

public interface Human {
    public void goHome();
    public void goShop();
}

HumanWithCar类,它要实现接口中的goHome方法和goShop方法,但是有车一族是一个笼统的概念,张三和李四都是有车一族,他们回家和购物的路线和方式肯定不是一样的,要执行的方法的具体内容不一样。所以,这两个方法应该被声明为虚方法,HumanWithCar类也应是抽象类。

package com.iocstudy.Human;

import com.iocstudy.car.Car;

public abstract class HumanWithCar implements Human {

    protected Car car;

    HumanWithCar(Car car){
        this.car = car;
    }

    public abstract void goHome();

    public abstract void goShop();
}

张三和李四:

package com.iocstudy.Human;

import com.iocstudy.car.Car;

public class ZhangSan extends HumanWithCar {
    public ZhangSan(Car car) {
        super(car);
    }

    @Override
    public void goHome() {
        car.start();
        car.turnLeft();
        car.turnRight();
        car.stop();
    }

    @Override
    public void goShop() {
        car.start();
        car.turnRight();
        car.stop();
    }
}
package com.iocstudy.Human;

import com.iocstudy.car.Car;

public class LiSi extends HumanWithCar {
    public LiSi(Car car) {
        super(car);
    }

    @Override
    public void goHome() {
        car.start();
        car.turnLeft();
        car.stop();
    }

    @Override
    public void goShop() {
        car.start();
        car.turnRight();
        car.turnLeft();
        car.stop();
    }
}

然后要书写IoC容器,按照约定,

  • 所有的Bean的声明周期交给IOC容器管理
  • 所有被依赖的Bean通过构造方法进行注入。
  • 被依赖的Bean需要优先创建。

所以,

  • 容器要能实例化bean
  • 实例化后要保存bean
  • 要能够提供bean
  • 每个bean要产生一个id与之相对应

那么,

  • 保存bean:要有私有域来存储IoC创建好的bean(使用Map存储)
  • 提供bean:提供一个getBean()方法
  • 实例化bean:提供一个setBean()方法,向Map中增加bean

创建容器类:

package com.iocstudy;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class IoCContainer {
    private Map<String,Object> beans = new ConcurrentHashMap<String, Object>();

    /**
     * 根据beanId获取一个bean
     * @param beanId beanId
     * @return 返回bean
     */
    public Object getBean(String beanId){
        return beans.get(beanId);
    }

    /**
     * 委托ioc容器创建一个bean
     * @param clazz 要创建的bean的class
     * @param beanId beanId
     * @param paramBeanIds 要创建的bean的class的构造方法所需要的参数的beanId(即要创建的bean所需要的依赖bean)
     */
    public void setBeans(Class<?> clazz,String beanId,String... paramBeanIds){

    }
}

接下来就是实现setBeans方法:

  • 因为是根据构造方法创建一个bean,构造方法是需要参数值的,所以要组装构造方法所需要的参数值
  • 调用构造方法,实例化bean
  • 将实例化的bean放到Map中
 public void setBeans(Class<?> clazz,String beanId,String... paramBeanIds){
        // 1 组装构造方法所需要的参数值
        Object[] paramValues = new Object[paramBeanIds.length];
           //因为约定中所有的被依赖的bean需要被优先创建,所以需要的bean从beans中取就可以了
        for (int i = 0; i < paramBeanIds.length; i++) {
            paramValues[i] = beans.get(paramBeanIds[i]);
        }
        // 2 调用构造方法,实例化bean
        Object bean = null; //先定义好最重要实例化的bean
            //循环使用要创建的bean的构造方法
        for (Constructor<?> constructor : clazz.getConstructors()) {
            try {
                bean = constructor.newInstance(paramValues);
                //这里不处理异常。如果最终所有的构造方法都不能完成实例化,则bean为null
            } catch (InstantiationException e) {
            } catch (IllegalAccessException e) {
            } catch (InvocationTargetException e) {
            }
        }
        //如果最终没有实例化,则抛出错误
        if(bean == null){
            throw new RuntimeException("找不到合适的构造方法实例化bean");
        }
        // 3 将实例化的bean放到Map中
        beans.put(beanId,bean);
    }

使用单元测试类进行测试:

package com.iocstudy;

import com.iocstudy.Human.Human;
import com.iocstudy.Human.LiSi;
import com.iocstudy.Human.ZhangSan;
import com.iocstudy.car.Audi;
import com.iocstudy.car.BMW;
import org.junit.Before;
import org.junit.Test;

public class springIocTest {
    private IoCContainer ioCContainer = new IoCContainer();

    @Before
    //向容器中注册bean
    public void before(){
        ioCContainer.setBeans(Audi.class,"audi");
        ioCContainer.setBeans(BMW.class,"BMW");
        ioCContainer.setBeans(ZhangSan.class,"zhangsan","audi");
        ioCContainer.setBeans(LiSi.class,"lisi","BMW");
    }

    @Test
    public void test()  {
        Human zhangsan  = (Human) ioCContainer.getBean("zhangsan");
        zhangsan.goHome();
        Human lisi  = (Human) ioCContainer.getBean("lisi");
        lisi.goHome();
    }
}

打印结果:

Audi.start
Audi.turnLeft
Audi.turnRight
Audi.stop
Audi.start
Audi.turnLeft
Audi.stop

这样就实现了一个最简单的IoC容器。

这样做的好处:

  • 所有的依赖关系被集中统一的管理起来,清晰明了。在Before中将所有的依赖关系先集中管理起来。
  • 每个类只需要关注于自己的业务逻辑。例如在张三的代码中,张三只需要关注goHome()的方法,而不需要关心他的车是什么,只需要IoC容器提供一辆就可以了。
  • 修改依赖关系是很简单的事情。例如,张三现在依赖的是奥迪车,那么在setBean中只需修改为BMW,他就依赖于宝马了。

那么再回过头来看看IOC和DI的理解:

在没有使用Spring的时候,每个对象在需要使用他的合作对象时,自己均要使用像new object() 这样的语法来将合作对象创建出来,这个合作对象是由自己主动创建出来的,创建合作对象的主动权在自己手上,自己需要哪个合作对象,就主动去创建,创建合作对象的主动权和创建时机是由自己把控的,而这样就会使得对象间的耦合度高了,A对象需要使用合作对象B来共同完成一件事,A要使用B,那么A就对B产生了依赖,也就是A和B之间存在一种耦合关系,并且是紧密耦合在一起,而使用了Spring之后就不一样了,创建合作对象B的工作是由Spring来做的,Spring创建好B对象,然后存储到一个容器里面,当A对象需要使用B对象时,Spring就从存放对象的那个容器里面取出A要使用的那个B对象,然后交给A对象使用,至于Spring是如何创建那个对象,以及什么时候创建好对象的,A对象不需要关心这些细节问题(你是什么时候生的,怎么生出来的我可不关心,能帮我干活就行),A得到Spring给我们的对象之后,两个人一起协作完成要完成的工作即可。 所以控制反转IoC(Inversion of Control)是说创建对象的控制权进行转移,以前创建对象的主动权和创建时机是由自己把控的,而现在这种权力转移到第三方,比如转移交给了IoC容器,它就是一个专门用来创建对象的工厂,你要什么对象,它就给你什么对象,有了 IoC容器,依赖关系就变了,原先的依赖关系就没了,它们都依赖IoC容器了,通过IoC容器来建立它们之间的关系。 这是我对Spring的IoC(控制反转)的理解。DI(依赖注入)其实就是IOC的另外一种说法,DI是由Martin Fowler 在2004年初的一篇论文中首次提出的。他总结:控制的什么被反转了?就是:获得依赖对象的方式反转了。

参考

  • https://www.imooc.com/video/19046

  • https://blog.csdn.net/bestone0213/article/details/47424255

  • http://sishuok.com/forum/blogPost/list/2427.html

  • https://blog.csdn.net/yqj2065/article/details/80450929