DIP 沉思录

Dependency Injection 这个名词,是在 Martin Fowler 的《Inversion of Control Containers and the Dependency Injection pattern》文章之后才广为人知。在文章中,Martin 解释了当时初起流行的 IOC 概念:为了消除应用程序对插件实现的依赖,程序的主控权从应用程序移到了框架。为了让 IOC 概念不那么令人迷惑,Martin 把流行的几种 IOC Container 实现模式命名为 Dependency Injection Pattern(DIP,下文简称 DI 模式)。很明显,DI 的定义更准确形象,而且因为 Martin 在软件开发社区巨大的影响力,DI 模式以及 spring framework 作为其最成功的实现之一很快被软件开发社区接受并成为日常开发的必备利器。

“依赖注入”给我们代码的编写,特别是测试代码的编写带来了很多好处。借助于 DI 模式,我们可以将服务的依赖声明和具体实现相分离,通过配置不同的服务实例交给框架管理,来让应用程序获得良好的灵活性和柔性。

比如我们有这样的代码:

public void hello() {
Foo foo
= new Foo();
foo.sayHello();
//
}

这种情况下,hello() 方法就依赖于 Foo 类的 sayHello() 方法实现,会给测试带来了很多的不稳定性,比如耗时、服务异常之类。如果依照 DI 模式来重构,这段代码就会变成这样:

public void hello(Foo foo) {
foo.sayHello();
//
}

在测试时我们就可以使用行为确定符合预期的‘mock’的 Foo 类来代替实际的 Foo 类,从而将测试关注点聚集到 hello() 方法,也避免了 Foo 类 sayHello() 方法的具体实现对测试可能带来的影响。

以上就是 DIP 最常见也是最令人熟悉的一层涵义,从 IOC、spring framework 开始接受 DIP,自然而然会把 DIP 等同于 DI 模式,但其实它还有另一层涵义。在《Agile Software Developement:principles,Patterns,and Practices》的第11章 依赖倒置原则,给出了 DIP 的另一种解读:DIP —— 依赖倒置原则,这里的 DIP 就不再是 Dependency Injection Pattern(依赖注入模式),而是 Dependency Inversion Principle(依赖倒置原则,以下简称 DI 原则)。DI 原则主要包括下面两条启发式规则:
A. 高层模块不应依赖于低层模块。二者都应依赖于抽象
B. 抽象不应依赖于细节。细节应依赖于抽象

人们通常会用好莱坞法则来诠释启发规则 A:“Don’t call us, we’ll call you.”其中的 we/us,就是指高层模块,you 则是低层模块,高层模块来决定低层的模块,就像好莱坞制片人最终掌握着演员上镜与否的生杀大权。

我们来看看软件系统中常见的类关系,高层的 Policy Layer 使用了低层的 Mechanism Layer,而低层的 Mechanism Layer 又使用了更低层的 Utility Layer(见图1)

图 1

图1中的依赖关系是传递的:高层的 Policy Layer 对于其下一直到 Utility Layer 的改动都是敏感的,一旦我们修改了低层模块的实现,高层模块不得不也修改相应的实现来适应低层模块的修改。这种依赖关系,与软件复用的目标是相悖的,而且也不符合实际的业务变化。我们通常会需要切换低层的实现方式或者版本,而很少会去修改高层的业务。比如有一个证书申请发放系统,需要使用异步的消息机制来处理用户请求。客户可能会要求把底层的 MessageQueue 从 IIS 切换成 ActiveQ,但高层的证书申请发放的业务流程是稳定的,不会因低层基础服务的改变而作出修改。

所以,为了应付现实中的变化,也为了向高层屏蔽这些变化,我们通常会给低层模块抽象出接口,作为高层和低层之间的契约。这样,高层模块就应该只与低层模块的接口打交道,不再关心低层模块的实现细节。而低层模块的实现,我们可以通过配置文件由框架来切换,不影响到高层的行为。说到这里,大家应该可以看到 Spring 的影子了。此时类关系图就变成了下图(图2)

图 2

嗯,现在的类关系已经遵循接口依赖了,甚至加上了框架的 DI 模式,系统也具有了一定的灵活性和易改变性,看起来很像大多数的系统了。但是,这是不是就是符合 DI 原则呢?我们可以看到,这里的接口通常是由低层模块来定义和派生,也就是低层模块抽象出来提供给外界调用的服务接口。高层模块其实还是依赖于低层模块,只是这次高层模块产生依赖的是低层模块里面声明的接口。想起曾经在 javaeye 上看到一篇帖子,作者抱怨为什么 java 不提供 extracts 关键字,这样就可以由框架或者容器在具体类上抽出接口定义(与 implements 对应),省得手工创建这些接口。或许这是目前依赖注入框架带来的误区吧:使用人员不理解 DIP 的本意,单纯是为框架的约束而创建。此时声明的类关系显然还是没有完全体现 DI 原则的精髓。

在书中 Robert 进一步对 DI 原则给出了解释:“请注意这里的倒置不仅仅是依赖关系的倒置,也是接口所有权的倒置。我们通常会认为工具库应该拥有它们自己的接口,但是应用 DIP (DI 原则)时,我们发现往往是消费者拥有抽象接口,而它们的服务者则从这些接口派生。”基于这种思路,前面例子更符合面向对象思想的层次关系图如下(图3)
图 3

图3和图2的区别主要在接口的所有权:高层模块应该拥有接口所有权,低层模块派生自高层模块里定义的接口。这就意味着:
1. 高层模块引用的接口定义应该和高层模块的其他类放在一起
2. 高层模块的复用是把高层拥有的所有类和接口定义作为一个整体来复用的
3. 接口定义的改变只有根据高层模块的需要才进行的,而不是低层模块

OK,直到现在,我们才能说 DI 原则原来是这个意思,否则,体会不到就有可能误人误己。Robert 在本章的结论中说“事实上,这种依赖关系的倒置正是好的面向对象设计的标志所在,使用何种语言来编写程序是无关紧要的。如果程序的依赖关系是倒置的,它就是面向对象的设计。如果程序的依赖关系不是倒置的,它就是过程化的设计。”信哉此言!

那么,要按照 OOA & OOD 的设计思路来进行系统设计,DI 原则对我们有什么帮助呢?其实,DI 原则不仅仅是抽象的原则,而且是可以启发推导出出种种具体的实践。我们来看看对 OOA & OOD 的帮助。因为依赖关系是倒置的,就可以通过对高层策略的抽象和定义驱动出低层服务者的接口。以此类推,直到把最底层的模块设计出为止。那什么是高层策略呢?怎么找出潜在的抽象?书中同样给出了答案:“它是应用背后的抽象,是那些不随具体细节的改变而改变的真理。它是系统内部的系统——它是隐喻(metaphore)”更有详细的具体实践,敬请关注本博其他文章。

References:
Inversion of Control Containers and the Dependency Injection pattern,http://www.martinfowler.com/articles/injection.html,Martin Fowler
控制反转与依赖注入模式,http://gigix.blogdriver.com/diary/gigix/inc/DependencyInjection.pdf,熊节 译
《Agile Software Developement:principles,Patterns,and Practices》,Robert Fowler 著,邓辉 译,孟岩 审

Leave a Reply

Your email address will not be published. Required fields are marked *