Java应用架构设计:模块化模式与 OSGi(3)

 由  张卫滨 发布

第3章 架构与模块化

模块化在软件架构中扮演着重要的角色。它填补了自从用Java开发企业软件系统以来就一直存在的空白。本章将会讨论这个空白领域并探讨模块化是如何作为重要的中间技术填补这个空白的。

3.1 定义架构

关于架构(architecture)这个词,有多种定义。但是在这些定义中存在一个共同的主题以及一些关键词。以下是诸多定义中的一个,它由 Booch、Rumbaugh 和 Jacobson 提出(1999):

架构就是一系列重要的决策,这些决策涉及软件系统的组织、组成系统的结构化元素及其接口的选择、元素之间协作时特定的行为、结构化元素和行为元素形成更大子系统的组合方式以及引导这一组织——也就是这些元素及其接口、它们之间的协作以及组合——的架构风格。

现在看一下 ANSI/IEEE Std 1471—2000(开放组织,the open group)的定义:

一个系统的基本组织,通过组件、组件之间和组件与环境之间的关系以及管理其设计和演变的原则具体体现。

在开放组织架构框架(The Open Group Architecture Framework,TOGAF)中,架构根据上下文有两个含义:

1)系统的正式描述,或者系统在组件级别的详细计划,这个计划会指导系统的实现。

2)组件的结构、它们之间的关系以及管理其设计随时间演变的原则指导

查看这些定义会发现有很多共同的关键词,在这些定义中我们用黑体突出显示。重要的潜在含义都是由这些关键词呈现的。但是,这些关键词引出了一些重要的问题,为了完全理解架构就必须要回答这些问题。是什么使得一个决策具备架构上的重要性?组成元素是什么?我们如何适应架构的演化?为了实现模块化必须要做什么?在分析这些问题前,我想从一个关于软件架构的故事开始。

3.2 关于软件架构的一个故事

软件架构使我想到了下面的这个故事(霍金,1998):

一位著名的科学家(据说是贝特郎·罗素)曾经作过一次关于天文学方面的讲演。他描述了地球如何绕着太阳运动,以及太阳又是如何绕着我们称为星系的巨大的恒星群的中心转动。演讲结束之时,一位坐在房间后排的矮个老妇人站起来说:“你说的这些都是废话。这个世界实际上是驮在大乌龟背上的一块平板。”这位科学家很有教养地微笑着答道:“那么这只乌龟站在什么上面呢?”“你很聪明,年轻人,的确很聪明,”老妇人说,“不过,这是一只驮着一只一直驮下去的乌龟群啊!”

——《时间简史》史蒂芬·霍金

软件架构就是“一只驮着一只一直驮下去的乌龟群”。 这是怎样做到的呢?这一节将会讨论这些想法。

3.2.1 象牙塔

我们中的很多人可能都接触过象牙塔。在一些功能失调的组织中,架构师和开发人员不能有效地交流。结果就是双方都缺少透明度和理解。如图 3.1 所示,架构师将他们的智慧转送给开发人员,而开发人员不能将高层次的理念转化为具体的实现。经常会出现故障,这是因为(尽管我知道还有其他的原因)架构师关注广度而开发人员关注深度。每个组 对软件架构都有不同的见解,尽管两者都信誓旦旦,但是 在他们所理解的中间区域存 在一个空白。架构师可能关注应用和服务,而开发人员可 能更关注代码。遗憾的是, 在他们的关注点之间还有很多事情。广度和深度之间的空 白区域形成了象牙塔架构。

改编自 http://www.rendell.org/jam/upload/2009/1/tower-12054835.jpg
【图片3.1 象牙塔】

3.2.2 乌龟和塔

毫无疑问,象牙塔架构的功能是有问题的,它的表现就是缺乏架构上的完整性。假设为了帮助架构师和开发人员,要怎样弥补广度和深度之间的空白呢?如何才能更有效地交流呢?怎样才能增加相互理解和透明度呢?

