内容


评论专栏

如何实现良好的模块化以及为什么 OSGi 如此出众

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 评论专栏

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

此内容是该系列的一部分:评论专栏

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

基本组成

在最基本的层面上,软件 “模块” 由一个接口和该接口的一个或多个实现组成。接口定义了该类与外部世界的关系。

在软件以外的领域,模块化的应用非常普遍。一个突出的例子就是货运。20 世纪 50 年代引入的联运集装箱(intermodal shipping container)对全球货运成本产生了极大的影响。集装箱采用标准设计,具有标准的大小,并按照标准的方式安放。这意味着船只、火车或卡车都可以设计为容纳这种标准尺寸的集装箱,而不必关心里面装的是什么,只需要关心集装箱的外部接口。这显著缩短了装载和卸载货物所需的时间,从而提高了效率。至于集装箱中装了什么以及如何处理这些货物则是装箱人员需要负责的事。

在 IT 界,也有一些有关硬件模块化的出色例子。要升级计算机的显卡,您需要考虑母板接口(例如,PCI Express)和显示器输入(VGA 或 DVI),然后选择一种能够同时支持这两种接口的卡。显示器和母板都不会限制卡,如用于提供支持的芯片集;关键在于所连接的接口。显卡制造商可以放心地设计卡的工作原理,或修改它的工作方式,并将他们的精力集中在构建高品质显卡上面,而不必担心接口另外一端的事物。

但是,在软件中,事物则并不是按照这样的思路设计的,这主要是因为实现良好的模块化非常困难。

模块化考量

让我们首先了解实现良好的模块化的一些关键考量。这些特征之所以重要的原因在于它们可以使代码更独立、更灵活且更具弹性:

  • 合约:该组件与其客户端之间签订了什么样的合约?它们如何访问它,它们期望的行为是什么,等等。在软件开发中,开发人员编写的代码通常取决于某个函数的未归档的行为,例如期望发生某个未归档的附作用。
  • 可见性: 任何模块都有可能包含大量有用的内部类、方法和工具,它们用于提供模块接口(或合约),但是模块并不将它们视为外部的。通常,在使用由供应商提供的模块时,很难分辨出哪些是内部的,哪些是外部的。最终结果是如果使用了某个内部模块,那么客户端第一次找到它时是在出现编译故障或运行时故障时,因为方法签名被修改了。
  • 共存:任何系统都可以包含一组模块,其中每个模块都依赖于其他模块。这些模块需要能够一起运行,而不会彼此造成问题。一个简单的例子就是 JDK 包含两个 List 类;第一个类属于 AWT(Abstract Window Toolkit),第二个类属于 Java Collections API。它们不会互相干扰,因为它们位于不同的包中。尽管这些包采取了一些措施来支持共存,然而难免会出现一些问题,最大的问题就是类的版本化。
  • 可替换性:定义了合约或接口后,您需要能够替换模块而不影响系统。回想一下前面的显卡例子:您可以替换(或升级)显卡,而不需要担心这会对整个系统产生影响。

应用模块化考量

集装箱

为了证明以上针对模块化的考虑是合理的,让我们将其应用到集装箱示例中,看看它们如何发挥作用:

  • 合约:集装箱具有标准尺寸,并且可以插入到另一个集装箱顶部的洞中。这使集装箱能够逐个堆叠起来。针对这种标准建造的船只、卡车、起重机和火车可以轻松地容纳集装箱,而不需要关心这些集装箱的制造者是谁或里面装的货物是什么。
  • 可见性:据此生产的起重机、船只、卡车不需要关心集装箱里面的货物,并且每个集装箱内的货物也与其他集装箱不相关。如果您的集装箱里面盛满了草莓,那么您不用担心将承载着汽车的集装箱放到顶部会将您的草莓压坏(尽管出于其他原因这不是个好主意,但是这并不是一个真正的模块化考虑)。
  • 共存:一个集装箱不需要了解其他集装箱的特性。它们是否采用不同的方式喷漆,或是由不同的制造商生产,这些都没有关系。
  • 可替换性:可以轻松地取出并替换一个集装箱,不会影响其他集装箱或运输工具。

可以看到,集装箱很好地满足了这些模块化考量。

Java

如果我们将这些考量应用于 Java,您很快就会发现 Java 本身并没有为模块化提供最佳环境:

  • 合约:虽然 Javadoc 提供了良好的 API 归档方式,但是默认值仅仅是描述类、接口和方法。然而,要真正从合约获益,您需要开发人员编写良好的 Javadoc 注释。通常,API 不具备充足的或正确的文档(如果有的话),这逊色于现代 IDE 中的自动完成特性。公平地说,这不是 Java 的问题;维护良好的开发实践是项目团队的责任。
  • 可见性:Java 有两个主要的可见性模式。第一个是可见性标记(visibility marker),可以将字段、方法和类标记为公共的、受保护的、私有的或默认的可见性。该标记由编译器强制执行,在运行时则不能很好地被执行;即,运行时将通过映像(reflection)公开这些内部内容(如果要求的话)。第二个是类路径中可访问的任何内容,意味着构成内部内容的类都可以被调用方看到。
  • 共存:再次查看类路径,当两个模块具有不同的版本且不兼容二进制文件时,您不能让两个模块依赖于一个通用库。您需要一致地升级所有内容,这对于简单的应用程序很容易做到,然而,对于复杂应用程序来说,可能需要进行一些测试和 QA。
  • 可替换性:可替换性有两个方面:冷替换(允许 JVM 重启)和热替换(无需 JVM 重启)。冷替换是可实现的,但是需要知道类路径上的哪些 JAR 文件需要移除并替换。这听起来简单,但是在实践中会非常复杂。Java 没有提供热替换。

