依赖注入和控制反转

来源:互联网 发布:金十数据新闻 编辑:程序博客网 时间:2024/06/07 08:37

学习过spring框架的人一定都会听过Spring的IoC(控制反转) 、DI(依赖注入)这两个概念,对于初学Spring的人来说,总觉得IoC 、DI这两个概念是模糊不清的,是很难理解的,今天和大家分享网上的一些技术大牛们对Spring框架的IOC的理解以及谈谈我对Spring Ioc的理解。

一、分享Iteye的开涛对Ioc的精彩讲解

  首先要分享的是Iteye的开涛这位技术牛人对Spring框架的IOC的理解,写得非常通俗易懂,以下内容全部来自原文,原文地址:http://jinnianshilongnian.iteye.com/blog/1413846

1.1、IoC是什么

  Ioc—Inversion of Control,即“控制反转”,不是什么技术,而是一种设计思想。在Java开发中,Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。如何理解好Ioc呢?理解好Ioc的关键是要明确“谁控制谁,控制什么,为何是反转(有反转就应该有正转了),哪些方面反转了”,那我们来深入分析一下:

  ●谁控制谁,控制什么传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对 象的创建;谁控制谁?当然是IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)

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

  用图例说明一下,传统程序设计如图2-1,都是主动去创建相关对象然后再组合起来:

图1-1 传统应用程序示意图

  当有了IoC/DI的容器后,在客户端类中不再主动去创建这些对象了,如图2-2所示:

图1-2有IoC/DI容器后程序结构示意图

1.2、IoC能做什么

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

  其实IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。

  IoC很好的体现了面向对象设计法则之一—— 好莱坞法则:“别找我们,我们找你”;即由IoC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。

1.3、IoC和DI

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

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

  ●谁依赖于谁:当然是应用程序依赖于IoC容器

  ●为什么需要依赖:应用程序需要IoC容器来提供对象需要的外部资源

  ●谁注入谁:很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象

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

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

  看过很多对Spring的Ioc理解的文章,好多人对Ioc和DI的解释都晦涩难懂,反正就是一种说不清,道不明的感觉,读完之后依然是一头雾水,感觉就是开涛这位技术牛人写得特别通俗易懂,他清楚地解释了IoC(控制反转) 和DI(依赖注入)中的每一个字,读完之后给人一种豁然开朗的感觉。我相信对于初学Spring框架的人对Ioc的理解应该是有很大帮助的。

二、分享Bromon的blog上对IoC与DI浅显易懂的讲解

2.1、IoC(控制反转)

  首先想说说IoC(Inversion of Control,控制反转)。这是spring的核心,贯穿始终。所谓IoC,对于spring框架来说,就是由spring来负责控制对象的生命周期和对象间的关系。这是什么意思呢,举个简单的例子,我们是如何找女朋友的?常见的情况是,我们到处去看哪里有长得漂亮身材又好的mm,然后打听她们的兴趣爱好、qq号、电话号、ip号、iq号………,想办法认识她们,投其所好送其所要,然后嘿嘿……这个过程是复杂深奥的,我们必须自己设计和面对每个环节。传统的程序开发也是如此,在一个对象中,如果要使用另外的对象,就必须得到它(自己new一个,或者从JNDI中查询一个),使用完之后还要将对象销毁(比如Connection等),对象始终会和其他的接口或类藕合起来。

  那么IoC是如何做的呢?有点像通过婚介找女朋友,在我和女朋友之间引入了一个第三者:婚姻介绍所。婚介管理了很多男男女女的资料,我可以向婚介提出一个列表,告诉它我想找个什么样的女朋友,比如长得像李嘉欣,身材像林熙雷,唱歌像周杰伦,速度像卡洛斯,技术像齐达内之类的,然后婚介就会按照我们的要求,提供一个mm,我们只需要去和她谈恋爱、结婚就行了。简单明了,如果婚介给我们的人选不符合要求,我们就会抛出异常。整个过程不再由我自己控制,而是有婚介这样一个类似容器的机构来控制。Spring所倡导的开发方式就是如此,所有的类都会在spring容器中登记,告诉spring你是个什么东西,你需要什么东西,然后spring会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的东西。所有的类的创建、销毁都由 spring来控制,也就是说控制对象生存周期的不再是引用它的对象,而是spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring控制,所以这叫控制反转。

