深度理解依赖注入(Dependence Injection)
http://www.cnblogs.com/xingyukun/archive/2007/10/20/931331.html 前面的话:提到依赖注入,大家都会想到老马那篇经典的文章。其实,本文就是相当于对那篇文章的解读。所以,如果您对原文已经有了非常深刻的理解,完全不需要再看此文;但是,如果您和笔者一样,以前曾经看过,似乎看懂了,但似乎又没抓到什么要领,不妨看看笔者这个解读,也许对您理解原文有一定帮助。
1
/*服务的接口*/
2 public interface MovieFinder { 3ArrayListfindAll(); 4} 5 6 服务的消费者7 class MovieLister 8 { 9publicMovie[]moviesDirectedBy(Stringarg){ 10ListallMovies=finder.findAll(); 11for(Iteratorit=allMovies.iterator();it.hasNext();){ 12Moviemovie=(Movie)it.next(); 13if(!movie.getDirector().equals(arg))it.remove(); 14} 15return(Movie[])allMovies.toArray(newMovie[allMovies.size()]); 16} 17 18消费者内部包含一个将指向具体服务类型的实体对象*/ 19privateMovieFinderfinder; 20消费者需要在某一个时刻去实例化具体的服务。这是我们要解耦的关键所在, 21*因为这样的处理方式造成了服务消费者和服务提供者的强耦合关系(这种耦合是在编译期就确定下来的)。 22*23publicMovieLister(){ 24finder=newColonDelimitedMovieFinder("movies1.txt"); 25} 26} 从上面代码的注释中可以看到,MovieLister和ColonDelimitedMovieFinder(这可以使任意一个实现了MovieFinder接口的类型)之间存在强耦合关系,如下图所示: 图1 这使得MovieList很难作为一个成熟的组件去发布,因为在不同的应用环境中(包括同一套软件系统被不同用户使用的时候),它所要依赖的电影查找器可能是千差万别的。所以,为了能实现真正的基于组件的开发,必须有一种机制能同时满足下面两个要求: (1)解除MovieList对具体MoveFinder类型的强依赖(编译期依赖)。 (2)在运行的时候为MovieList提供正确的MovieFinder类型的实例。 换句话说,就是在运行的时候才产生MovieList和MovieFinder之间的依赖关系(把这种依赖关系在一个合适的时候“注入”运行时),这恐怕就是Dependency Injection这个术语的由来 。再换句话说,我们提到过解除强依赖,这并不是说MovieList和MovieFinder之间的依赖关系不存在了,事实上MovieList无论如何也需要某类MovieFinder提供的服务,我们只是把这种依赖的建立时间推后了,从编译器推迟到运行时了。 依赖关系在OO程序中是广泛存在的,只要A类型中用到了B类型实例,A就依赖于B。前面笔者谈到的内容是把概念抽象到了服务使用者和服务提供者的角度,这也符合现在SOA的设计思路。从另一种抽象方式上来看,可以把MovieList看成我们要构建的主系统,而MovieFinder是系统中的plugin,主系统并不强依赖于任何一个插件,但一旦插件被加载,主系统就应该可以准确调用适当插件的功能。 其实不管是面向服务的编程模式,还是基于插件的框架式编程,为了实现松耦合(服务调用者和提供者之间的or框架和插件之间的),都需要在必要的位置实现 面向接口编程 ,在此基础之上,还应该有一种方便的机制实现 具体类型之间的运行时绑定 ,这就是DI所要解决的问题。 2.DI的实现方式 和上面的图1对应的是,如果我们的系统实现了依赖注入,组件间的依赖关系就变成了图2: 图2 说白了,就是要提供一个容器,由容器来完成(1)具体ServiceProvider的创建(2)ServiceUser和ServiceProvider的运行时绑定。下面我们就依次来看一下三种典型的依赖注入方式的实现。特别要说明的是,要理解依赖注入的机制,关键是理解容器的实现方式。本文后面给出的容器参考实现,均为 黄忠成老师 的代码,笔者仅在其中加上了一些关键注释而已。 2.1 Constructor Injection(构造器注入) 我们可以看到,在整个依赖注入的数据结构中,涉及到的重要的类型就是ServiceUser,ServiceProvider和Assembler三者,而这里所说的构造器,指的是 ServiceUser的构造器 。也就是说,在构造ServiceUser实例的时候,才把真正的ServiceProvider传给他: 1
2
{
接下来我们看看Assembler应该如何构建:
3//其他内容,省略 4 5publicMovieLister(MovieFinderfinder) 6{ 7this.finder=finder; 8} 9} private MutablePicoContainerconfigureContainer() { 2MutablePicoContainerpico=newDefaultPicoContainer(); 3 4下面就是把ServiceProvider和ServiceUser都放入容器的过程,以后就由容器来提供ServiceUser的已完成依赖注入实例, 5其中用到的实例参数和类型参数一般是从配置档中读取的,这里是个简单的写法。 6所有的依赖注入方法都会有类似的容器初始化过程,本文在后面的小节中就不再重复这一段代码了。7Parameter[]finderParams={newConstantParameter("movies1.txt")}; 8pico.registerComponentImplementation(MovieFinder.class,ColonMovieFinder. 9pico.registerComponentImplementation(MovieLister.class); 10至此,容器里面装入了两个类型,其中没给出构造参数的那一个(MovieLister)将依靠其在构造器中定义的传入参数类型,在容器中 11进行查找,找到一个类型匹配项即可进行构造初始化。12returnpico; 13} 需要在强调一下的是,依赖并未消失,只是延后到了容器被构建的时刻。所以正如图2中您已经看到的,容器本身( 更准确的说,是一个容器运行实例的构建过程 )对ServiceUser和ServiceProvoder都是存在依赖关系的。所以,在这样的体系结构里,ServiceUser、ServiceProvider和容器都是稳定的,互相之间也没有任何依赖关系;所有的依赖关系、所有的变化都被封装进了容器实例的创建过程里,符合我们对服务应用的理解。而且,在实际开发中我们一般会采用配置文件来辅助容器实例的创建,将这种变化性排斥到编译期之外。 即使还没给出后面的代码,你也一定猜得到,这个container类一定有一个GetInstance(Type t)这样的方法,这个方法会为我们返回一个已经注入完毕的MovieLister。 一个简单的应用如下: void testWithPico() 2 { 3MutablePicoContainerpico=configureContainer(); 4MovieListerlister=(MovieLister)pico.getComponentInstance(MovieLister.5Movie[]movies=lister.moviesDirectedBy("SergioLeone"); 6assertEquals("OnceUponaTimeintheWest",movies[0].getTitle()); 7} 上面最关键的就是对pico.getComponentInstance的调用。Assembler会在这个时候调用MovieLister的构造器,构造器的参数就是当时通过pico.registerComponentImplementation(MovieFinder.class,ColonMovieFinder.class,finderParams)设置进去的实际的ServiceProvider--ColonMovieFinder。下面请看这个容器的参考代码: 构造注入所需容器的伪码 2.2 Setter Injection(设值注入) <
beans
>
bean id ="MovieLister" class ="spring.MovieLister" 3 property name ="finder" 4 ref local ="MovieFinder" /> 5 </ property 6 bean ="MovieFinder" ="spring.ColonMovieFinder" 8 ="filename" 9 value > movies1.txt 10 11 12 > 下面也给出支持设值注入的容器参考实现,大家可以和构造器注入的容器对照起来看,里面的差别很小,主要的差别就在于,在获取对象实例(GetInstance)的时候,前者是通过反射得到待创建类型的构造器信息,然后根据构造器传入参数的类型在容器中进行查找,并构造出合适的实例;而后者是通过反射得到待创建类型的所有属性,然后根据属性的类型在容器中查找相应类型的实例。
设值注入的容器实现伪码
2.3 Interface Injection (接口注入) interface
InjectFinder
{
2voidinjectFinder(MovieFinderfinder); 3} 接下来,ServiceUser必须实现这个接口: class
MovieLister:InjectFinder
2 { publicvoidinjectFinder(MovieFinderfinder){ 5} 6} 容器所要做的,就是根据接口定义调用其中的inject方法完成注入过程,这里就不在赘述了,总的原理和上面两种依赖注入模式没有太多区别。 //
服务定位器的初始化
2 ServiceLocatorlocator = new ServiceLocator(); 3 locator.loadService( " MovieFinder " , new ColonMovieFinder( " movies1.txt " )); 4 ServiceLocator.load(locator); 服务定义器的使用 6 其实这个使用方式体现了服务定位器和依赖注入模式的最大差别:ServiceUser需要显示的调用ServiceLocator,从而获取自己需要的服务对象; 7 而依赖注入则是隐式的由容器完成了这一切。 8 MovieFinderfinder = (MovieFinder)ServiceLocator.getService( " MovieFinder " ); 9 正因为上面提到过的ServiceUser对ServiceLocator的依赖性,从提高模块的独立性(比如说,你可能把你构造的ServiceUser或者ServiceProvider给第三方使用)上来说,依赖注入可能更好一些,这恐怕也是为什么大多数的IOC框架都选用了DI的原因。ServiceLocator最大的优点可能在于实现起来非常简单,如果您开发的应用没有复杂到需要采用一个IOC框架的程度,也许您可以试着采用它。 3.广义的服务 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |