Apache Geronimo 中的依赖注入,第 1 部分: 用新的方式观察 J2EE 应用程序中的解耦
2009-11-11 00:00:00 来源:WEB开发网软件开发人员一直在追求代码重用——原因很明显。这种追求的方法随着时间而变化,从 Fortran 中的函数到面向对象编程(OOP)和面向接口的继承。在每一步上,我们都会发现比以前更好的技术可以让我们把代码从硬依赖中解耦出来。其中提高代码重用最好的一个方式就是把接口与实现解耦。在 Art of UNIX Programming 一书中,Eric Raymond 把这一点融入了几个 UNIX® 原则:
模块化原则:编写由整洁的接口连接的简单部件。
分离原则:把策略与机制分离;把接口与引擎分离。
表示原则:把知识放在数据中,这样程序逻辑就可以又蠢又壮。
虽然这些观点老了,但我们仍然不断地寻找用 Java™ 技术实现它们的新方式。最新一轮解耦 —— 依赖注入,就反映了上面描述的观点。像在不同地方以不同方式实现的许多新概念一样,在概念和实现之间出现了许多混乱。在这个由两部分构成的系列中,我讨论了 DI 的概念(也叫做控制反转 或 IoC),然后演示它在 Apache Geronimo 中的实现方式。
DI 与 IoC
DI 框架造成混乱的一个方面是用来描述它们的术语。您听说过控制反转 和依赖注入 几乎可以互换使用。但是,它们指的并不是一回事。
IoC 是多数构架的一个通用术语。字面上说,它意味着把控制元素转变为一般来说被它控制的元素上(受控者)。换句话说,框架中通常的控制部件变成了被控制的部件。例如,在模型-视图-控制器(MVC)设计模式中,控制器调用方法。在事件驱动环境中,视图通过事件处理程序调用代码。这样,视图(即受控者)获得了控制器的角色。
我发现控制反转这个术语的包含面太广。这就像把 Java 2 Platform, Enterprise Edition (J2EE) 开发与软件开发混在一起一样。确实,J2EE 是 软件开发,但它是软件开发的高度专业版本。
幸运的是,Martin Fowler 已经站出来解决这个问题,他与多个框架的构建者合作,把这种形式的解耦命名为依赖注入。这个术语好得多,因为它具体描述了开发人员做的工作。所以,贯穿本文和下一篇文章,我都把这种形式的编码称为依赖注入。如果您阅读的其他资料使用 IoC,请确定知道作者指的是什么。
依赖注入
彼此依赖才能执行工作的软件组件,被看成是耦合在一起的。在软件开发中,某种程度的耦合是不可避免的。但是,应当尽量把耦合控制在最小程度上。例如,在构建库代码时,最好把类型定义成接口,而不要定义成具体的类。这样,日后就可以在不改变库代码的情况下修改具体的类,因为库只依赖接口中创建的定义。因为容器能够在运行时把组件注入到依赖它的组件中,所以 DI 提供了消除组件间高度耦合的另外一种方式。请看 图 1 所示的例子。
图 1. 简单、原始的客户持久性框架
在这个例子中,Customer Workflow 类不可避免地与 Customer Persistence 类耦合在一起,后者又绑定到数据库。表示这个架构的技术术语是 yike!幸运的是,Java 世界的开发人员避免了这种高度耦合。实际上,避免这类耦合是创建企业 JavaBean(EJB)技术背后的一个动机。
如果换用接口来表示 Customer Persistence 类,可以实现一些代码分离。 图 2 显示了同一结构改进后的版本。
图 2. 使用接口对关系进行解耦
强制对接口进行依赖,可以提供各种实现了接口的合约的实现类。在这种情况下,应当有一个 Customer Persistence 类读取 XML 文档,另有一个使用关系数据库。Workflow 类并不知道(或关心)具体采用哪种机制。
深入 EJB 领地
跳过一些逻辑结论之后,通过 DI 的方式,通过接口、工厂、池(代理)对象和其他的 EJB 技术,可以实现代码重用。图 3 展示了用典型的 EJB 关系描述的同一关系。
图 3. 用接口进行解耦的 EJB 版本
真的全都需要这样么?EJB 技术当然提供了一层解耦,但代价是什么呢?EJB 规范的作者使用的是当时流行的工具:继承、接口和设计模式。他们也试图通过把问题放在框架中,从而解决开发人员在编写企业应用程序时会遇到的每个问题。图 3 中没有看到的是对象池、自动事务处理、安全性以及 EJB 技术提供的所有其他好东西。惟一的问题就是我并不需要这些工具 —— 可 EJB 框架也包含了它们。所以,本来简单的解耦问题现在成了大问题。
您可以看到为什么对这种形式的解耦会有激烈的反对。正如 Bruce Tate 在一篇 blog 文章中雄辩地指出:为了创建一个简单的应用程序,“我没有必要吃掉整头大象”。规定性方式强制实现那些不需要的元素,因而这些方式不可行。它们会在带来好处的同时带来很大的复杂性,而不论迟早,复杂性会超过人们以为会从框架中得到的好处。
通过 DI 解耦
从马后炮的角度来说,EJB 2.0 不是解决这些问题的正确方法。所以肯定有一种更简单的方法,可以在不添加不需要的负担与复杂性的情况下解耦应用程序。当前解决这个问题的思考依赖于 DI。实际上,EJB 3 就采用了这种方式,就像 Geronimo 一样(它回避了所有重量级框架,以便实现更干净的技术)。
在查看 Geronimo 以及它的 DI 实现方式之前,请看一个更简单的容器。以下示例使用 PicoContainer,这是 ThoughtWorks 开发的一个开放源码的容器,除了执行 DI 之外什么也不做。这个容器显示了注入的独立工作方式,然后会转到 Geronimo 的工作方式和本系列中的第二篇文章。
构造函数注入和 PicoContainer
围绕着如何执行 DI,主要有两派思想:构造函数注入 和 setter 注入。构造函数注入利用构造函数判断要返回的具体对象类型。Setter 注入通过 set() 方法注入类型。
在 PicoContainer 中,通过构造函数注入依赖项。例如,图 2 显示的 Workflow 类需要通过持久性机制创建 customer 对象。对于这个示例,有两个查找器类:FinderFromFile 和 FinderFromDb,前者用 XML 文件进行搜索,后者则用关系数据库。虽然这个示例很小,但也使用了多个文件,归纳如表 1。
文件 | 类型 | 目的 |
CustomerLister | 类 | 这个类使用一个查找器类型来按名称查找客户 |
FinderFromFile | 类 | CustomerFinder 接口的实现,它在文本文件中查找客户 |
CustomerFinder | 接口 | 这个接口定义可以查找 Customer 对象的一些语义 |
Customer | 类 | 搜索的主题;封装客户信息 |
CustomerWorkflow | 类 | 应用程序的控制器类,负责初始化容器。 |
TestCustomerListing | 类 | 进行单元测试,执行客户的查找器 |
图 4 表示了这些类之间的关系。
图 4. PicoContainer 示例类之间的关系
代码
以下代码清单显示了 DI 在 PicoContainer 内的工作方式。
CustomerFinder
CustomerFinder 接口使 DI 成为可能。为了让 DI 工作,必须拥有一个接口,可以把这个接口的具体类注入到需要的行为的消费者中。在这个示例中,CustomerFinder 接口定义了方法 find(String name),如 清单 1 所示。
清单 1. 接口定义了如何查找客户public interface CustomerFinder {
Customer find(String name);
}
这个接口定义了查找客户的语义,但没有公布执行实际搜索的细节。(根据使用 XML 文件还是数据库,细节会有所不同。)
FinderFromFile
FinderFromFile 具体类实现 FinderFromFile 接口。这个类如 清单 2 所示,负责按名称从文本文件中查找客户。
清单 2. 实现文本文件的 find(String name) 的类public class FinderFromFile implements CustomerFinder {
private String fileName;
public FinderFromFile(String fileName) {
this.fileName = fileName;
}
public Customer find(String name) {
// . . . details omitted
}
}
CustomerLister
下面,定义负责调用查找器类的类 —— CustomerLister,如 清单 3 所示。就是这个类的依赖项(也就是 CustomerFinder 接口使用的某个实现)被注入。
清单 3. 其查找器执行容器注入的类public class CustomerLister {
private CustomerFinder finder;
public CustomerLister(CustomerFinder finder) {
this.finder = finder;
}
public Customer findCustomerByName(String name) {
return finder.find(name);
}
}
在 清单 3 中,可以看到 CustomerLister 的构造函数接受一个实现 CustomerFinder 接口的类的实例。容器把这个实例注入这个 CustomerLister。这个行为在 DI 世界中称作构造函数注入,因为实例是通过一个构造函数传递的。
另一种(也是使用更广的)行为是 setter 注入,在这种注入中,通过 set() 方法注入依赖类。PicoContainer 对这两种注入都支持,惟一真正的区别就是在依赖类不可用的时候,是否允许用显式的依赖构造类。这里的理论就是:如果不能注入依赖项,那么就不能构建依赖类。如果把理论撇开,这两类注入的功能实际没有区别。
清单 4 显示了使用 setter 注入时 CustomerFinder 接口的样子。
清单 4. 使用 setter 注入的 CustomerFinderpublic class CustomerLister {
private CustomerFinder finder;
public CustomerLister() {
}
public void setFinder(CustomerFinder finder) {
this.finder = finder;
}
}
CustomerWorkflow
下一个要研究的类是 CustomerWorkflow 类,在这个示例中它充当控制器。这个类在它的 configureContainer() 方法中配置 PicoContainer。这个方法然后再创建容器(充当单体)并注册组件和组件的参数。CustomerWorkflow 类如 清单 5 所示。
清单 5. 这个示例(CustomerWorkflow 配置容器)的控制器public class CustomerWorkflow {
public MutablePicoContainer configureContainer() {
MutablePicoContainer pico = new DefaultPicoContainer();
Parameter[] finderParams =
{new ConstantParameter("customerListing.xml")};
pico.registerComponentImplementation(CustomerFinder.class,
FinderFromFile.class, finderParams);
pico.registerComponentImplementation(CustomerLister.class);
return pico;
}
}
注意,PicoContainer 的配置允许为注入的组件指定参数(在 Geronimo 中,这些组件被称作 GBean)。还请注意,配置是用 Java 代码编写的,而不是在 XML 文件(例如 Spring 容器)中完成的。如果愿意使用 XML,可以采用 NanoContainer,这是一个开放源码的容器,与 PicoContainer 类似。不论使用哪个容器,现在已经设置了注入的依赖项。现在只需要调用需要这些依赖项并允许容器执行注入的代码。
TestCustomerFinder
问题的最后一部分是驱动进程的类。这个示例使用单元测试来验证每件事都按预期工作。TestCustomerListing 类(如 清单 6 所示)把所有这一切都绑在一起。
清单 6. 演示注入的测试用例public class TestCustomerListing extends TestCase {
private CustomerWorkflow workflow;
public void setup() {
workflow = new CustomerWorkflow();
}
public void teardown() {
workflow = null;
}
public void testCustomerFinder() {
MutablePicoContainer pico = workflow.configureContainer();
CustomerLister lister = (CustomerLister)
pico.getComponentInstance(CustomerLister.class);
Customer foundCustomer =
lister.findCustomerByName("Homer");
assertEquals("Homer", foundCustomer.getName());
}
}
TestCustomerListing 类在 setup() 方法中创建 Workflow 类的实例,然后调用 configureContainer() 方法。然后这个类用 PicoContainer 把 CustomerLister 类 —— 这个类有正确的依赖项(在这个示例中,是 FinderFromFile 查找器类)—— 提交给 lister 对象,后者会获得对指定客户的引用。
不用管移动部分的数量,这个示例演示了 DI 的解耦能力。为了创建一个保持客户的全新方式,仍然可以通过扩展接口、对容器添加配置代码来找到客户。通过这种方式,就获得了以前只使用语言内置的 OOP 无法得到的解耦程度。
下期预告
理解像 DI 这样的主题时,困难之一就是这个主题与其他无关主题的耦合数量。例如,研究 Geronimo 来学习 DI 就很困难,因为 Geronimo 中包含许多移动的部分,但与 DI 毫无关系。在本文中,我把这个主题与实现分离,选择可以使用的最小软件栈来研究 DI 的特征。我讨论了 DI 的动机和定义,并用一个示例,演示了容器如何可以把依赖项从一个组件注入到另一个组件。
在本系列的第 2 部分,我抛弃 PicoContainer 而转向 Geronimo,演示了在本文中工作的相同原则也适用于像 J2EE 应用服务器那样复杂的环境。Geronimo 提供了与 PicoContainer 相同的解耦程度,但是还带有许多预先定义好的服务勾子,允许注入更复杂的行为。
- ››Apache添加mod_aspdotnet.so支持ASP.NET配置指南
- ››Apache中改变php.ini的路径
- ››Apache2.2与Tomcat6整合及虚拟主机配置
- ››Apache+php+mysql在windows下的安装与配置图解
- ››Apache+Subversion完美结合,CentOS下实现版本控制...
- ››Apache HTTPServer2.2.16 发布
- ››Apache Tomcat 6.0.29 (稳定版)
- ››Geronimo V2.1.5 中的安全提升
- ››Apache HTTP Server 2.3.6 alpha 发布
- ››Apache+Subversion如何实现版本控制
- ››Apache+Subversion完美结合
- ››Apache的几种常见应用举例与分析
更多精彩
赞助商链接