让我们通过另一种定义形式重新看一下软件架构的定义。我最欣赏的软件架构定义是 Ralph Johnson 提出的,Martin Fowler 在一篇文章中(2003)对此进行了引用。他说:

在大多数成功的软件项目中,从事该项目的专家开发人员对系统的设计存在共识。这种共识称为 “架构”。共识包括如何将系统分为组件以及组件如何通过接口进行交互。这些组件通常会由更小的组件组成,但是架构只包括组件以及能够被所有开发人员所理解的接口……架构是重要的事情,无论它是什么。

这个定义与本章前面的那些定义的关键区别在于“共识”,它表示软件架构中有社会性的方面。对于系统如何拆分为组件以及它们之间怎样交互,我们必须建立共识。架构不仅是一些技术理念,它也是一个社会性的结构。通过架构的社会性方面,我们可以弥合架构师和开发人员之间的分歧。

为了保证共识,必须要实现自上而下的架构(architecture all the way down,参见 6.1 节)。架构师不能仅关注服务,开发人员也不能仅关注代码。每个组必须都要关注巨大的中间地带,如图 3.2 所示。

【图 3.2 自上而下的架构】

仅关注高层抽象是不够的。只强调代码质量也是不够的。我们必须通过其他方式消除这个断层,这包括模块和包设计。通常,在各种会议上演讲时,我会要求那些从事服务设 计的听众举手,很多手举了起来。我还会 要求那些从事类设计和代码质量的人举手,同样,有很多手举了起来。但是,当我要求那些从事包和模块设计的人举手时,只有很少比例的人举手了。

这是很令人遗憾的,模块和包的设计在重要性方面是与服务和类的设计相同的。但是在这个过程中,我们强调了服务和代码质量,而忽略了它们之间还有什么。在每个应用和服务内部都会出现腐化的设计,即便是在最灵活的代码上构建的应用和服务,也有可能充斥着重复难以理解的代码。富有弹性的包结构以及对应的软件模块将会帮助你消除服务和代码之间的断层。模块化是很重要的中间技术,它能够帮助我们实现自上而下的架构,这种架构会填充广度和深度之间的鸿沟。

我们需要关注模块化,以保证所讲述的架构故事始终是一致的。它是将一切绑定在一起的黏合剂。它有助于连接低层次的类设计与高层次的服务设计。它能帮助降低象牙塔、增强通信、增加透明性、确保互相理解并检查多个层级的一致性。它使得我们能够实现自上而下的架构并实现架构的目标。

3.3 架构的目标

模块化不仅有助于解决软件架构中社会性方面的问题,还会帮助我们设计更加灵活的软件系统,也就是,具备弹性、高适应性以及可维护性架构的系统。查看一下之前关于架构的定义,就会得到架构的目标。Fowler 所引用的由 Johnson 给出的架构定义表明架构就是关于重要的事情的。在以下表述中,Booch 明确说明如果有的事情很难改变,那么在架构上它就是重要的:

所有的架构都是设计,但并不是所有的设计都是架构。架构表现为重要的设计决策,这些决策会形成系统,在这里重要性是通过变化的成本衡量的。

基于以上表述,可以得到这样的结论:软件架构的目标必须是减少变化的影响和成本,从而消除架构上的重要性。通过创建灵活易变化的方案,我们试图使得一些事情在架构上不那么重要,如图 3.3 所示。但是这里存在一个悖论。

【图 3.3 架构的目标 】

3.3.1 悖论

消除架构并不是新的理念。实际上,Fowler 在他的文章“谁需要架构师?”(Who Needs an Architect?,2003)中提到了“丢掉软件架构”(getting rid of software architecture)。通过减少变化的影响和成本来消除架构的必由之路就是灵活性。系统越灵活,就越能按需 进 行适 应 和进 化。 但 是这 里 有一 个悖 论 ,Ralph Johnson 的表 述支 持 了这 个 观点(Fowler 2003):

……让每件事都易于变化会使得整个系统非常复杂……

