加入收藏 | 设为首页 | 会员中心 | 我要投稿 安卓应用网 (https://www.0791zz.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 综合聚焦 > 程序设计 > 正文

重温控制反转和依赖注入

发布时间:2020-05-23 12:36:23 所属栏目:程序设计 来源:互联网
导读:记得刚大学毕业的时候在上海实习,每天花很多时间来研究Java技术,无意间独到了一篇讲解控制反转和依赖注入的文章,作者是Martin Fowler。那时候刚学习EJB和Spring,只是觉得文章里谈到的设计思想是一种很新鲜的尝试,而且又正好在Spring框架中大量的被使用,

记得刚大学毕业的时候在上海实习,每天花很多时间来研究Java技术,无意间独到了一篇讲解控制反转和依赖注入的文章,作者是Martin Fowler。那时候刚学习EJB和Spring,只是觉得文章里谈到的设计思想是一种很新鲜的尝试,而且又正好在Spring框架中大量的被使用,所以自己也反复的开始使用并坚持针对接口编程、解耦合等思想。


不知不觉五年过去了,突然又在网上看到这篇文章,感概狼多,于是又开始仔细阅读起来。这里想把自己所读所感写下来,温故而知新:
Inversion of Control(控制反转)所提倡的是一种思想,个人觉得目的是针对Java在大型项目的架构和实现和设计存在诸多的组件、Service和业务模块这一特点,把组件模块之间的依赖关系独立出来,做到互相之间尽量的解耦合。对于这么做所产生的复杂依赖关系的控制权交给一个或者多个装配器,直到运行时才让装配器来选择具体的实现。Martin在文章中把这种思想总结了另外一个更容易理解的名字叫Dependency Injection(依赖注入),分Constructor注入、Setter注入和Interface注入三种方式讲解了各自的优缺点以及运用时要注意的地方。


个人觉得,理解这一切的前提首先是针对接口编程的意识。记得曾经跟一个技术也还不错的哥们反复讨论过一行代码:

List list = new ArrayList();
ArrayList list = new ArrayList();


那位哥们坚持觉得这两种写法没有本质区别,我则坚持第一种体现了一种针对接口编程的思想和素质。为此我们争论不休直至深夜...现在想来,刚毕业那时候确实无法理解针对接口编程的思想,只是一味的盲从。现在看来,始终用接口来编程的好处主要有以下几点:1、把具体的实现推迟到了运行时,在接口层面可以把项目的大体框架搭好,更快的从全局上总览设计的问题所在,更早的来修改架构达到一个稳定合理的状态。2、针对接口所写的代码,只要接口不曾改变,其具体的实现可以在项目上线运行之后,很轻易的替换。3、接口本身起到了一个约束和契约的作用,接口把系统的行为给规范化了,之后任何实现都尊重这个规范,这样就保证了多态的行为实现却遵循了同样的规范,有统一的标准。


最典型的例子就是J2EE,整个J2EE就是一套完整的规范,任何厂商可以去实现其中的部分或者全部,在满足这些起规范作用的接口的条件下,都可以和系统的开发者所写的代码良好的合作运行。比如你写的一个电子商务网站调用了许多Servlet、或者在更大型的系统你开发了许多MBean,这些代码可以运行在实现了部分J2EE规范的Tomcat容器里,也同样可以运行在实现了大部分J2EE规范的JBoss或者WebLogic容器里。


有了针对接口编程的意识,始终会在系统中用接口来定义模块和模块之间的依赖关系、行为。随之,问题就来了,模块之间的接口以及依赖、协作关系都定义完毕,那么接口的具体实现类何时介入?又由谁来负责创建?如果类似之前看到的代码: List list = new ArrayList()来在使用的模块中当下构建的话,还是模块与模块间、接口与实现之间给耦合在一起了。针对这类问题,Martin Fowler提出了依赖注入的模式,让另外一个第三方的Assembler来负责把具体的最合适的实现类在运行的时候注入到你的模块中,模块内部可以完全不考虑使用的是哪一个实现类,当实现类改变了更替了的时候,模块的代码可以完全不必做任何的改变。通过这种方式来达到进一步的解耦合。


由于这里我们谈到的是模块在运行时才决定使用接口的哪一个实现类,所以实现类的注入总是更多的发生在模块要开始工作执行它的工作的时候。这个时间点在大多数情况下,是构造函数或者初始化之后的setter方法中就不难理解了。而到底选择构造函数注入还是初始化之后的setter方式注入,主要取决于模块的运行需要依赖的接口数量和复杂程度而定的。我们也能想象到如果依赖的接口数目越多,情况越复杂,你需要定义的构造函数种类也就越多,降低了代码的可读性,此时你可能就该考虑更多的使用单一的构造函数+相对独立的setter方法注入那么多接口的实现类。(另外,由于每一个setter方法都有自己明确表意的名字,而构造函数只能通过参数的名称和类型来表意,情况复杂之后必然造成模糊的表意)


另外文章中还谈到了另外一种方式定义第三方的Assembler,即ServiceLocator。这个Locator负责把所有种类的Service实现类都准备好,当模块需要使用某一个Service的时候,向这个ServiceLocator索要,由这个Locator向模块提供具体的Service实现类的服务。笔者也在项目中确实见到了这种做法,其适用场合是项目定义了众多Service接口,而大部分模块处理业务逻辑都需要用到这些公共的Service库中的多个Service,那么把模块与ServiceLocator给绑定起来,利用Locator来管理所有Service的注入和使用,也不失为一种可选的方案。ServiceLocator的最大好处是让模块不需要去烦恼配置哪个Service实现类,全权交给Locator统一安排。而在构造函数注入和Setter注入中,模块至少要通过配置文件的方式来告诉Assembler你要给我装配一个什么实现类。


无论是各种依赖注入还是ServiceLocator,模块都从原始方式里自己来创建接口实现类改变为把实现类的选取和创建等控制权交了出来,给第三方的框架装配器(如Spring、ServiceLocator等)来负责,这就是反复讨论的控制反转。
最后附上一个简单的小例子作为结束:

interface Gamer{
 void play();
}
class XBox implements Gamer{}
class Wii implements Gamer{}
class PS3 implements Gamer{} 


class MyBusiness{
 Gamer myGamer;
 
 void setMyGamer(Gamer gamer){
  this.myGamer = gamer;
 }
 
 void playGame(){
  myGamer.play();
 }
}


使用大家最常见的Spring担任这个第三方的Assembler来按照MyBusiness对Gamer接口的依赖注入实现类:

<!-- the most important is who responsible for inject the specific implementation of Gamer into MyBusiness -->
<!-- by spring,indicate the implementation into xml and spring helps to inject the instance into MyBusiness -->
<beans>
 <bean id="myBusiness" class="MyBusiness">
  <property name="myGamer">
   <!-- you can easily replace it by change configuration here into another optional bean id -->
   <ref local="xbox">
  </property>
 </bean>
 
 <bean id="xBox" class="XBox"/>
 <bean id="xBox" class="PS3"/>
 <bean id="xBox" class="Wii"/>
</beans>

(编辑:安卓应用网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读