彻底理解spring框架当中的依赖注入(DI)与控制反转(IOC)理念

什么是依赖注入

人生当中第一次听说到这个概念是在spring框架的学习当中,当然依赖注入并不局限于spring,其实依赖注入早已不是一个新鲜词,而是一个犹如古董般的设计理念,但是我还年轻呐那么就从这里终结他吧!

DI不是凭空想出来的,是逐渐的降低耦合度发展出来的,我们这里也尝试一下这个发展过程,从对象依赖、接口依赖、到依赖注入三方面帮助大家理解的更为清晰。

对象依赖

在A里用到B类的实例化构造,就可以说A依赖于B,这个会导致什么问题了?:

1.A要使用B必须了解B的所有内容细节,B可能提供了n个方法,但是A实际上只需要用到其中2个,其它的和A完全无关。
2.B发生任何变化都会影响到A,开发A和开发B的人可能不是一个人,B把一个A需要用到的方法参数改了,B的修改能编译通过,能继续用,但是A就跑不起来了。
3.最重要的是,如果A想把B替换成C,则A的改动会非常大。A是服务使用者,B是提供一个具体服务的,C也能提供类似服务,但是A已经严重依赖于B了,想换成C非常之困难。

一个具体的例子,B是文件存储类,A使用B来存储内容,C是网盘存储类,我已经用了B来存储资源到我们自己的服务器硬盘,现在想换成存到网盘上,对于下面的例子来说,当然简单,在实际项目中几乎就是大改动了,带来很大风险。
示例代码如下:
//B类
Class B{
    public string writeContentToFile(string content){
          //把content写到一个随机名字的文件path里,然后把文件名返回
          return path;
   }
}
Class C{
    public string writeContentToPan(string content){
          //把content写到一个网盘里,然后把url返回
          return url;
   }
}
//A类
Class A{
    static void Main(string[] args)
    {
          B b = new B();
          string path = b.writeContentToFile("哈哈");
          Console.WriteLine(path);
        //如果要换成C,代码都改了
          C c = new C();
          string url=c.writeContentToPan("哈哈");
          Console.WriteLine(url);
    }
}

接口依赖

学过面向对象的同学马上会知道可以使用接口来解决这几个问题,还是上面的例子,如果早期实现类B的时候就定义了一个接口叫IWriter,B和C都会实现这个接口里的方法,这样从B切换到C就只需改一行了:


//IWriter接口
interface IWriter{
    public string write(string content);
}
//B类
Class B : IWriter{
    public string write(string content){
        //写到我们自己服务器的硬盘上并返回一个url指向这个文件
        return 服务器rooturl+writeContentToFile(content);
    }
    private string writeContentToFile(string content){
          //把content写到一个随机名字的文件path里,然后把文件名返回
          return path;
   }
}
Class C: IWriter{
    public string write(string content){
        return writeContentToPan(content);
    }
    public string writeContentToPan(string content){
          //把content写到一个网盘里,然后把url返回
          return url;
   }
}
//A类
Class A{
    static void Main(string[] args)
    {
          IWriter writer = new B();
         //从B改成C只需要把上面一行的B改成C就可以了
          string url= writer.write("哈哈");
          Console.WriteLine(url);
    }
}

A对B对C的依赖变成对IWriter的依赖了,上面说的几个问题都解决了。但是目前还是得实例化B或者C,因为new只能new对象,不能new一个接口,还不能说A彻底只依赖于IWriter了。从B切换到C还是需要重新编译和发布A,能做到更少的依赖吗?能做到A在运行的时候想切换B就B,想切换C就C,不用改任何代码甚至还能支持以后切换成D吗?

依赖注入(DI)

我们先不考虑DI这个概念,我们先用我们自己的方法来解决上面的问题,这里就需要用到反射,反射的概念大家可以仔细去了解一下,当然也可以参见我的另一篇博客:浅谈Java反射与框架