随着灵活性的增长,复杂度(参见 4.1 节)也会随之增加。复杂性是我们要试图驯服的怪兽,因为复杂的事情比简单的事情更难以处理。可以肯定的是,没有明确的道路通往胜利。但是,如果既能驯服复杂性又能增加灵活性,如图 3.4 所示,那又会怎么样呢?现在探讨一下设计灵活的软件系统同时又不增加复杂度的可能性。可能吗?换句话说,怎样消除架构?

【图 3.4 最大化灵活性,管理复杂度 】

3.3.2 消除架构

正如对 Johnson 的引用中所明确指出的,设计无限灵活的系统是不可行的。因此,我们很有必要识别出在什么地方保证灵活性从而减少变化的影响和成本。但挑战在于,我们并不是总能在项目的初期就知道最终哪里会发生变化,所以对于我们不知道的事情无法创建一个灵活的方案。这就是预先设计的大架构(Big Architecture Up Front,BAUF)所面临的问题,这也是我们需要根据当时的情况做出架构决策的原因。换句话说,在有充足的知识保证我们能够做出最明智的决策之前,我们应当尽可能地延迟做出具体的架构决策,因为那样会使我们局限在一个具体的解决方案中。

当面临不确定时,对问题进行隔离并做出决策前我们要十分慎重并要确保当未知问题的答案出现时,初始决策能够灵活变化,我们这样做也是基于以上的原因。为此,模块化是被忽略的一个部分,它能够帮助我们尽可能地减少变化带来的影响和成本,这是我们使用模块化架构设计软件系统的驱动力。在《UML User Guide》“为系统中的接缝(seam)建模”。他(1999):

识别系统中的接缝涉及识别在系统体系架构中明确的分界线。在这些分界线 的每一侧,都会发现可独立变化的组件,只要在分界线两侧的组件遵循由接口描述的契约,在一侧变化的组件就不会影响另一侧的组件。

Booch 称为组件的地方,我们称为模块。Booch 称为接缝的地方,我们称为结合点(joint,参见 4.5 节)。模块化以及设计模式和 SOLID 原则(参见附录),代表了我们尽可能减少变化的影响和成本的最好愿望,同时它也会消除变化所带来的架构重要性。

3.4 模块化:被忽视的部分

架构定义的两个关键因素是组件和组合(参见 1.1节)。但是对于组件 4(实际上,这使我想起了架构也是如此)这个词还没有一个标准的和公认的定义。大多数的地方都使用这个词代表“一块代码”(a chunk of code)。但那是不合适的,在 OSGi 环境中,模块显 然是一个软件组件。开发具备适应性、灵 活性以及可维护性架构的系统需要模块化,这是因为我们必须设计灵活的系统,以便在开发过程中面临变化时,能够根据当时的情况做出决定。模块化一直是被忽略的一个部分,它能够让我们更容易地适应变化并且关注系统中那些需要灵活性的特定领域,如图 3.5 所示。改变封装在一个模块中的设计要比改变分散在多个模块中的设计更容易一些。

它是真正的封装吗

在标准的 Java 中,没有办法强制将设计细节封装在模块中,因为 Java 没有提供将包或类定义为模块作用域的方法。于是,一个模块中的类总是够访问另一个模块的实现细节。这就是像 OSGi 这样的模块化框架,能够发挥作用的地方了,因为它能够在清单文件头中显式声明导入包和导出包,进而强制将实现细节封装在模块中。即便是包中公开的类,只要这个包没有显式声明为导出,其他的模块也不能进行访问。区别很细小,但是很重要。在贯穿本书的模式中,我们将会看到几个这样的例子,当这些例子出现时,我会明确指出。现在,先看一个简单的例子(关于在没有运行时模块系统环境下的模块化,参见 2.3 节)。

【图 3.5 封装性设计】

1.标准 Java:没有封装

图 3.6 展现了 Client 类要依赖 Inter 接口,Impl 提供了实现。Client 类打包在 client.jar 模块中,Inter 和 Impl 打包在 provider.jar 模块中。这是一个关于模块化系统的好例子,但它展现了在标准 Java 中,我们是无法封装实现细节的,因为没有办法阻止对 Impl 的访问。provider.jar 模块外的类依然可以接触到 Impl 类,并且能够直接实例化和使用它。

