`
wbj0110
  • 浏览: 1610664 次
  • 性别: Icon_minigender_1
  • 来自: 上海
文章分类
社区版块
存档分类
最新评论

理解依赖注入

阅读更多
摘要:提到依赖注入,大家都会想到老马那篇经典的文章。其实,本文就是相当于对那篇文章的解读。所以,如果您对原文已经有了非常深刻的理解,完全不需要再看此文;但是,如果您和笔者一样,以前曾经看过,似乎看懂了,但似乎又没抓到什么要领,不妨看看笔者这个解读,也许对您理解原文有一定帮助。
[1] 依赖在哪里
[2] DI的实现方式
[3] Setter Injection
[4] 除了DI,还有Service Locator

1.依赖在哪里
   老马举了一个小例子,是开发一个电影列举器(MovieList),这个电影列举器需要使用一个电影查找器(MovieFinder)提供的服务,伪码如下:

 1/*服务的接口*/
 2public interface MovieFinder {
 3    ArrayList findAll();
 4}

 5
 6/*服务的消费者*/
 7class MovieLister
 8{
 9    public Movie[] moviesDirectedBy(String arg) {
10        List allMovies = finder.findAll();
11        for (Iterator it = allMovies.iterator(); it.hasNext();) {
12            Movie movie = (Movie) it.next();
13            if (!movie.getDirector().equals(arg)) it.remove();
14        }

15        return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
16    }

17
18    /*消费者内部包含一个将指向具体服务类型的实体对象*/
19    private MovieFinder finder;
20    /*消费者需要在某一个时刻去实例化具体的服务。这是我们要解耦的关键所在,
21     *因为这样的处理方式造成了服务消费者和服务提供者的强耦合关系(这种耦合是在编译期就确定下来的)。
22     **/

23    public MovieLister() {
24        finder = new ColonDelimitedMovieFinder("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传给他:

 

1class MovieLister
2{
3   //其他内容,省略
4
5   public MovieLister(MovieFinder finder)
6   {
7       this.finder = finder;
8   }

9}

接下来我们看看Assembler应该如何构建:

 1private MutablePicoContainer configureContainer() {
 2    MutablePicoContainer pico = new DefaultPicoContainer();
 3    
 4    //下面就是把ServiceProvider和ServiceUser都放入容器的过程,以后就由容器来提供ServiceUser的已完成依赖注入实例,
 5    //其中用到的实例参数和类型参数一般是从配置档中读取的,这里是个简单的写法。
 6    //所有的依赖注入方法都会有类似的容器初始化过程,本文在后面的小节中就不再重复这一段代码了。
 7    Parameter[] finderParams =  {new ConstantParameter("movies1.txt")};
 8    pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);
 9    pico.registerComponentImplementation(MovieLister.class);
10    //至此,容器里面装入了两个类型,其中没给出构造参数的那一个(MovieLister)将依靠其在构造器中定义的传入参数类型,在容器中
11    //进行查找,找到一个类型匹配项即可进行构造初始化。
12    return pico;
13}

需要在强调一下的是,依赖并未消失,只是延后到了容器被构建的时刻。所以正如图2中您已经看到的,容器本身(更准确的说,是一个容器运行实例的构建过程)对ServiceUser和ServiceProvoder都是存在依赖关系的。所以,在这样的体系结构里,ServiceUser、ServiceProvider和容器都是稳定的,互相之间也没有任何依赖关系;所有的依赖关系、所有的变化都被封装进了容器实例的创建过程里,符合我们对服务应用的理解。而且,在实际开发中我们一般会采用配置文件来辅助容器实例的创建,将这种变化性排斥到编译期之外。
   即使还没给出后面的代码,你也一定猜得到,这个container类一定有一个GetInstance(Type t)这样的方法,这个方法会为我们返回一个已经注入完毕的MovieLister。 一个简单的应用如下:

1public void testWithPico() 
2{
3    MutablePicoContainer pico = configureContainer();
4    MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.class);
5    Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
6    assertEquals("Once Upon a Time in the West", movies[0].getTitle());
7}

上面最关键的就是对pico.getComponentInstance的调用。Assembler会在这个时候调用MovieLister的构造器,构造器的参数就是当时通过pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams)设置进去的实际的ServiceProvider--ColonMovieFinder。下面请看这个容器的参考代码:

2.2 Setter Injection(设值注入)
   这种注入方式和构造注入实在很类似,唯一的区别就是前者在构造函数的调用过程中进行注入,而它是通过给属性赋值来进行注入。无怪乎PicoContainer和Spring都是同时支持这两种注入方式。Spring对通过XML进行配置有比较好的支持,也使得Spring中更常使用设值注入的方式:

 1<beans>
 2    <bean id="MovieLister" class="spring.MovieLister">
 3        <property name="finder">
 4            <ref local="MovieFinder"/>
 5        property>
 6    bean>
 7    <bean id="MovieFinder" class="spring.ColonMovieFinder">
 8        <property name="filename">
 9            <value>movies1.txtvalue>
10        property>
11    bean>
12beans>

下面也给出支持设值注入的容器参考实现,大家可以和构造器注入的容器对照起来看,里面的差别很小,主要的差别就在于,在获取对象实例(GetInstance)的时候,前者是通过反射得到待创建类型的构造器信息,然后根据构造器传入参数的类型在容器中进行查找,并构造出合适的实例;而后者是通过反射得到待创建类型的所有属性,然后根据属性的类型在容器中查找相应类型的实例。

设值注入的容器实现伪码

2.3 Interface Injection (接口注入)
   这是笔者认为最不够优雅的一种依赖注入方式。要实现接口注入,首先ServiceProvider要给出一个接口定义:

1public interface InjectFinder {
2    void injectFinder(MovieFinder finder);
3}

接下来,ServiceUser必须实现这个接口:

1class MovieLister: InjectFinder
2{
3   public void injectFinder(MovieFinder finder) {
4      this.finder = finder;
5    }

6}

容器所要做的,就是根据接口定义调用其中的inject方法完成注入过程,这里就不在赘述了,总的原理和上面两种依赖注入模式没有太多区别。

2.4  除了DI,还有Service Locator
   上面提到的依赖注入只是消除ServiceUser和ServiceProvider之间的依赖关系的一种方法,还有另一种方法:服务定位器(Service Locator)。也就是说,由ServiceLocator来专门负责提供具体的ServiceProvider。当然,这样的话ServiceUser不仅要依赖于服务的接口,还依赖于ServiceContract。仍然是最早提到过的电影列举器的例子,如果使用Service Locator来解除依赖的话,整个依赖关系应当如下图所示:
图3
用起来也很简单,在一个适当的位置(比如在一组相关服务即将被调用之前)对ServiceLocator进行初始化,用到的时候就直接用ServiceLocator返回ServiceProvider实例:

 

1//服务定位器的初始化
2ServiceLocator locator = new ServiceLocator();
3locator.loadService("MovieFinder"new ColonMovieFinder("movies1.txt"));
4ServiceLocator.load(locator);

5//服务定义器的使用
6//其实这个使用方式体现了服务定位器和依赖注入模式的最大差别:ServiceUser需要显示的调用ServiceLocator,从而获取自己需要的服务对象;
7//而依赖注入则是隐式的由容器完成了这一切。
8MovieFinder finder = (MovieFinder) ServiceLocator.getService("MovieFinder");
9

正因为上面提到过的ServiceUser对ServiceLocator的依赖性,从提高模块的独立性(比如说,你可能把你构造的ServiceUser或者ServiceProvider给第三方使用)上来说,依赖注入可能更好一些,这恐怕也是为什么大多数的IOC框架都选用了DI的原因。ServiceLocator最大的优点可能在于实现起来非常简单,如果您开发的应用没有复杂到需要采用一个IOC框架的程度,也许您可以试着采用它。

3.广义的服务
   文中很多地方提到服务使用者(ServiceUser)和服务提供者(ServiceProvider)的概念,这里的“服务”是一种非常广义的概念,在语法层面就是指最普通的依赖关系(类型A中有一个B类型的变量,则A依赖于B)。如果您把服务理解为WCF或者Web Service中的那种服务概念,您会发现上面所说的所有技术手段都是没有意义的。以WCF而论,其客户端和服务器端本就是依赖于Contract的松耦合关系,其实这也从另一个角度说明了SOA应用的优势所在。

 

分享到:
评论

相关推荐

    一个轻量级的PHP依赖注入框架

    首先,让我们理解依赖注入的基本概念。在面向对象编程中,一个类可能依赖于其他类来完成某些任务。传统的做法是直接在类内部创建或引用这些依赖的实例,这被称为硬编码。而依赖注入则是通过外部将依赖对象传递给需要...

    C++ 依赖注入

    为了深入理解依赖注入在C++中的应用,文章还提供了实现依赖注入容器的附录和参考文献,便于读者进一步学习和实践。通过这些内容的学习,可以掌握依赖注入的基本概念、方法和技巧,从而在实际开发中有效地利用依赖...

    C#中的依赖注入:构建灵活和可测试的应用程序

    通过理解依赖注入的基本概念、实现方式和服务的生命周期管理,开发者可以构建出更加灵活、可测试和易于维护的应用程序。随着.NET平台的发展,依赖注入已经成为.NET Core应用程序的标准配置,使得依赖注入的使用变得...

    Spring依赖注入使用静态工厂设注入demo

    首先,理解依赖注入的基本概念。依赖注入是指在运行时,由容器(如Spring IoC容器)负责创建对象并管理这些对象之间的依赖关系,而不是由对象自身去寻找或创建它所依赖的对象。这样可以降低对象间的耦合度,使系统更...

    Unity MVC实现依赖注入实例

    首先,理解依赖注入的核心概念:对象的依赖关系不在对象内部创建,而是通过外部容器在运行时注入。这样可以避免硬编码依赖关系,提高代码的可重用性和可测试性。 Unity框架提供了一种灵活的方式来管理对象的生命...

    AngularJS之依赖注入模拟实现

    AngularJS是一个流行的前端框架,它的核心特性之一就是依赖注入(Dependency Injection...这不仅可以应用在学习和研究AngularJS的场景下,也对于理解依赖注入这一设计模式以及在其他编程环境中实现依赖注入提供了参考。

    依赖注入demo

    通过阅读和分析这些代码,你可以更深入地理解依赖注入的工作原理及其在实际项目中的应用。 总的来说,依赖注入通过注解和反射的结合,使得对象的依赖关系不再硬编码,而是由外部容器动态管理,从而提高了代码的灵活...

    面试官常问的spring依赖注入和bean的装配问题.pdf

    无论是使用Spring框架还是Spring Boot,理解依赖注入的机制和装配方法,都是构建高质量Spring应用程序的关键。在实际开发过程中,开发者应该根据项目的具体需求和团队的开发习惯选择最适合的依赖注入和装配方式。

    如何简单的理解依赖注入详解

    让我们通过一个简单的例子来深入理解依赖注入的概念。 假设我们有两个类,一个接口A和它的实现类B。在没有依赖注入的情况下,我们可能会这样创建和使用它们: ```java class User { private A a = new B(); // ....

    PHPDI是一个依赖注入容器强大且与框架无关

    ...依赖注入(Dependency Injection)是一...通过理解依赖注入的概念和PHP-DI提供的特性,开发者可以编写出更易于维护和扩展的代码。无论是小型项目还是大型框架,PHP-DI都能提供出色的支持,帮助你构建更加健壮的应用。

    Spring学习笔记(5)----依赖注入的简单实现

    首先,理解依赖注入的基本思想:对象的依赖关系不再由对象自身管理,而是由外部容器(如Spring应用上下文)负责注入。这样做的好处是降低了对象间的耦合度,使得各个组件可以独立地被测试和复用。 依赖注入在Spring...

    arcgis server flex 中的依赖注入

    #### 一、理解依赖注入的概念与价值 依赖注入(Dependency Injection,简称DI)是一种设计模式,旨在减少类间的耦合度,提高软件系统的灵活性和可测试性。在传统的编程模式下,一个类可能会直接创建它所依赖的对象...

    Spring依赖注入使用setter设注入demo

    首先,理解依赖注入的基本思想:对象之间的依赖关系不是由对象自身管理,而是由外部容器(在这里就是Spring IoC容器)负责创建和维护。这样,对象可以在不关心依赖的具体实现情况下,通过配置文件或注解来获取所需的...

    Spring中的依赖注入.rar

    首先,让我们深入理解依赖注入的概念。依赖注入是指将一个对象所需的依赖对象传递给它,而不是由该对象自己创建或查找。这样,对象只需要关注自己的业务逻辑,而不必关心依赖如何获取。Spring框架提供了两种主要的...

    Spring 简单案例(依赖注入)_02

    理解源码对于深入理解依赖注入的工作原理至关重要,而工具可能指的是使用Spring的配置工具或者IDE插件来辅助开发。 在压缩包文件 "ExpectSpring" 中,可能包含了演示依赖注入的代码示例或整个项目结构。这个文件名...

    依赖注入那些事儿

    ### 依赖注入那些事儿 #### 1. IGame游戏公司的故事 ##### 1.1 讨论会 IGame是一家专注于开发动作角色扮演游戏的游戏公司。这类游戏通常包含一个基础功能——打怪,即玩家通过攻击怪物来获得经验值、虚拟货币和...

    了解ASP.NET Core 依赖注入.doc

    首先,让我们理解依赖注入的基本概念。依赖是指一个类在执行其功能时需要其他类的协助。例如,`AccountController`需要`ILoginService`来处理用户登录。遵循依赖于抽象的设计原则,我们将`EFLoginService`封装为`...

    Spring依赖注入使用构造设注入demo

    首先,理解依赖注入的概念。依赖注入允许我们不在类内部创建对象,而是通过外部源(如Spring容器)将依赖的对象传递给类。这样,类只需要关注自身的业务逻辑,而无需关心依赖的实现细节。 构造器注入是DI的一种形式...

    自己的代码模拟spring的依赖注入

    首先,理解依赖注入的概念至关重要。依赖注入意味着组件不直接创建其依赖的对象,而是由外部实体(如Spring IoC容器)提供。这样做的好处是,我们可以轻松地替换或更改依赖,而无需修改组件的源代码,符合开闭原则。...

    Spring依赖注入使用实例工厂设注入demo

    首先,理解依赖注入的基本概念。依赖注入允许我们把一个对象(依赖)传递到另一个对象(客户端)中,而不是让客户端自行创建依赖。这有助于解耦组件,使得系统更加灵活,更容易进行单元测试。 接下来,我们讨论实例...

Global site tag (gtag.js) - Google Analytics