使用 XML

安全编码实践,第 2 部分

任务的隔离

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 使用 XML

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

此内容是该系列的一部分:使用 XML

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

这一期文章中,我继续讨论常见的 XML 陷阱以及(可能更重要)如何避免这些陷阱。上一期专栏主要围绕着 XML 语法本身使用中的一些常见误解进行的。本文将考察如何在应用程序中结合 XML 支持来提高效率和可维护性。

编程语言和开发工具(包括数据库、IDE 和建模工具)对 XML 的支持不断增强。撰写本文的时候,Java 技术至少有 5 种与 XML 有关的正式 API:

  • Java Architecture for XML Binding (JAXB)
  • Java API for XML Processing (JAXP),可能是最庞大的 API,包括 4 部分:SAX、DOM、TrAX 和 XPath API
  • Java API for XML Registries (JAXR)
  • Java API for XML-based RPC (JAX-RPC)
  • SOAP with Attachments API for Java (SAAJ)

另外,还可以找到数不清的非正式 Java API,如 JDOM、PDOM、Castor 和 StAX(最终将集成到 JAXP 中)。而且重要的是,多数项目都对标准 API 提供了扩展,如 JAX-RPC 的 Axis 扩展。

选择太多了,令人无所适从。如果最后期限很短(这一行中通常如此),就毫不奇怪开发人员为什么常常选择最流行的 API(按照我的经验一般是 DOM),不再审慎考虑所作出的选择。

与此类似,谁有时间去设计一个 XML 词汇表呢?直接将对象层次转成 XML 标签似乎要快得多。

但是这方面的决策对能否在规定的时间和预算内交付产品有很大的影响,更不必说维护应用程序的难易程度了。

一句忠告:选择一种设计是不同特性之间权衡的结果。没有一种设计适合所有的需要,因此要花点时间对照应用程序需求来验证这种权衡。但我发现,与其他设计相比,有些设计似乎是更好的起点,后面我将对这些设计加以说明。

用 XML 作为接口

从设计的观点来看,这有助于把焦点放在 XML 文档的角色上,而不是放在它们的结构上。从结构上说,XML 文档是存储数据的仓库。更有趣的是,可以将 XML 文档用作应用程序之间的接口。

在这种情况下,一个应用程序将准备 XML 文档,而另一个应用程序则消费文档。这样的例子可以找到很多:XML 编辑器保存后的文档用于内容管理程序;新闻读者从 Web 服务器上下载 Atom 或 RSS;SOAP 客户机向服务器发送 SOAP 请求;Eclipse 平台读取 XML 插件描述文件,等等。

即使在最简单的情况下,即应用程序生成 XML 文件供自己消费,仍然可以将文档看作是应用程序两次运行(可能恰好是不同的版本)之间的接口。

如果将它看作是一个接口设计问题,那么用于 JavaBeans 和其他 API 的规则同样适用于 XML 设计。

契约式设计

已经提出的关于接口设计的很多方法论中,关系最密切的是 Eiffel 语言最先引入的契约式设计方法。

契约式设计的核心是,按照与其他组件签定的契约,详细说明每个组件的功能需求。同法律契约一样,契约中包含每个组件的责任和权利,即该组件提供什么,以及需要其他组件提供了什么。

从实践上来看,功能需求是根据数据结构和先决/后决条件来表达的。由于能够使用模式语言(如 W3C XML Schema 和 RELAX NG)以及 Schematron 这样的结构化验证语言,所以 XML 非常适合于这种方法。

定义的词汇表变成了组件之间的契约。

弹性

应用程序的功能需求肯定会随时间的推移而发生变化,支持它的 XML 词汇表也将发生变化。但是为了确保最后期限的疯狂冲刺,词汇表的设计很少考虑能否应付测试阶段。两个常见的问题是缺少版本控制方案和依赖于对象数据模型。

版本控制方案支持向后兼容,使以前的应用程序能够处理较新的应用程序生成的文件,反之亦然。良好的版本控制方案使:

  • 新的应用程序能以兼容模式工作。
  • 以前的应用程序能够打开新的文件而不会崩溃。

第一点很容易通过包含 version 标签或属性来实现。如果版本低于当前版本,应用程序必须求助于向后兼容性。

第二点需要更多的工作。因为不能修改以前的代码(否则该代码就是一个新应用程序了),所以必须从一开始就包括向前兼容性。具体而言,代码必须:

  • 知道如何处理新的标签,比如忽略或者报告错误(但是不能崩溃)。
  • 在文件破坏向后兼容性时,应该能够检测并报告错误。

后一点常常被忽视。新版本词汇表中所做的修改可能要求老的阅读程序不应处理该文件。版本控制方案必须提供一种机制报告这种情况,比如用名为 compatibleWith 的标签或属性规定处理该文档所需要的最小版本号。应用程序应该拒绝打开 compatibleWith 编号比自己的版本号高的文档。清单 1 给出了一个例子。

清单 1. 简单的版本控制方案
<v:root compatibleWith="3" version="5"
    xmlns:v="http://psol.com/2005/sample">
    <!-- content goes here -->
</v:root>

清单 1 是这个特定应用程序的版本 5 编写的,版本 3、版本 4 或版本 5 都可以处理它,但是不能用版本 1 或版本 2 处理它。

新的应用程序也可以使用不同的名称空间来表示不兼容的修改。

数据模型和词汇表

通常,人们已经围绕着应用程序的对象模型作了大量开发工作,因此,利用这些成果并从对象模型中衍生出 XML 模型似乎是合理的。