实际上,Impl 是作为包作用域的类进行定义的,如程序清单 3.1 所示。但是部署在 client.jar 模块中的 Spring XML 配置文件 AppContext.xml 依然可以在运行时创建 Impl 实例并将其注入 Client 中。AppContext.xml 和 Client 类分别如程序清单 3.2 和程序清单 3.3 所示。关键问题在于 AppContext.xml 部署在 client.jar 模块中,而它创建的 Impl 类部署在provider.jar 模块中。如程序清单 3.2 所示,部署在 client.jar 模块中的 AppContext.xml 违反了封装性,因为它引用了 provider.jar 模块的实现细节。因为 Spring 的配置是全局配置,所以结果就是违反了封装性。

【图 3.6 标准 Java 不能封装模块中的设计细节 程序清单 3.1 类 Impl 】

【程序清单 3.1 类 Impl 】
package com.p2.imp1;

import com.p2.*;

class Impl implement Inter {
      public void doIt() { ... /* any implementation */ }
}

【程序清单 3.2 Spring 配置文件 AppContext.xml 】
<beans>
      <bean id="inter" class="com.p2.imp1.Imp1"/>
</beans>

【程序清单 3.3 类 Client】
package com.p1;

import com.p2.*;
import org.springframework.context.*;
import org.springframework.context.support.*;

public class Client {
    public static void main(String args[]) {
        ApplicationContext appContext = new 
        FileSystemXmlApplicationContext(
            "com/pl/appContext.xml");
        Inter i = (Inter) appContext.getBean("inter");
        i.doIt();
    }
}

2.OSGi 与封装

现在,来看看相同的例子用OSGi(参见第13 章)如何实现。这里,provider.jar 模块中的 Impl 类严格进行了封装,Impl 对其他模块中的类是不可见的。Impl 类和 Inter 接口与前面的例子中是一样的,不需要任何变化。我们将已存在的应用放在了 OSGi 框架中,它会强制封装模块的实现细节并提供模块间进行交流的一种机制。

图 3.7 展示了这种新的结构。实际上,它是抽象化模块模式(Abstract Modules pattern,参见 11.1 节)的一个样例。这里将 Spring XML 配置文件拆分成了 4 个不同的文件。本来我可以只使用两个配置文件,但是对于每个模块我希望将标准 Java 和 OSGi 框架的配置分离开。provider.jar 模块负责对自己进行配置并在安装时暴露它的服务功能。在描述这种方式之前,这里对每个配置文件进行简单的介绍。

【图 3.7 使用 OSGi 实现的封装性设计 】
  • client.xml:标准的 Spring 配置文件,它描述 OSGi 该如何启动应用。

  • client-osgi.xml:Spring 配置文件,它允许 Client 类使用 OSGi µService。

  • provider.xml:Spring 配置,包含 provider.jar 模块中的 bean 定义。

  • provider-osgi.xml:Spring 配置,它将 provider.xml 中的 bean 定义为 OSGi µService。

在了解这两个模块如何装配在一起之前,先看一下 provider.jar 模块,它包含了 Inter接口、Impl 实现以及两个配置文件。重复一遍,Inter 和 Impl 与前面的样例是相同的,所以我们只看一下配置文件。图 3.7 中的 provider.xml 文件定义了标准的 Spring bean 配置,也就是之前 AppContext.xml 文件所展示的那样。程序清单 3.4 展现provider.xml 文件。关键的一点在于这个配置文件要部署在 provider.jar 模块中。不能试图在 provider.jar 模块外实例化 Impl 类。因为 OSGi 保证了封装,所以任何试图访问模块实现细节的行为将会导致运行时错误,如 ClassNotFoundException 异常。

【程序清单 3.4 provider.xml 配置文件】
<beans>
      <bean id="inter" class="com.p2.impl.Impl"/>
</beans>