2.2、DI(依赖注入)

  IoC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。比如对象A需要操作数据库,以前我们总是要在A中自己编写代码来获得一个Connection对象,有了 spring我们就只需要告诉spring,A中需要一个Connection,至于这个Connection怎么构造,何时构造,A不需要知道。在系统运行时,spring会在适当的时候制造一个Connection,然后像打针一样,注射到A当中,这样就完成了对各个对象之间关系的控制。A需要依赖 Connection才能正常运行,而这个Connection是由spring注入到A中的,依赖注入的名字就这么来的。那么DI是如何实现的呢? Java 1.3之后一个重要特征是反射(reflection),它允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性,spring就是通过反射来实现注入的。

  理解了IoC和DI的概念后,一切都将变得简单明了,剩下的工作只是在spring的框架中堆积木而已。

三、我对IoC(控制反转)和DI(依赖注入)的理解

  在平时的java应用开发中,我们要实现某一个功能或者说是完成某个业务逻辑时至少需要两个或以上的对象来协作完成,在没有使用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年初的一篇论文中首次提出的。他总结:控制的什么被反转了?就是:获得依赖对象的方式反转了。

四、小结

  对于Spring Ioc这个核心概念,我相信每一个学习Spring的人都会有自己的理解。这种概念上的理解没有绝对的标准答案,仁者见仁智者见智。如果有理解不到位或者理解错的地方,欢迎广大园友指正!



前言

关于这个话题, 网上有很多文章,这里, 我希望通过最简单的话语与大家分享.
依赖注入和控制反转两个概念让很多初学这迷惑, 觉得玄之又玄,高深莫测.
这里想先说明两点:

  1. 依赖注入和控制反转不是高级的,很初级,也很简单.
  2. 在JAVA世界,这两个概念像空气一样无所不在,彻底理解很有必要.

第一节 依赖注入 Dependency injection

这里通过一个简单的案例来说明.
在公司里有一个常见的案例: "把任务指派个程序员完成".

把这个案例用面向对象(OO)的方式来设计,通常在面向对象设计中,名词皆可设计为对象
这句话里"任务","程序员"是名词,所以我们考虑创建两个Class: Task 和 Phper (PHP 程序员)

Step1 设计

文件: Phper.Java

package demo;public class Phper {    private String name;    public Phper(String name){        this.name=name;    }    public void writeCode(){        System.out.println(this.name + " is writing php code");    }}

文件: Task.java

package demo;public class Task {    private String name;    private Phper owner;    public Task(String name){        this.name =name;        this.owner = new Phper("zhang3");    }    public void start(){         System.out.println(this.name+ " started");         this.owner.writeCode();    }}

文件: MyFramework.java, 这是个简单的测试程序.

package demo;public class MyFramework {     public static void main(String[] args) {         Task t = new Task("Task #1");         t.start();     }}

运行结果:
Task #1 started
hang3 is writing php code

我们看一看这个设计有什么问题?
如果只是为了完成某个临时的任务,程序即写即仍,这没有问题,只要完成任务即可.
但是如果同事仰慕你的设计,要重用你的代码.你把程序打成一个类库(jar包)发给同事.
现在问题来了,同事发现这个Task 类 和 程序员 zhang3 绑定在一起,他所有创建的Task,都是程序员zhang3负责,他要把一些任务指派给Lee4, 就需要修改Task的源程序, 如果没有Task的源程序,就无法把任务指派给他人. 而通常类库(jar包)的使用者通常不需要也不应该来修改类库的源码,如果大家都来修改类库的源码,类库就失去了重用的设计初衷.

我们很自然的想到,应该让用户来指派任务负责人. 于是有了新的设计.

Step2 设计:

文件: Phper.java 不变.
文件: Task.java