尽管 Java 本身没有满足这些要求,但是它提供了一些工具可以实现模块化。任何使用 Java EE 应用服务器的人都将发现这些模块化考量都可以在 Java 内实现:每个 EAR 文件都是彼此分离的,您可以用不同级别的代码运行多个 EAR 文件。但是 Java EE 观点是每一个这些 EAR 文件都是独立的应用程序,而不是模块,因此虽然它显示了一些模块化行为,但是它是在粗粒度级别上实现的。

因此,考虑到 Java 本身没提供良好的模块化解决方案,那么您该怎么做?有两种常用的互补系统:依赖关系管理和模块系统。

模块化解决方案

依赖关系管理

采用模块化的第一步通常是利用构建时依赖关系管理系统。其中最著名的系统之一是 Maven,在其中,您将用名称和版本定义模块,并通过将 Java 源代码放入其中定义内容。其他模块可以根据名称和版本与您的模块建立依赖关系。Maven 还可以轻松地定义模块来聚合其他模块。聚合使您能够拥有一个 API 模块和实现模块;与您的模块有依赖关系的模块将根据 API 模块进行构建,并针对 API 和实现模块运行。

Maven 以及其他依赖关系管理系统定义模块之间的依赖关系。这通常为许多人提供了足够的模块化。比较一下这些系统与模块化考量,看看它们真正提供的价值:

  • 合约:依赖关系管理系统定义了模块的身份,这是合约的一部分。然而,这是一个虚拟的构造,它极大地限制了提供商的能力。例如,如果将某个模块重构为两个,那么将影响此前依赖该模块的客户端;它们将获得部分所需的 API,但不是全部。代码本身没有改变,但是需要修改模块的元数据。有多种方法可以解决这个问题,可以生成聚合模块,但是这充其量只是一种暂时的解决方法,并且可能会公开比实际所需更多的内容。返回到显卡类比中,这意味着显卡不仅可以看到 PCI Express(这是必需的),还可以看到与 CPU、打印机、RAM 等的母板接口(这是没有必要的)。
  • 可见性:通过使用依赖关系管理,将在构建时而不是运行时解决可见性问题。这也许足够满足要求,但是要访问内部细节,需要添加与实现模块的依赖关系。对模块的可见性的控制取决于用户,而不是提供商;在运行时不会强制执行,因为正常的 Java 可见性会在运行时生效。
  • 共存在运行时不受支持。在此处使用普通的 Java 类加载,但是您可以对不同的依赖关系版本使用两个模块构建,并且不会造成问题。
  • 可替换性仅限于修改依赖关系以依赖不同的内容。然而,这通常只在进行重构时有效。

虽然依赖关系管理为实现良好的模块化解决方案做了很好的铺垫,然而,它仅仅起到了一部分作用。

OSGi

在为 Java 设计模块系统的各种选择中,最著名的是 OSGi。OSGi 定义了一个模块(称为包),声明对所需的包和提供的包的依赖关系。在此基础上应用了模块生命周期和服务模型的概念。服务模型使用了一个服务库,其中,对象可以通过所提供的接口进行传递。服务库是动态的,因此服务可以来来去去。服务用户可以在服务被删除以及切换到另一个服务时收到通知。

那么,与模块化考量相比,OSGi 的表现如何呢?

  • 合约:OSGi 很好地匹配了合约要求。一个模块声明了它提供给客户端模块的功能,以及它需要从其他模块获得的有关包的功能。您不声明对模块的依赖关系,而是声明对所使用的包的依赖关系。模块化系统可以很好地匹配您的需求,而不需要考虑是哪个模块提供的。模块化系统不会告诉您未声明为需求的内容。
  • 可见性:OSGi 采用两种方式确保可见性。首先,模块只看到声明存在依赖关系的包。其次,模块声明要公开哪些包,因此没有公开的包是私有的。这意味着可以很明显地确定哪些包是私有的,OSGi 实现了这一点。
  • 共存通过包版本化获得支持。每个模块可以定义它所提供和需要的包的版本。这使得系统中可以存在两个使用不同包版本的模块。
  • 可替换性:OSGi 允许在运行时停止某个模块并替换它。这对系统的影响取决于所做的更改以及应用程序的编写方式。一些更改将引起严重的问题,而其他更改的影响则很微小。更改应用程序的类视图要比替换服务库中注册的服务的破坏程度更大。

如您所见,OSGi 可以很好地满足模块化考量。

结束语

了解了对于实现良好的 Java 模块化至关重要的考量因素后,可以清楚地看到 OSGi 是最佳选择。第一轮 Java 模块化始于 IDE 之类的大型软件产品,之后是应用服务器。综合本文所述的考量因素和其他原因,选择了 OSGi 作为实现 IBM® WebSphere® Application Server 模块化的机制。此外,一些较大的 Java 项目,如 Eclipse、Glassfish、Apache Geronimo 3 和 Apache ServiceMix,全部基于 OSGi。

下一轮采用模块化的将是那些可以从应用服务器的模块化功能获益的大型应用程序。正是出于这一采用,成立了 OSGi Enterprise Expert 组来确保企业 Java 应用程序可以利用 OSGi 的优势。OSGi Alliance 在 2010 年 3 月发布了 OSGi Service Platform Enterprise Specification(版本 4.2)的第一个版本,而 IBM 在同一年稍后的时间发布了面向 OSGi Applications 和 JPA 2.0 的 WebSphere Application Server Feature Pack。


相关主题


评论

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=WebSphere, Web development
ArticleID=753648
ArticleTitle=评论专栏: 如何实现良好的模块化以及为什么 OSGi 如此出众
publish-date=08252011