但实际上,这样可能造成维护方面的恶梦。构成好的对象模型的那些特点变成了 XML 词汇表的负担:

  • 对象模型常常包含冗余信息,比如用散列表加快搜索的速度,而冗余信息却意味着 XML 文档需要更多的验证。
  • 相反,模型可以动态地计算出为了进行验证而记录的信息。比如,对于在线购买,需要存储的不仅仅是每个产品行,还包括总价格,因为买主已经认可了总价格。在对象模型中,根据每个产品项重新计算总价格是完全合理的。
  • 虽然正在使用的开发工具和库不被认为是纯粹的 对象设计,但它们常常影响对象模型。比如,如果使用了一个小部件(widget)库,那么可能需要调整模型,以适应这些部件。
  • 对象模型的一部分可以使用位掩码(bit mask)或原生数组来优化,并牺牲可读性来提高速度。
  • 最后,对象模型不需要在应用程序的不同版本间保持稳定,在软件的演化中常常增加和删除属性。

如果 XML 词汇表是对象模型的直接映射,那么它不可能很稳定。这在开发中可能引起一些问题,因为需要不断地修改与 XML 有关的代码,所以在维护中甚至会造成更多的问题。要记住的是,文档是一个借口,因此必须在可行的情况下将其从实现细节中隔离出来。

相反,如果花点时间确定对象模型中稳定的关系,那么可以用最小的代价派生出更稳定的词汇表。使用 UML 和原型(stereotype),甚至可以从一个图同时生成对象模型和 XML 词汇表(请参阅参考资料中“UML, XMI, and code generation”一文)。

作为替换,可以把焦点集中到数据模型的功能 视图上。对象模型是实现应用程序各个细节的一种技术视图,随着技术的演化而改变。但是功能视图要稳定得多,应用程序即便迁移到新的平台上,基本上仍然可以提供相同的一组服务。

从应用程序一方来看

现在已经讨论了 XML 词汇表,还需要考察一下应用程序。同样,基本的原理是将 XML 处理看作是一种接口来封装。

最糟糕的情况(我恐惧地发现经常如此)是围绕着 DOM(或者 JDOM)树来设计应用程序。除了少数例外(比如浏览器),DOM 使用起来一直都是一种可怕的对象模型。

DOM 看起来很有吸引力,因为它容易使用而且相当通用。很多开发人员认为,它能胜任交给它的任何任务,而且多数情况下确实如此。很多开发人员通过 DOM 学习了 XML 编程,因此认定 DOM 是最合理的选择。

DOM 是 W3C 为一类限制相当严格的应用程序设计的,这类应用程序就是 Web 浏览器。对于相关的应用程序,包括编辑器和 XML 处理工具,它工作得很好,但是对于一般用途的应用程序,它的表现不是最理想的。

使用 DOM 模型最主要的问题是,它迫使您将 XML 代码分散到整个应用程序中。更好的办法是将 XML 代码集中到一两个包中。

我回想起一个项目,我曾经审查过围绕着 DOM 树建立的相当大的产品。在这个应用程序中,差不多每个类都要从 XML 树中提取数据或者向此树中插入数据。如图 1 所示,可以看到基本上每个包都依赖于 DOM 树。

图 1. 该模型中每个包都依赖于 DOM
该模型中每个包都依赖于 DOM
该模型中每个包都依赖于 DOM

事件证明,由于多种原因,这样做难以调试、难以处理、难以维护:

  • 从字符串到原生类型的转换不统一。验证不一致,不同的例程以不同的方式解释同一 DOM 树。
  • DOM 中的信息不是理想的格式,造成算法非常复杂。
  • 难以跟踪变化。一个例程更新 DOM 树后,可能造成其他例程不能访问同一数据。
  • XML 词汇表中的任何变化都需要团队成员检查成千上万行代码。
  • 调试极其困难,无人知道哪一个例程造成了 DOM 树中的哪一种结果。

对于 SQL 数据库,分解从对象模型中加载的数据是一种好办法。这一点同样适用于 XML。

图 2 示范了一种更健壮的替代方案,它依靠应用程序定义的对象模型,将处理 XML 的代码隔离在 Serialization 包中。

图 2. 隔离 XML 代码
隔离 XML 代码
隔离 XML 代码

通过这个专用的包,可以将模型装入内存,或者将它序列化为 XML。这样做的好处包括:

  • 在应用程序精心定义的某一部分中封装(隔离)处理 XML 的代码。可能仍然涉及到成千上万行代码,不过可以单独进行测试。
  • 有利于更一致地使用 XML,因为 XML 代码由一名开发人员负责(我也曾遇到过需要整个团队的大规模 XML 代码)。
  • 加载的时候,可以重新组织数据,以便更好地适应算法。比如,加载到散列表或者数据库中可以帮助处理大型数据集。
  • 更容易采用模型/视图/控制器(MVC)范型。
  • 对象模型从 XML 模型中解耦出来,可以独立演化。

显然,序列化例程可能还要使用 DOM 实际进行解析(虽然我倾向于认为 SAX 更有效)。还可以看看 JAXB 或 Castor,它们基本上自动生成序列化包。

上述好处的代价是什么呢?一开始的代价要高一些,不过您很快就会发现,封装 XML 代码的时候节省了时间。在开发的第一个阶段,通常都希望 XML 词汇表能够较快地演化。即使在这个阶段,也会看到将这些变动局部化所带来的好处。

结束语

本文给我们的启示是:一点点深谋远虑可以带来极大的回报。花一点时间来设计应用程序和隔离 XML 组件吧。

下一期文章将回顾十分重要的验证问题。


相关主题


评论

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML
ArticleID=90843
ArticleTitle=使用 XML: 安全编码实践,第 2 部分
publish-date=08012005