package demo;public class Task {    private String name;    private Phper owner;    public Task(String name){        this.name =name;    }    public void setOwner(Phper owner){        this.owner = owner;    }    public void start(){         System.out.println(this.name+ " started");         this.owner.writeCode();    }}

文件: MyFramework.java, 这是个简单的测试程序.

package demo;public class MyFramework {     public static void main(String[] args) {         Task t = new Task("Task #1");         Phper owner = new Phper("lee4");         t.setOwner(owner);         t.start();     }}

这样用户就可在使用时指派特定的PHP程序员.
我们知道,任务依赖程序员,Task类依赖Phper类,之前,Task类绑定特定的实例,现在这种依赖可以在使用时按需绑定,这就是依赖注入(DI).
这个例子,我们通过方法setOwner注入依赖对象,

另外一个常见的注入办法是在Task的构造函数注入:

    public Task(String name,Phper owner){        this.name = name;        this.owner = owner;    }

在Java开发中,把一个对象实例传给一个新建对象的情况十分普遍,通常这就是注入依赖.

Step2 的设计实现了依赖注入.
我们来看看Step2 的设计有什么问题.

如果公司是一个单纯使用PHP的公司,所有开发任务都有Phper 来完成,这样这个设就已经很好了,不用优化.

但是随着公司的发展,有些任务需要JAVA来完成,公司招了写Javaer (java程序员),现在问题来了,这个Task类库的的使用者发现,任务只能指派给Phper,

一个很自然的需求就是Task应该即可指派给Phper也可指派给Javaer.

Step3 设计

我们发现不管Phper 还是 Javaer 都是Coder(程序员), 把Task类对Phper类的依赖改为对Coder 的依赖即可.
这个Coder可以设计为父类或接口,Phper 或 Javaer 通过继承父类或实现接口 达到归为一类的目的.
选择父类还是接口,主要看Coder里是否有很多共用的逻辑代码,如果是,就选择父类
否则就选接口.

