1、引言
如何处理编程中遇到的复杂问题是IT人员常遇到的最大挑战之一。因此我们非常需要各种工具来帮助我们更好地理解和管理软件开发中的复杂性。这样的工具不仅能够帮助我们创立更好的程序源代码,而且能够提高软件的开发效率。源代码产生器是一种能够从简单模型产生复杂程序源代码的工具。所以,源代码产生器是帮助我们妥善处理软件复杂性的理想候选工具。
XML是W3C(World Wide Web Consortium)设计来描述结构数据的公共标准。XSL(eXtensible Stylesheet Language)则是用来转换和格式化XML的相关语言。如果说XML是一种表达信息的工具,那么XSL就是用来操作这些信息的语言。
XML和XSL是应因特网交换和展示数据的需求而产生的。工业界普遍接受XML的另外一个原因是这种技术对许多复杂问题常常能够提供意想不到的解决方案。但是如今关于XSL最引人注目的秘密大概是它可以用来创立你自己的源代码产生器。使用XML和XSL一起来建立源代码产生模板可能是这两种技术中另一项引人注目的应用。
我们将在本文中通过一个简单实例探讨使用XML和XSL创立源代码产生器的可能性。XML用来描述所解决问题的概念模型,XSL从XML模型中读取信息并产生程序源代码。
这里我们假定读者已经熟悉什么是XML以及XSL的有关基础知识。许多书籍已经对此进行了详细介绍,读者可以找来参阅。
2、源代码产生器
2.1、如何处理复杂性
复杂问题经常需要复杂的解决方案并导致复杂的源程序代码。虽然一个好的设计方案可以导致比较简单和容易理解的程序源代码,但是它的应用也有其局限性。包含在概念模型中的信息与语言中的人为因素混为一起妨碍了隐含在程序源代码后面的设计思想。这使得将需求转化为程序源代码变得更加困难,同时也影响了对实现进行修正。对此问题的一个处理方式就是将概念模型与将此概念模型转化为程序源代码的过程分离开来,而使用产生源代码的工具来产生最终代码。
第一种类型的源代码产生器需要两种输入:描述问题范畴的信息模型与产生源代码所需要的处理指令即实现逻辑(见图一)。一个描述信息模型结构的总体模型也是必要的。这个总体模型是一个包含了总体数据的关于模型的模型或者关于信息的信息。第二种类型的源代码产生器具有更精致的体系结构。它可以产生中间设计模型并可以对中间设计模型迭代数次后再产生最终的源代码(见图二)。
分离的信息模型允许我们将商务需求直接映射为模型元素。另一方面,分离的实现逻辑使得对实现进行修改变得更容易。这些修改既可以是小至编程风格的改变,也可以是大至另一种实现语言的转换。所产生代码中的任何重复都不会影响到信息模型,它受实现逻辑控制。
2.2、源代码产生器与剪贴
每一个人都同意剪贴在编程中不可取。因为它造成代码重复,因而使得软件维护更加困难。将重复的代码分离为另一个函数或类是比较可取的一种方法。但是,现实生活中剪贴不仅是一种快速、“肮脏”的源代码再利用方式,而且有时是唯一的选择。
在处理源代码再利用时,并不是所有的编程语言都一样。C++无可非议地提供了最有效的源代码再利用工具:继承(inheritance)、模板(template)及宏(macro)。但是即使这样强有力的语言也不可避免代码重复。例如,一个类方法就需要在头文件.h中申明,在.cpp文件中实现。这就造成了对任何类方法签名(method signature)的改变都需要涉及到两处地方。
这还仅仅是冰山一角。更大的源代码重复来源于在将概念模型转化为软件程序时并不是所有的不可分信息总能映射为一个语素。这就造成了表述相同信息的代码在不同的地方重复出现。源代码的重复使得程序更难理解和维护。
做为一个例子,让我们来考虑仅有几个属性(property)的Employee类。这个类的概念模型如图三所示。按照通常的编程习惯,一个类的属性经常是通过一个私有(private)域和相应的公开(public)类方法get及set来实现。最终的设计模型与实现代码也见于图三中。此法中,属性的名称及类型这些信息就不得不在几处地方出现。
其它语言,如微软的C#,通过引进新的语言特性来显性支持类的属性。这使得类属性的实现较小依赖于剪贴。但是任何语言都不可能为了消除剪贴而无限扩展来增加所需特性。
造成信息重复的另一根源是由于随着产品相关问题的不断提出,新的代码不断被添加到一个原本非常简单的类原型(class prototype)中。真正的产品代码经常涉及到一些新的错误处理、单元测试和大量使用说明。一个简单的类可能需要包裹起来才能在命令行或COM组件中使用。以COM中包裹C++类为例,一方面关于类的信息必须在IDL文件中重复,另一方面与COM相关的支持又必须添加进来。一个具有GUI界面的COM组件就需要Visual Basic代码来显示和操作类的功能(functionality)。与数据库相关的则需要建立能够存取类对象的库表(table),以及选择(select)、更新(update)、插入(insert)、删除(delete)这些库表的SQL代码。最终,在现实世界里一个简单的类可能需要由分布在几个文件中由不同语言编写的数千行代码来支持。而隐含在这些文件中的信息绝大部分则是重复的,可能仅仅来源于类的几个属性。
2.3、其它优点
利用源代码产生器自动产生代码使得编程格式更容易保持一致。假如你要求两个人来编写一个简单的Employee类,你可能得到两个完全不同的版本。利用源代码产生器来建立Employee类,你就能够保证其代码一致。任何编程中的格式、风格、规则、好的习惯都可以通过产生器的实现逻辑来得以保证。
源代码的重新组织是每一个编程人员日常工作的一部分。但这也是一项既难又易出错的剪贴工作。将实现逻辑放在源代码产生器中简化了源代码重组过程。我们只需要修改代码产生逻辑就可以了。
自动产生源代码的唯一不足是需要建立信息模型和编写实现逻辑,特别是模型、产生逻辑、辅助代码需要由不同的语言编写时麻烦更大。但这可以通过让具有不同技能的人分工建立不同的模型来解决:具有商务知识的人建立信息模型,具有编程知识的人建立代码产生逻辑。这一方法从根本上改变了软件的开发过程。尽管商务专家和编程人员间仍然需要协作,但是他们可以独立建立自己的模型。
3、XSL源代码产生器
3.1、为什么采用XML和XSL
建立源代码产生器的一个重要决策是利用何种形式来表述信息模型(information model)和实现逻辑(implementaion logic)。XML作为一种通用数据格式因其在因特网上交换数据的独立性和平台中立而被工业界广泛接受。XML的标准化、灵活性、和通用性使其成为表达信息模型的当然首选。因此,使用XSL来表述实现逻辑也就顺理成章了。
另一选择是用XSL来产生UML模型,而不是直接产生源代码。OMG定义的MOF(Meta Object Facility)语言可以用来描述基于UML的整体模型(meta-model)。XMI(XML Metadata Interchange Format)是基于XML表述UML的格式语言。从XML信息模型中产生中间设计模型可以通过XMI或MOF来实现。这里转换成中间XMI模型的过程应该是一目了然的,因为XSL简化了从XML到XMI的转换。这种具有中间设计模型的方法如图二所示。
本文将探讨如何利用XSL建立源代码产生器模板。该模板从包含了信息模型的XML文件中读取信息并产生最终代码。本文采用了一个简单例子以便读者能够更好的理解如何使用XML和XSL来编写自己的源代码产生器。利用XSL将信息模型转换为源代码的一个重要优势是XSL为申明语言。XSL是申明语言是因为它只描述转换结果,而不是如何进行转换的具体步骤。这是较高一级的抽象。当然XSL也提供了一些程序控制结构。但是这些特性与真正的编程语言相比很有限。因此复杂的源代码产生逻辑最好由XSL和某种编程语言结合表述。
3.2、工作原理
一个XML文件组织的如同一个树状结构。每一个节点具有名称、值、属性和子节点。XML这种既简单又实用的设计使得几乎任何信息都可以存储于XML文件。一个XML文件可以与一个文件类型定义—DTD(Document Type Definition)文件结合。DTD文件存有描述XML文件结构的数据。XML文件可以通过相应的DTD文件来鉴定以保证其结构有效。
XSL由一系列规则组成。这些规则决定了如何处理XML文件中的相应元素。每一个规则都有与自己相匹配的标准。这些标准规定了在何种情况下该规则被激活。每一条规则也有与自己相关联的转换XML元素的格式。XSL规则由不同等级组成,因为每一条规则可以通过<xsl:apply-templates>标签来激发其它规则。
下面的规则经常遇到,其意义也显而易见。<xsl:value-of>用来从XML文件中选择某个元素的值。<xsl:text>用来指定文本。<xsl:if>和<xsl:when>用来模拟if-then-else和switch控制结构。<xsl:for-each>迭代XML元素集中的每一个元素。<xsl:include>将一个XSL文件引进到另一个文件中。
用XSL处理XML文件需要一个XSL处理器。该处理器读取XML文件和XSL格式表格,然后利用XSL转换规则对XML文件进行相关转换。本文使用了James Clark先生开发的免费XSL处理器—XT,读者可在www.jclark.com/xml/xt.html获得该处理器及相关信息。
源代码产生器需要如下文件
(1)DTD文件,该文件描述总体模型。
(2)XML文件,该文件描述信息模型。
(3)XSL文件,该文件用来产生源代码。
XML文件中描述信息模型的数据结构必须与总体模型DTD文件中定义的一致。XSL源代码产生模板从XML文件中读取信息,并产生相应源代码。
用XML和XSL产生程序源代码的技术由下列几个步骤构成(如图一所示)
(1)商务分析人员(business analyst)建立概念总体模型DTD文件。该模型规定XML文件中的数据结构。这些数据包含了XML文件中的元素名称,可能属性,以及各元素间关系。
例如,在建立图一中的总体模型第一步时,我们可能考虑此模型仅由class元素组成。每一个class应具有name属性。
<!ATTLIST class
name NMTOKEN #REQUIRED>
每一个class还应具有一个properties选项元素。其中一个元素的零或一势(cardinality)用?表示。
<!ELEMENT class (properties?)>
properties元素是由几个property元素组成的集。其中一个元素的零或多势(cardinality)用*表示。
<!ELEMENT properties (property*)>
每一个property元素应具有name属性和type属性。
<!ATTLIST property
name NMTOKEN #REQUIRED
type CDATA #REQUIRED>
上面的这些DTD语句规定了一个简化的总体模型。在后面我们会进一步对此进行扩展。
(2)商务专家(business expert)建立一个信息模型XML文件。这个文件由具体的元素、元素的属性、及子元素组成。元素的结构必须与第一步建立的DTD文件中定义的结构一致。
例如对于一个具有属性Name,类型为string和属性Salary,类型为double的Employee类,其XML文件可以定义为
<class name="Employee">
<properties>
<property name="Name"
type="string">
</property>
<property name="Salary"
type="double">
</property>
</properties>
</class>
(3)程序开发员(programmer)利用XSL建立源代码产生逻辑。XSL文件分为两部分。第一部分从XML文件中读取信息。第二部分利用读取的信息产生源代码。
例如如下的XSL模板对XML文件中的class元素进行操作。该模板首先插入C++关键词class,然后选择当前class元素的name属性,接着插入{,激活包含在子元素properties集中的每一个子元素property模板,最后插入}。
<xsl:template match="class">
<xsl:text>class </xsl:text>
<xsl:value-of select="@name"/>
<xsl:text>
{</xsl:text>
<xsl:for-each
select="properties/property">
<xsl:apply-templates select="."/>
</xsl:for-each>
<xsl:text>
};</xsl:text>
</xsl:template>
子元素property的XSL模板插入几个缩进空格,选择当前property元素的type属性,接着插入空格和_,选择该元素的name属性,最后插入;。
<xsl:template match="property">
<xsl:text>
</xsl:text>
<xsl:value-of select="@type"/>
<xsl:text> _</xsl:text>
<xsl:value-of select="@name"/>
<xsl:text>;</xsl:text>
</xsl:template>
最终产生的源代码如下
class Employee
{
string _Name;
double _Salary;
};
虽然这是一个相当简单的例子,但是已经勾画出了编写一个源代码产生器的具体步骤。
4、具体实例
下面我们具体讨论一个实例。我们将按照上节的步骤来建立自己的源代码产生器。
4.1、整体模型(meta-model)
整体模型应由商务分析人员来建立。整体模型规定了模型的结构。我们的整体模型是由类构成的。这些类具有属性和相关类函数。图四是我们整体模型的UML模型。根据这个整体模型而定义的DTD文件Model.dtd如清单一所示。
清单一 整体模型Model.dtd
— Listing 1: Model.dtd — The meta-model —
<!ELEMENT class (info?, dependencies?, uses?,
parents?, methods?, properties?)
>
<!ATTLIST class
name NMTOKEN #REQUIRED
package NMTOKEN #IMPLIED
>
<!ELEMENT dependencies (dependency*)>
<!ELEMENT dependency (#PCDATA)>
<!ELEMENT uses (use*)>
<!ELEMENT use (#PCDATA)>
<!ELEMENT parents (parent*)>
<!ELEMENT parent (#PCDATA)>
<!ATTLIST parent
name NMTOKEN #REQUIRED
visibility (public | private) "public"
>
<!ELEMENT methods (method*)>
<!ELEMENT method (info?, return?, params*, exceptions*)>
<!ATTLIST method
name NMTOKEN #REQUIRED
type CDATA #REQUIRED
visibility (public | protected | private) "public"
modifier (virtual | static) ""
const (true | false) "false"
>
<!ELEMENT params (param*)>
<!ELEMENT param (info?)>
<!ATTLIST param
name NMTOKEN #REQUIRED
type CDATA #REQUIRED
default CDATA #IMPLIED
>
<!ELEMENT properties (property*)>
<!ELEMENT property (info?)>
<!ATTLIST property
name NMTOKEN #REQUIRED
type CDATA #REQUIRED
has_get (true | false) "true"
has_set (true | false) "true"
has_data (true | false) "true"
is_unique (true | false) "false"
>
<!ELEMENT info (#PCDATA)>
<!ENTITY amp "&" >
<!ENTITY lt "<" >
<!ENTITY gt ">" >
<!ENTITY quot """ >
— End of Listing —