我们这里先简单认为反射可以实现通过加载一个类的名称字符串来运行时动态new一个对象,Java和C#都有类似的功能,我们假定这个方法叫 NewInstanceByString,看看下面的代码示例:

IWriter b = new B();
IWriter b =  NewInstanceByString("B"); //等同于上一句代码
IWriter c = new C();
IWriter c =  NewInstanceByString("C"); //等同于上一句代码

 

有了反射我们就能解决上面的问题
我们需要增加一个配置文件config.json
IWriter、B和C没有变化

//A类
Class A{
    static void Main(string[] args)
    {
          //如果config.json里存的B就会new一个B对象,存的C就会new一个C对象
          IWriter writer = NewInstanceByString(ReadFile("config.json"));
          string url= writer.write("哈哈");
          Console.WriteLine(url);
    }
}

 

大家可以看到,我想从B切换到C或者D,只需要修改config.json就可以了,源码完全不用修改,A做到只依赖于IWriter了,这就是DI实现的基本思路。其中注入的意思就是在运行时动态实例化一个对象,就像打针一样注入到这个对象的使用者A,对于A来说并不需要知道是B还是C还是D被注入了。

针对这种情况的基础上理论化这种思想,创建了一些大家达到共识的概念,创立了DI这个大家都认可的标准。这个思想是和开发语言无关的。

DI的基本概念是容器,这个容器用于注册接口和对应的实现,A从容器中根据接口来获取实现,具体的实现是那个类不需要了解,用完怎么释放也不需要管。
我们接下来看这三个阶段A的依赖变化情况(假定A需要用到2个功能,每个功能有三种类似的实现方式):


 

 通过DI,最后功能使用者A只依赖于容器和接口,不会直接再依赖于具体的实现了。

再说说控制反转IOC

首先:IOC(Inverse of Contro)控制反转,有时候也被称为DI(Dependence Injection)依赖注入,它是一种降低对象耦合关系的一种设计思想。其实在spring框架中DI与IOC说的其实是一回事

一句话本来我接受各种参数来构造一个对象,现在只接受一个参数——已经实例化的对象。

因为这个对象在注入的时候已经被实例化了,所以我们不需要去用传统的方式去New对象,通常情况下,被注入对象会直接依赖于被依赖对象。但是,在IOC的场景中,二者之间通过IOC Service Provider 来打交道,所有的被注入对象和依赖对象现在由IOC Service Provider统一管理。被注入对象需要什么,直接跟IOC Service Provider招呼一声,后者就会把相应的被依赖对象注入到被关注对象中,从而达到IOC Service Provider为被注入对象服务的目的。IOC Service Provider在这里就是通常的IOC容器额外充当的角色。从被注入对象的角度看,与之前直接寻求依赖对象相比,依赖对象的取得方式发生了反转,控制也从被注入对象转到了IOC Service Provider那里。

其实IOC就这么简单!原来是需要什么东西自己去拿,现在是需要什么东西就让别人送过来。

在spring全家桶尤其是spring boot中很少见手动去new对象就是这个原因。构造它这个『控制』操作也交给了第三方,也就是控制反转IOC,我们见下图

说了老半天也就是我们所需要的对象是注入进来的,从什么地方注入而来?从注册中心(婚介所)那获取到被依赖的对象,而和它的构造方式解耦了。

在spring框架当中我们所需要的每一个对象可以将其注册为组件component,将其一个个放在注册中心当中,而这个注册中心就是IOC容器

👆👆讲的太好了有木有b( ̄▽ ̄)d ,我相信上面的内容已经让你对IOC有了足够的了解,如果你有耐心那么请接着看

下面我们再从面向对象的设计模式层面去一步一步的揭开IOC的神秘面纱,更具体的多维的去理解IOC的设计思想

一般的项目开发思想