这里我们选择接口的办法:

  1. 新增Coder接口,
    文件: Coder.java
    package demo;public interface Coder { public void writeCode();}
  2. 修改Phper类实现Coder接口
    文件: Phper.php
    package demo;public class Phper implements Coder { private String name; public Phper(String name){     this.name=name; } public void writeCode(){     System.out.println(this.name + " is writing php code"); }}
  3. 新类Javaer实现Coder接口
    文件: Javaer.php
    package demo;public class Javaer implements Coder { private String name; public Javaer(String name){     this.name=name; } public void writeCode(){     System.out.println(this.name + " is writing java code"); }}
  4. 修改Task由对Phper类的依赖改为对Coder的依赖.
    文件: Task.java
    package demo;public class Task { private String name; private Coder owner; public Task(String name){     this.name =name; } public void setOwner(Coder owner){     this.owner = owner; } public void start(){      System.out.println(this.name+ " started");      this.owner.writeCode(); }}
  5. 修改用于测试的类使用Coder接口:
    package demo;public class MyFramework {  public static void main(String[] args) {      Task t = new Task("Task #1");     // Phper, Javaer 都是Coder,可以赋值      Coder owner = new Phper("lee4");     //Coder owner = new Javaer("Wang5");      t.setOwner(owner);      t.start();  }}
    现在用户可以和方便的把任务指派给Javaer 了,如果有新的Pythoner加入,没问题.
    类库的使用者只需让Pythoner实现(implements)了Coder接口,就可把任务指派给Pythoner, 无需修改Task 源码, 提高了类库的可扩展性.

回顾一下,我们开发的Task类,
在Step1 中与Task与特定实例绑定(zhang3 Phper)
在Step2 中与Task与特定类型绑定(Phper)
在Step3 中与Task与特定接口绑定(Coder)
虽然都是绑定, 从Step1,Step2 到 Step3 灵活性可扩展性是依次提高的.
Step1 作为反面教材不可取, 至于是否需要从Step2 提升为Step3, 要看具体情况.
如果依赖的类型是唯一的Step2 就可以, 如果选项很多就选Step3设计.

依赖注入(DI)实现了控制反转(IoC)的思想.
看看怎么反转的?
Step1 程序

this.owner = new Phper("zhang3");

Step1 设计中 任务Task 依赖负责人owner, 就主动新建一个Phper 赋值给owner,
这里是新建,也可能是在容器中获取一个现成的Phper,新建还是获取,无关紧要,关键是赋值, 主动赋值. 这里提一个赋值权的概念.
在Step2 和 Step3, Task 的 owner 是被动赋值的.谁来赋值,Task自己不关心,可能是类库的用户,也可能是框架或容器.
Task交出赋值权, 从主动赋值到被动赋值, 这就是控制反转.

第二节 控制反转 Inversion of control

什么是控制反转 ?
简单的说从主动变被动就是控制反转.

上文以依赖注入的例子,对控制反转做了个简单的解释.
控制反转是一个很广泛的概念, 依赖注入是控制反转的一个例子,但控制反转的例子还很多,甚至与软件开发无关.
这有点类似二八定律,人们总是用具体的实例解释二八定律,具体的实例不等与二八定律(不了解二八定律的朋友,请轻松忽略这个类比)

现在从其他方面谈一谈控制反转.
传统的程序开发,人们总是从main 函数开始,调用各种各样的库来完成一个程序.
这样的开发,开发者控制着整个运行过程.
而现在人们使用框架(Framework)开发,使用框架时,框架控制着整个运行过程.

对比以下的两个简单程序:

  1. 简单java程序
    package demo;public class Activity { public  Activity(){     this.onCreate(); } public void onCreate(){     System.out.println("onCreate called"); } public void sayHi(){     System.out.println("Hello world!"); } public static void main(String[] args) {     Activity a = new Activity();     a.sayHi();  }}
  2. 简单Android程序
    package demo;import android.app.Activity;import android.os.Bundle;import android.widget.TextView;public class MainActivity extends Activity{ @Override public void onCreate(Bundle savedInstanceState) {     super.onCreate(savedInstanceState);     TextView tv = new TextView(this);     tv.append("Hello ");     tv.append("world!");     setContentView(tv); }}

这两个程序最大的区别就是,前者程序的运行完全由开发控制,后者程序的运行由Android框架控制.
两个程序都有个onCreate方法.
前者程序中,如果开发者觉得onCreate 名称不合适,想改为Init,没问题,直接就可以改, 相比下,后者的onCreate 名称就不能修改.
因为,后者使用了框架,享受框架带来福利的同时,就要遵循框架的规则.

这就是控制反转.
可以说, 控制反转是所有框架最基本的特征.
也是框架和普通类库最大的不同点.

很多android开发工程师在享用控制反转带来的便利,去不知什么是控制反转.
就有点像深海里的鱼不知到什么是海水一样.

通过框架可以把许多共用的逻辑放到框架里,让用户专注自己程序的逻辑.
这也是为什么现在,无论手机开发,网页开发,还是桌面程序, 也不管是Java,PHP,还是Python框架无处不在.

回顾下之前的文件: MyFramework.java

package demo;public class MyFramework {     public static void main(String[] args) {         Task t = new Task("Task #1");         Coder owner = new Phper("lee4");         t.setOwner(owner);         t.start();     }}

这只是简单的测试程序,取名为MyFramework, 是因为它拥有框架3个最基本特征

  1. main函数,即程序入口.
  2. 创建对象.
  3. 装配对象.(setOwner)

这里创建了两个对象,实际框架可能会创建数千个对象,可能通过工厂类而不是直接创建,
这里直接装配对象,实际框架可能用XML 文件描述要创建的对象和装配逻辑.
当然实际的框架还有很多这里没涉及的内容,只是希望通过这个简单的例子,大家对框架有个初步认识.

控制反转还有一个漂亮的比喻:
好莱坞原则(Hollywood principle)
"不要打电话给我们,我们会打给你(如果合适)" ("don't call us, we'll call you." )
这是好莱坞电影公司对面试者常见的答复.

事实上,不只电影行业,基本上所有公司人力资源部对面试者都这样说.
让面试者从主动联系转换为被动等待.

为了增加本文的趣味性,这里在举个比喻讲述控制反转.
人们谈恋爱,在以前通常是男追女,现在时代进步了,女追男也很常见.
这也是控制反转
体会下你追女孩和女孩追你的区别:
你追女孩时,你是主动的,你是标准制定者, 要求身高多少,颜值多少,满足你的标准,你才去追,追谁,什么时候追, 你说了算.
这就类似,框架制定接口规范,对实现了接口的类调用.

等女孩追你时,你是被动的,她是标准制定者,要求有车,有房等,你买车,买房,努力工作挣钱,是为了达到标准(既实现接口规范), 你万事具备, 处于候追状态, 但时谁来追你,什么时候追,你不知道.
这就是主动和被动的区别,也是为什么男的偏好主动的原因.

这里模仿好莱坞原则,提一个中国帅哥原则:"不要追哥, 哥来追你(如果合适)",
简称CGP.( Chinese gentleman principle: "don't court me, I will court you")

扩展话题

  1. 面向对象的设计思想
    第一节 提到在面向对象设计中,名词皆对象,这里做些补充.
    当面对一个项目,做系统设计时,第一个问题就是,系统里要设计哪些类?
    最简单的办法就是,把要设计系统的名词提出来,通常,名词可设计为对象,
    但是否所有名词都需要设计对应的类呢? 要具体问题具体分析.不是不可以,是否有必要.
    有时候需要把一些动词名词化, 看看现实生活中, 写作是动词,所有写作的人叫什么? 没有合适的称呼,我们就叫作者, 阅读是动词,阅读的人就称读者. 中文通过加"者","手"使动词名词化,舞者,歌手,投手,射手皆是这类.
    英语世界也类似,通过er, or等后缀使动词名词化, 如singer,writer,reader,actor, visitor.
    现实生活这样, Java世界也一样.
    Java通过able,or后缀使动词名词化.如Runnable,Serializable,Parcelable Comparator,Iterator.
    Runnable即可以运行的东西(类) ,其他类似.
    了解了动词名词化,对java里的很多类就容易理解了.

  2. 相关术语(行话)解释
    Java 里术语满天飞, 让初学者望而生畏. 如果你不想让很多术语影响学习,这一节可忽视.
    了解了原理,叫什么并不重要. 了解些术语的好处是便于沟通和阅读外文资料,还有就是让人看起来很专业的样子.

    • 耦合(couple): 相互绑定就是耦合第一节 Step1,Step2,Step3 都是.
    • 紧耦合(Tight coupling) Step1 中,Task 和 zhang3 绑在一起; Step2中 Task 和 Phper 绑在一起, 都是.
    • 松耦合(Loose coupling) Step3 中,Task 和 Coder 接口绑在一起就是
    • 解耦(Decoupling): 从Step1 , Step2, 到 Step3 的设计就是Decoupling, 让对象可以灵活组合.
    • 上溯造型或称向上转型(Upcasting). 把一个对像赋值给自己的接口或父类变量就是.因为画类图时接口或父类在画在上面,所以是Upcasting. Step3中一下程序就是:

      Coder owner = new Phper("lee4");

    • 下溯造型或称向下转型(Downcasting). 和Upcasting 相反,把Upcasting过后的对象转型为之前的对象. 这个上述程序不涉及,顺带说一下

      Coder owner = new Phper("lee4");
      Phper p = (Phper) owner;

    • 注入(Inject): 通过方法或构造函数把一个对象传递给另一个对象. Step3 中的setOwner 就是.
    • 装配(Assemble): 和上述注入是一个意思,看个人喜好使用.
    • 工厂(Factory): 如果一个类或对象专门负责创建(new) 对象,这个类或对象就是工厂
    • 容器(Container): 专门负责存放创建好的对象的东西. 可以是个Hash表或 数组.
    • 面向接口编程(Interface based programming) Step3 的设计就是.