王宏明,林卫永,王泉荣,温业中
(通号万全信号设备有限公司,杭州 310000)
XML 是一种广泛使用的具有结构性和自描述性的标记语言,在有轨电车领域的工程建模、配置文件、数据交换等功能上常被使用。由于轨道交通领域的安全、效率等要求,C/C++语言常当做实现语言。但是由于C++语言缺少java、C#等高级语言的反射特性,将XML 文件读取到系统内部,转换为计算机能够理解的对象时,没有自动转换的方法,实现比较繁琐,代码中充斥着if、else 等判断,代码的可维护性、可扩展性都比较差。在将C++对象写入到XML 文件时,也需要通过定义一大堆的字符串来写到XML 文件中,代码的可维护性,可扩展性都不够好。文献[1]通过Java 来实现了XML到编程语言的转换,将XML 元素对应到定义好的Java 类中,但是对于如何具体实现XML 节点到Java 属性的载入没有涉及。文献[2]对于XML 和数据库的对象模型的转换数学算法进行了阐述,也没有对如何实现该机制进行详细的描述。文献[3]对于如何将一个定义好的结构体输出到XML 文件中进行研究,但是缺少通用性。
本文提出一种基于QT 的XML 文件自动转换的方法,只需要定义好类的数据结构,就能够自动的将符合要求的XML 载入进来,同时也能将C++对象按照数据结构定义,以XML 格式保存。
以有轨电车的软件领域经常用到的XML 配置文件为例来进行说明。
对于该文档,可以得到一个XML 模式定义D,按照文献[4]的五元组定义进行分析,可以得到文档树,如图1 所示。
图1 XML文档树Fig.1 XML file tree
对示例文档对象分析,可知,该文档需要3 个C++类进行对应。使用UML 图来表示3 个类及关系,如图2 所示。
图2 UML类图Fig.2 UML class diagram
对于XML 文档与C++类之间,已经得到对应的逻辑关系。但是在C++语言上,还需要解决下面2 个问题。
1)动态生成一个类对象实例
C++本身没有根据名字生成类对象实例的功能。想要在读取XML 的parameters 节点时生成QParameters 的实例,可以用C++的模板功能来模拟实现动态生成类对象实例的功能。示例代码如下:
通过createInstance
为了能够让C++程序能够通过名字查询到该函数,然后调用,还需要定义个函数指针类型。
typedef QObject* (*createInstance_func)(QObject* parent );
然后通过哈希表QHast
2)动态设置类属性的值
C++有运行时类型信息RTTI(Run-Time Type Identif ication),但是该信息只能用来鉴别类型,无法操作类的成员变量。
C++类本身没有属性,只有类成员变量及类成员函数,不提供动态设置成员变量的功能。但是QT 提供了一套元对象系统(Meta-Object System),可以帮助实现该功能。
首先要基于QT 的元对象系统来赋予C++类属性的功能。通过在类中使用以下定义:
Q_PROPE RT Y(int carl ength READ getCarlength WRITE getCarlength)
可以赋予C++类属性。示例中的carLength就是属性名。
有了属性以后,把这个XML 节点的值48 赋予属性carlength,其实现方式为调用QObject::setProperty 方法。
XML 文档的解析,主要有两种方式,一种文档对象模型DOM(Document Object Model),使用树形结构来描述XML 文档,层次结构清晰,较为符合人类的抽象认知,在处理过程中会将整个文档的内容都载入到内存中,内存占用率较高。另外一种是流式解析的SAX(simple API for XML),相比于DOM,SAX 的速度更快,效率更高,但它是逐行扫描,边扫描边解析,操作复杂。为便于说明,后面使用DOM 的方式来说明。
XML 文档的内容如图1 所示,configuration为根节点,parameters 和database 作为其子节点。对于每一个子节点,进行如图3 所示的处理。
1)根据传入的DOM 节点,判断是否有子节点。有子节点则认为是复合属性,否则为简单属性。
2)如果为简单属性,则提取子节点的内容,赋值给对象实例。然后继续处理下一子节点。
3)如果是复合属性,那么根据前述的createInstance 方法,按照子节点名,动态生成一个对应的对象实例。
4)对3)生成的对象实例进行内容组装。
图3 子节点处理流程Fig.3 Child node processing flow
5)判断当前子节点的属性名是否记录在List类型的信息中,如果是List 类型的,则按照List属性的方法来组装。
6)如果不是List 类型的属性,则将该对象实例的值赋予父实例对象。
通过以上步骤,可以通过递归的方式,高效得将XML 文件内容转换为C++的对象实例。
将C++对象实例转换XML 形式输出相对比较简单,只需要遍历C++类的所有属性,然后组成XML 节点即可,在此不再赘述。
依据动态对象生成原理和算法设计,实现部分主要分为两部分:动态对象生成的实现和对象实例组装的实现。
动态对象生成模块使用一个泛型类DynamicObjectFactory。 该 类 主 要 就 是 构建对象生成原理中的哈希表QHash
由于QT 中只有QObject 的派生类才能使用属性,所以要求的XML 相关的类需要从QObject 派生。通过此方法将类名字和创建对象实例的方法注册保存,以便后续使用。
对象实例的组装模块,定义了一个基类XmlBaseData,由基类来实现XML 转换的工作,那么以后使用时,只需要定义XML 节点对应的数据结构的类,即可将XML 转换为C++的对象实例。
XmlBaseData 的类结构如图4 所示。内部有两个静态的成员变量,分别记录List 类型的属性和复合属性,以便组装实例的时候名字的查找。
图4 XMLBaseData的UML类图Fig.4 UML class diagram of XMLBaseData
load 函数用于将XML 的内容组装到C++对象实例中,write 则将对象实例以XML 的方式写出到文件中。
preload 函数则是为了一些特殊的节点或类型预留,为一个返回0 的空函数。在派生类需要对某些节点进行特殊处理预留。
registerAllProp 函数是需要在派生类的构造函数中调用1 次的函数,即所有该类的实例只需要进行1 次处理的函数。该函数的作用是遍历当前类的所有属性,将所有的复合属性保存到complexProps 哈希表中,以便于后续的处理。
以本文中的示例XML 文档为例,需要创建如下的类,类关系如图5 所示。
3 个XML相关的类QConfiguration、QParameters、QDatabase 都从XmlBaseData 基类派生,本身不需要特别定义方法,只需定义数据的属性即可。在使用时,只需调用基类的load 函数即可将XML 文件内容读入。
图5 UML类关系图Fig.5 UML class relationship diagram
示例代码如图6 所示。
图6 示例代码Fig.6 Sample code
运行结果如图7 所示。
只需定义好数据结构的成员,通过短短3 ~5行代码就可以完成XML 读取的工作。
提出一种将XML 文档和C++对象实例的互相转换方法,并实现了该方法。该方法能够让开发人员关注于数据和业务,从繁琐的字符串比较,一大堆的if-else 组合中解放出来,使用较少的代码高效地实现了从XML 文件到C++对象实例的转换,具有通用性,同时也为特殊需要提供了接口预留。该方法使用了基于QT 的实现方式,由于QT 是跨平台的架构,所以该方法可以在windows 和Linux上跨平台使用。
图7 示例代码运行结果Fig.7 Operation result of sample code