一般而言,在开发中使用分层体系结构,都是上层调用下层的接口,上层依赖下层的执行,这就使得调用者依赖被调用者。所以现在调用者和被调用者之间就存在十分紧密得联系,假如现在一方要进行变动,那么就会导致程序出现较大得变动,显然这不合适,这样降低了程序得可扩展性。

      . 举个例子:现在要给一家卖茶叶得商家做一个管理系统:该商家一开始只卖绿茶,但是现在业务扩展了要开始卖红茶,传统的方法我们会针对茶抽象出一个基类,绿茶只需要继承该基类即可。

 

 

采用上面的方法实现后,在需要GreenTea的时候只需要执行以下的代码即可,AbstracTea tea = new GreenTea(); 虽然这种方式是可以满足设计要求的,但是明显存在可扩展性不好的缺点,假如现在商家发现绿茶销售不好,开始销售红茶,那么理论上是只需要实现一个Black类,并且让这个类继承AbstracTeam类即可,但是,系统中所有用到了AbstracTea tea = new GreenTea();的地方都需要改编成AbstracTea tea = new BlackTea();而这种创建实例的方式往往会导致程序的改动量非常的大。

项目中引入工厂模式

那么问题来了,怎么做才能够增强系统的可扩展性呢?首先我们能想到的方式是采用工厂模式把创建对象的行为包装起来:

 

通过上面的方式,我们把创建对象的过程委托给TeaFactory来完成,在需要使用Tea对象的时候只需要调用工厂类的gettea方法即可,具体创建对象的逻辑我们放在工厂类中去实现,当商家需要改变茶的类别时候,我们只需要去改动工厂类中创建对象的逻辑即可,这样就满足了系统的可扩展性。

我们再举一个实际的工厂模式实现的例子

我们将创建一个 Shape 接口和实现 Shape 接口的实体类。下一步是定义工厂类 ShapeFactory

FactoryPatternDemo,我们的演示类使用 ShapeFactory 来获取 Shape 对象。它将向 ShapeFactory 传递信息(CIRCLE / RECTANGLE / SQUARE),以便获取它所需对象的类型。

 

 

创建一个接口

Shape.java

public interface Shape { void draw(); }

 

创建实现接口的实体类

Rectangle.java

public class Rectangle implements Shape {
 
   @Override
   public void draw() {
      System.out.println("Inside Rectangle::draw() method.");
   }
}

 

Square.java

public class Square implements Shape {
 
   @Override
   public void draw() {
      System.out.println("Inside Square::draw() method.");
   }
}

 

Circle.java

public class Circle implements Shape {
 
   @Override
   public void draw() {
      System.out.println("Inside Circle::draw() method.");
   }
}

 

创建一个工厂,生成基于给定信息的实体类的对象

ShapeFactory.java

public class ShapeFactory {
    
   //使用 getShape 方法获取形状类型的对象
   public Shape getShape(String shapeType){
      if(shapeType == null){
         return null;
      }        
      if(shapeType.equalsIgnoreCase("CIRCLE")){
         return new Circle();
      } else if(shapeType.equalsIgnoreCase("RECTANGLE")){
         return new Rectangle();
      } else if(shapeType.equalsIgnoreCase("SQUARE")){
         return new Square();
      }
      return null;
   }
}

 

使用该工厂,通过传递类型信息来获取实体类的对象

FactoryPatternDemo.java

public class FactoryPatternDemo {
 
   public static void main(String[] args) {
      ShapeFactory shapeFactory = new ShapeFactory();
 
      //获取 Circle 的对象,并调用它的 draw 方法
      Shape shape1 = shapeFactory.getShape("CIRCLE");
 
      //调用 Circle 的 draw 方法
      shape1.draw();
 
      //获取 Rectangle 的对象,并调用它的 draw 方法
      Shape shape2 = shapeFactory.getShape("RECTANGLE");
 
      //调用 Rectangle 的 draw 方法
      shape2.draw();
 
      //获取 Square 的对象,并调用它的 draw 方法
      Shape shape3 = shapeFactory.getShape("SQUARE");
 
      //调用 Square 的 draw 方法
      shape3.draw();
   }
}

 