OSGi如何阻止其他的类直接实例化Impl类呢? provider.jar 模块中的清单文件(Manifest.mf)只提供了 com.p2 包中的类,并不含 com.p2.impl 包。所以,注册为 OSGi µService 的 Inter 接口能够被其他的模块访问,但是 Impl 类就不能被访问了。程序清单 3.5 展示了清单文件中导出包的那一部分。

【程序清单 3.5 provider.jar 模块的 Manifest.mf 文件声明了所要导出的包 】
Export-Package: com.p2

provider-osgi.xml 文件使得事情变得很有意思,通过它provider.jar 模块的行为提供为 OSGi µService,这个服务会作为 Client 和 Impl 之间的协议。为 provider.jar 模块提供配置的文件放在 provider.jar 模块中,所以并没有违反封装的事情发生。

程序清单 3.6 展示了这个配置。使用 OSGi 框架注册的 µService 服务称为 interService,它引用了程序清单 3.4 中定义的 Impl bean,在这里以 Inter 类型提供行为。此时,provider.jar 模块有一个名为 interService 的 OSGi µService,它可以被其他的模块使用。在 provider.jar 模块在 OSGi 框架中安装并激活后,这个服务就可用了。

【程序清单 3.6 provider-osgi.xml 配置文件 】
<osgi:service id="interService" ref="inter"
      interface="com.p2.Inter"/>

现在,看一下 client.jar 模块。client.xml 文件会配置 Client 类。它实际上将程序清单 3.3 中 Client 类的 main 方法替换为 run 方法,OSGi 框架会实例化 Client 类,使用 Inter 类型对其进行配置,并会调用 run 方法。程序清单 3.7 和程序清单 3.8 分别展现了 client.xml 文件和 Client 类。这种机制会进行初始化处理并取代之前样例中 Client 类的 main 方法。

【程序清单 3.7 client.xml 配置文件 】
<beans>
      <bean name="client" class="com.p1.impl.Client"
        init-method="run"/>
                <property name="inter"
                 ref="interService"/>
       </bean>   
</beans>

【程序清单 3.8 Client 类 】
package com.p1.imp1;

import com.p2.*;
import com.p1.*;

public class Client {
    private Inter i;
    public void setInter(Inter i) {
        this.i =i;
    }
    
    public void run() throws Exception {
        i.doIt();
    }
}

注入 Client 类中的 Inter 类型是通过 client-osgi.xml 使用一个 Inter 类型的 µService,如程序清单 3.9 所示。

【程序清单 3.9 client-osgi.xml 配置文件 】
<osgi:reference id="interService"
      interface="com.p2.Inter"/>

client.jar 模块的清单文件导入了 com.p2 包,这样就可以访问 Inter µService 了。程序清单 3.10 展示了 client.jar 模块导入和导出包这部分的清单文件配置。

【程序清单 3.10 client.jar 模块的 Manifest.mf 文件】
Import-Package: com.p2

这个简单的例子有多个很有意思的设计。provider.jar 模块是独立部署的(Independent Deployment pattern,独立部署模式,参见9.5节)。它不依赖其他模块,并且将自己的行为提供为µService。在这个系统中,没有其他模块需要知道这些细节。

将 Impl 类和 Inter 接口分别打包到不同模块中,会使设计更灵活。通过将接口从实现中分离,可以给系统带来更大的灵活性,尤其是在Abstraction pattern,分离抽象模式,参加 11.3 节)。

乍看起来,它似乎也违反了外部配置模式(External Configuration pattern,参见 10.2 节)。在为一个模块定义外部配置的时候,还希望能够保证将实现细节封装起来。外部配置更多的是允许客户端配置模块到上下文环境中,并不是关于提供模块实现细节的。

这个示例所能带给我们的最关键一点就是 provider.jar 模块中的类进行了严格的封装,因为 OSGi 框架强制实现了类型的可见性。我们只暴露了模块导出包中的公开类,µService 机制使得模块间能够以一种很灵活的方式交流。µService 跨越了系统的结合点(参见 4.5 节),因为 OSGi 是动态的,所以对µService 的依赖也是如此。µService 的实现可以在运行时添加或移除,系统可以在它们出现时绑定新的实例。

