前面的话:提到依赖注入,大家都会想到老马那篇经典的文章。其实,本文就是相当于对那篇文章的解读。所以,如果您对原文已经有了非常深刻的理解,完全不需要再看此文;但是,如果您和笔者一样,以前曾经看过,似乎看懂了,但似乎又没抓到什么要领,不妨看看笔者这个解读,也许对您理解原文有一定帮助。
1.依赖在哪里
老马举了一个小例子,是开发一个电影列举器(MovieList),这个电影列举器需要使用一个电影查找器(MovieFinder)提供的服务,伪码如下:
2 public interface MovieFinder {
3 ArrayList findAll();
4}
5
6 服务的消费者 7 class 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传给他:
接下来我们看看Assembler应该如何构建:
1 private 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.arams);
9 pico.registerComponentImplementation(MovieLister.class);
10 至此,容器里面装入了两个类型,其中没给出构造参数的那一个(MovieLister)将依靠其在构造器中定义的传入参数类型,在容器中
11进行查找,找到一个类型匹配项即可进行构造初始化。12 return pico;
13}
即使还没给出后面的代码,你也一定猜得到,这个container类一定有一个GetInstance(Type t)这样的方法,这个方法会为我们返回一个已经注入完毕的MovieLister。 一个简单的应用如下:
void testWithPico()
2 {
3 Mutablepicocontainer pico = configureContainer();
4 MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.5 Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
6 assertEquals("Once Upon a Time in the West", movies[0].getTitle());
7}