内容


Apache Geronimo 中的依赖注入,第 1 部分

用新的方式观察 J2EE 应用程序中的解耦

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: Apache Geronimo 中的依赖注入,第 1 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:Apache Geronimo 中的依赖注入,第 1 部分

敬请期待该系列的后续内容。

软件开发人员一直在追求代码重用——原因很明显。这种追求的方法随着时间而变化,从 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 样式的依赖

真的全都需要这样么?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 对象。对于这个示例,有两个查找器类:FinderFromFileFinderFromDb,前者用 XML 文件进行搜索,后者则用关系数据库。虽然这个示例很小,但也使用了多个文件,归纳如表 1。

表 1. PicoContainer 示例中的文件
文件类型目的
CustomerLister这个类使用一个查找器类型来按名称查找客户
FinderFromFileCustomerFinder 接口的实现,它在文本文件中查找客户
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 注入的 CustomerFinder
public 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 相同的解耦程度,但是还带有许多预先定义好的服务勾子,允许注入更复杂的行为。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source, Java technology
ArticleID=109529
ArticleTitle=Apache Geronimo 中的依赖注入,第 1 部分: 用新的方式观察 J2EE 应用程序中的解耦
publish-date=04242006