在后面的讨论中,会看到更多的例子。即使使用标准的 Java 无法强制封装模块的实现,但设计更加模块化的软件系统依然是很必要的。你将会看到,通过使用本书中讨论的一些技术,当使用运行时模块系统时,我们会占据一个很有利的位置。

3.5 回答我们的问题

之前,在引入软件架构的三种 定义时,我们曾经提出过如下 问题。通过前面的解释,我们回答了这些问题。但为了更清楚一些,让我们再简单回答一下:

是什么使一个决策具备架构上的重要性?如果变化的影响和成本很大,那么这样的决策就具备架构上的重要性。

组成元素是什么?组成元素包括类、模块以及服务。

怎样适应架构的演化?演化是通过设计灵活的解决方案实现的,这样的方案能够适应变化。但是灵活性也会带来复杂性,我们必须在系统合适的地方构建灵活性。

3.6 结论

架构的目标是尽可能减少变化的影响和成本。模块化通过填补高层架构组件以及底层代码之间的空白,帮助我们实现这个目标。模块化是帮助我们增加架构敏捷性的重要媒介。它填补了架构师和开发人员之间的断层。借助它,我们可以创建适应变化的软件架构。模块化能够帮助我们实现自上而下的架构。

3.7 参考文献

Booch, Grady, James Rumbaugh, and Ivar1999. The Unified Modeling Language User Guide. Reading, MA: Addison-Wesley.

The Open Group.The Open Group Architecture Framework. www.open-group.org/architecture/togaf8-doc/arch/chap01.html

Hawking, Stephen. 1998. A Brief History of Time. Bantam.

Fowler, Martin. 2003. “Who Needs an Architect?” IEEE Software.

Booch, Grady. 2006. On Design.

www.handbookofsoftwarearchitecture.com/index.jsp?page=Blog&part=All


hobbs136 2013-12-17 11:12

--------<br>---------<br>-----------<br>应该是这样一些块,这些块高内聚,osgi提出了一个实现标准,如果由jvm来实现不是更好?

顶(0) 踩(0) 回复

沉寒枫 2013-12-04 12:06

回复admott: 他在说.net封闭,不开源吗?

顶(0) 踩(0) 回复

泪雨迷情 2013-11-20 13:16

不错,期待下一章节

顶(0) 踩(0) 回复

兴华 2013-11-05 13:45

这一章看了,很有收货,写的不错啊.

顶(0) 踩(0) 回复

admott 2013-10-14 21:47

回复thor: 这位老兄厉害,讽刺.NET弱点的论调实在太绝,没一定历练的人,估计看不出这段是反话。

顶(0) 踩(0) 回复

thor 2013-10-13 19:25

java 从语言的角度上看真的是老化落后了,支持模块化本就该是语言本身和jvm负责的事情,用一个internal关键字声明包、类或方法来实现模块封装,再支持个模块的加载和卸载以满足热部署的需要,这真的有那么难吗?osgi能实现,为什么jvm自己不能实现?无非就是操作classloader,jvm自己做不是更自然?osgi的发明本身就是一种无奈。语言的设计上真该多学学microsoft,从100多年前的COM,到.NET处处都有模块化的影子,封装和面向接口的编程方式早就被应用了无数岁月,反观java这么多年来就出了个osgi...真是差距啊。

顶(2) 踩(0) 回复

Neuromancer 2013-10-10 22:20

软件架构就是“一只驮着一只一直驮下去的乌龟群”。 在最高层的乌龟将自己的智慧写在它下面一只乌龟的背上,然后这只乌龟根据自己的理解,将理解后的智慧写在它下面的一只乌龟上,一只一只的写下去,结果呢。。。。

顶(0) 踩(0) 回复

Tang Yong 2013-09-28 00:29

另外,翻译质量非常高!

顶(0) 踩(0) 回复

Tang Yong 2013-09-28 00:28

这一节写得很好!尤其是对架构和模块化的阐述,其他书找不到。

顶(0) 踩(0) 回复
查看评论