执行程序,输出结果

Inside Circle::draw() method.
Inside Rectangle::draw() method.
Inside Square::draw() method.

 

好了我们终于又来到了IOC容器

上面提到的工厂模式虽然增强了系统的可扩展性,但是从本质上来讲,工厂模式只不过是把会变动的逻辑移动到了工厂类里里面,当系统类较多的时候,系统的扩展就使得系统经常需要改变工厂类中的代码,但是我们采用了IOC设计思想后,程序就会有更好的可扩展性:

 

 

Spring容器将会根据配置文件来1.创建调用者对象,2.同时把被调用的对象的实例化对象通过构造函数或者set()方法的形式注入到调用者对象当中。

首先创建Spring的配置文件

<beans>
   <bean id = "sale" class = "Sale" singleton = "false">
      <constrctor-arg>
           <ref bean = "tea"/>
      </constrctor-arg>
   </bean>
   <bean id = "tea" class = "Bluetea" singleton = "false"/>
</beans>

在实现sale类的时候,需要按照下面的方式进行

class Sale{
    private AbstracTea tea;
public Sale(AbstracTea tea){ this.tea = tea; } }

当spring容器创建sale对象的时候,就会根据配置文件创建一个BlueTea对象,作为Sale构造函数的参数。当需要把BlueTea改成BlackTea时,只需要修改上述配置文件,而不需要修改代码。

需要使用Sale时候,可以通过下面的方式来创建sale对象

ApplicationContext ttt = new FileSystemXmlApplicationContext("配置文件");
Sale s = (Sale)ctx.getBean("sale");

 


 

IOC大结局

IOC的优势:

在应用程序中的组件需要获取资源时,传统的方式是组件主动的从容器中获取所需要的资源,在这样的模式下开发人员往往需要知道在具体容器中特定资源的获取方式,增加了学习成本,同时降低了开发效率。反转控制的思想完全颠覆了应用程序组件获取资源的传统方式:反转了资源的获取方向——改由容器自动的将资源推送给需要的组件,开发人员不需要知道容器是如何创建资源对象的,只需要提供接收资源的方式即可,极大的降低了学习成本,提高了开发的效率。这种行为也称为查找的被动形式。

1.通过IOC容器,开发人员不需要关注对象是如何创建的,同时增加新类也非常方便,只需要修改配置文件即可实现对象的热插拔。

2.IOC容器可以通过配置文件来确定需要注入的实例化对象,因此非常便于进行单元测试。

IOC缺点:

1.对象是通过反射机制实例化出来的,因此对系统的性能有一定的影响。

2.创建对象的流程变得复杂。

3.过度设计:有没有过度设计的嫌疑?小伙伴说我就用.NET Core做一个业务系统,我都不用任何接口,直接用什么就new什么不行吗?有必要理解和使用DI吗?

其实不管是用Java还是.Net Core,都是面向对象的语言,依赖抽象而不依赖具体实现已经成为一个基本共识,包括NetCore的框架,包括nuget上许多好用的库,也都是基于DI来设计和完成的,我们没有理由不去理解和使用它。我的想法是需要有这种思维习惯,不是说写任何功能都需要先定义接口,使用DI,而是在写强依赖的代码的时候,停下来花一分钟下意识的想想有这里的逻辑有没有变化和扩展的可能,想想我写的功能有没有可能被别人使用,有没有必要降低耦合度。
从工作量上来说,一开始就有这种想法和习惯,这种代码的改动不会费劲和有风险。相反,如果到了项目快结束或者做完了,才碰见那种从B要切换到C的情况,就抓瞎了。




 

参考链接1:https://blog.csdn.net/qq_38735934/article/details/81074852

参考链接2:https://www.runoob.com/design-pattern/factory-pattern.html

参考链接3:https://www.jianshu.com/p/c0bbf59671b7



 

posted @ 2019-10-26 17:33  月半大熊猫  阅读(502)  评论(0编辑  收藏  举报