本文介绍了支持以“Ruby 方式”进行建模和代码生成的 RGen 框架 [1] 。从 MDA 和 MDD [2] (但是除开那些不严格遵守这些方法的行为)的意义上说,我使用了“建模”这个名词:模型是元模型的实例。元模型即是(或者是很大程度上接近于)领域特定语言(DSL)。模型转换被用来将模型转换成不同元模型的实例,代码生成是一种将模型转换成文本输出的特殊转换方法。
RGen 受到了 openArchitectureWare(oAW) [3] 这个有着相似应用范围的 Java 框架的影响。RGen 的核心思想不仅仅是使用 Ruby 在框架内作为应用程序的实现逻辑,而且还用来定义元模型,模型转换和代码生成。RGen 通过对每个切面提供内在的 DSLs,简化了这个过程。其他的项目也证明了 Ruby 十分适合在在这种情况下使用。一个著名的案例即是 Ruby on Rails [4] ,它包含了一些内置的 Ruby DSLs。但是,oAW 使用了一些外部的 DSLs 来定义模型转换和代码生成。
经验告诉我们 RGen 方法是非常的轻量级,而且极其灵活,使得开发更加高效,部署更加简易。我发现在那些启发式的项目中,即发现缺少支持的工具,却没有预先制定工具开发计划的项目中特别有用。使用 Ruby 和 RGen 我们能够以最小的努力来开发需要的工具,而且人们将会从这个工具中受益非凡。
使用诸如 RGen 和 oAW 这样框架的典型应用是代码生成器(例如在嵌入式设备中)和构建以及操纵模型的工具,通常以 XML 或者一种自由的文本或者图形语言表示。在本文中,我将会使用“C++ 代码生成器的 UML 状态图”作为例子。在现实世界中,我们仍然在上述的项目中使用 RGen 来为汽车的嵌入式电子控制单元(ECU)进行建模和生成代码的工作。
建模框架最重要的基本方面是表示模型和元模型的能力。元模型描述了特殊目的的模型看起来会是什么样,即定义了领域特定语言的抽象语法。典型地说,一个建模框架与元模型的应用和这两者之间的相互转换相关。
RGen 在 Ruby 中采用了一种前向的模型和元模型表示方法,就像面向对象的语言一样:对象用来表示模型元素,类用来表示元模型元素。模型和元模型之间的关系以及它们的 Ruby 表示法如图 1 所示。
为了支持领域特定语言,一个建模框架必须支持自定义的元模型。RGen 通过提供元模型定义语言,简化了这个过程,这个语言看起来和领域特定语言很相似。像其他的 DSL 一样,元模型定义语言的抽象语法也是由其元模型来定义,也就是所谓的元 - 元模型。
图 1:模型,元模型以及其 Ruby 表示
不像元模型,元 - 元模型在 RGen 里面已经得到了修复。此架构使用了 ECore – Eclipse 建模框架(EMF) [5] 的元 - 元模型。图 2 是 ECore 元模型的一个简单视图:在 ECore 中,元模型基本由一些以层级的包组织起来的类组成,这些类的属性和引用相互指向。从多个超类中可以抽象出一个类来。引用是无向的,但是可能会和一个相反的引用连接在一起,因此成为有向的引用。引用的目标便是其类型,必须是一个类;目标的角色便是引用的名字。属性是(非类的)数据类型的实力,可能是原生类型或者是枚举类型。
图 2:ECore 元模型的简化视图
和其他例如 oAW 的框架相反,RGen 的元模型定义语言的具体语法是 Ruby 形式,一种内部的 DSL。列表 1 描述了简单的状态机元模型:代码使用普通的 Ruby 关键词来定义一个模块 1 和一些类分别表示一个元模型包和元模型类。为了从普通的 Ruby 类和模块中分辨出这些元素,需要一些额外的代码:模块加上了一些特殊的 RGen 模块扩展(1),而且这些类都是继承于 RGen 元模型的基类 MMBase(2)。
元模型类的超类关系由 Ruby 类继承来表示(3)。注意到 Ruby 原始不支持多重继承,但是得益于其灵活性,一个特殊的 RGen 命令可以支持这个特性。这样,这个类必须是 MMMultiple(,,……)的返回值,这个方法是一个全局方法,用于在全局中构建一个中间超类。
<span># 表 1:元模型的状态机样例 </span> <span>module</span> <span>StatemachineMetamodel</span> extend <span>RGen</span>::<span>MetamodelBuilder</span>::<span>ModuleExtension</span> <span># (1)</span> <span>class</span> <span>ModelElement</span> < <span>RGen</span>::<span>MetamodelBuilder</span>::<span>MMBase</span> <span># (2)</span> has_attr <span><span>'</span><span>name</span><span>'</span></span>, <span>String</span> <span>end</span> <span>class</span> <span>Statemachine</span> < <span>ModelElement</span>; <span>end</span> <span># (3)</span> <span>class</span> <span>State</span> < <span>ModelElement</span>; <span>end</span> <span>class</span> <span>SimpleState</span> < <span>State</span>; <span>end</span> <span>class</span> <span>CompositeState</span> < <span>State</span>; <span>end</span> <span>class</span> <span>Transition</span> < <span>ModelElement</span> has_attr <span><span>'</span><span>trigger</span><span>'</span></span>, <span>String</span> <span># (4)</span> has_attr <span><span>'</span><span>action</span><span>'</span></span>, <span>String</span> <span>end</span> <span>Statemachine</span>.contains_one_uni <span><span>'</span><span>topState</span><span>'</span></span>, <span>State</span> <span># (5)</span> <span>Statemachine</span>.contains_many_uni <span><span>'</span><span>transitions</span><span>'</span></span>, <span>Transition</span> <span>CompositeState</span>.contains_many <span><span>'</span><span>subStates</span><span>'</span></span>, <span>State</span>, <span><span>'</span><span>container</span><span>'</span></span> <span>CompositeState</span>.has_one <span><span>'</span><span>initState</span><span>'</span></span>, <span>State</span> <span>State</span>.one_to_many <span><span>'</span><span>outgoingTransitions</span><span>'</span></span>, <span>Transition</span>, <span><span>'</span><span>sourceState</span><span>'</span></span> <span>State</span>.one_to_many <span><span>'</span><span>incomingTransitions</span><span>'</span></span>, <span>Transition</span>, <span><span>'</span><span>targetState</span><span>'</span></span> <span>end</span>
元模型的属性和引用由 MMBase 提供的特殊的类方法指定。因为 Ruby 类的定义是在相关代码执行的时候才被解释。在一个类的定义域内,当前对象是这个类的一个对象,所以这个类对象的方法能够被直接调用 2 .
has_attr 方法用来定义属性。它的参数是属性的名字以及一个原生的 Ruby 数据类型 3 (4)。RGen 在内部将 Ruby 类型映射成 ECore 的原生类型,这个例子中是转换成 EString。对于引用的每个规范,都有一些方法可用。 contains_one_uni 和 contains_many_uni 定义了单向的引用,而 one_to_one,one_to_many 和 many_to_many 则是双向的。这些方法在源类的对象中调用,第一个参数是目标的角色,然后是目标类和在双向引用(5)中源的角色。注意,目标类需要在能够被引用之前定义好。因为这个原因,大多数 RGen 元模型中的引用都是在类的定义外定义的。
当模型创建之后,元模型 Ruby 类会被初始化。属性值和引用目标都保存在相关对象的实例变量中。由于 Ruby 不允许对实例变量的直接访问,于是需要使用专用的存取方法。在 Ruby 的传统表示法中,getter 方法的名字和相关变量保持一致,setter 方法也是一样。setter 方法能够用在表达式的左边。
上述的类方法通过元编程方式来构建需要的存取方法。除开对实例变量的存取,这些方法还需要检查参数的类型,在特定时候需要抛出异常。运行时类型校验是构建于 Ruby 的顶端,此处是不需要原生的类型校验 4 。在一对多的引用中,getter 方法会返回一个数组,setter 方法也用数组来添加或者删除引用对象。在双向引用中,存取方法自动地加上反引用。
表 2 举了这样一个例子:常规的 Ruby 类初始化机制(1)能够被用来创建对象以及一个特殊的构造器,这个构造器能够方便地设置属性和引用(4)。注意包的名字需要标示出类的名字。将包中的模块加入到当前的命名空间之后,就能够避免重复(3)。状态 s1 的名字属性可以被设置,getter 方法的结果也需要检查(2)。一个传出转换被添加到 s1(to-many)中,然后自动创建的后向引用(to-one)会被检查(5)。转换的目标状态被设置为 s2,通过显式地复制(to-one),结果队列包含了一个元素 t1(to-many)。第二个目标状态创建之后使用另外一种转换连接到源状态。最后,所有传出转换的目标状态被设置成 s2 和 s3(7)。方法 targetState 是在 outgoingTransitions 的结果(数组)上再被调用的。这种简化的记法是可以被接受的,因为 RGen 扩展了 Ruby 数组,通过中转调用一个未知方法,存取其中的元素,然后将输出放入一个单独的集合中。
<span># 表 2:状态机元模型的初始化样例。</span> s1 = <span>StatemachineMetamodel</span>::<span>State</span>.new <span># (1)</span> s1.name = <span><span>'</span><span>SourceState</span><span>'</span></span> <span># (2)</span> assert_equal <span><span>'</span><span>SourceState</span><span>'</span></span>, s1.name include <span>StatemachineMetamodel</span> <span># (3)</span> s2 = <span>State</span>.new(<span>:name</span> => <span><span>'</span><span>TargetState1</span><span>'</span></span>) <span># (4)</span> t1 = <span>Transition</span>.new s1.addOutgoingTransitions(t1) <span># (5)</span> assert_equal s1, t1.sourceState t1.targetState = s2 <span># (6)</span> assert_equal [t1], s2.incomingTransitions s3 = <span>State</span>.new(<span>:name</span> => <span><span>'</span><span>TargetState2</span><span>'</span></span>) t2 = <span>Transition</span>.new(<span>:sourceState</span> => s1, <span>:targetState</span> => s3) <span># (7)</span> assert_equal [s2,s3], s1.outgoingTransitions.targetState
如上所示,RGen 元模型定义语言创建了需要表示 Ruby 元模型的模块、类和方法。不仅如此,元模型本身还可以作为一个普通的 RGen 模型。因为 RGen 包含了 ECore 的元模型,ECore 的元模型用其自身的元模型定义语言来表达。元模型的 RGen 模型能够通过表示元模型元素的 Ruby 类或者模块中的 Ecore 方法来存取。
列表 3 的例子是:在 StatemachineMetamodel 模块上调用 ecore 方法得到了 EPackage 的一个实例(1),在 State 类上调用会得到一个 EClass 的实力(2)。而且这两个都是属于同一个模型,事实上名字叫做“State”的 EClass 是叫做 “StatemachineMetamodel”的 EPackage 中的一个分类器(3)。元模型 Rgen 模型能够像其他的任何 RGen 模型那样被操作。样例代码说明了“State”类的超类有一个叫做“name”的属性(4)。
<span># 表 3:存取 ECore 元模型 </span> smPackage = <span>StatemachineMetamodel</span>.ecore assert smPackage.is_a?(<span>ECore</span>::<span>EPackage</span>) <span># (1)</span> assert_equal <span><span>'</span><span>StatemachineMetamodel</span><span>'</span></span>, smPackage.name stateClass = <span>StatemachineMetamodel</span>::<span>State</span>.ecore assert stateClass.is_a?(<span>ECore</span>::<span>EClass</span>) <span># (2)</span> assert_equal <span><span>'</span><span>State</span><span>'</span></span>, stateClass.name assert smPackage.eClassifiers.include?(stateClass) <span># (3)</span> assert stateClass.eSuperClasses.first.eAttributes.name.include?(<span><span>'</span><span>name</span><span>'</span></span>) <span># (4)</span>
作为一个普通的模型,元模型模型能够被任何可用的序列器和初始器序列化和初始化。RGen 包含了一个 XMI 序列器以及 XMI 初始器,使得元模型能够和 EMF 进行交换。同样地,元模型模型能够作为 RGen 模型转换的源或者目标,例如从或者到一个 UML 类模型。模型转换将会在下节中讲到。最后,使用 RGen 元模型生成器,元模型模型能够返回到 RGen 元模型 DSL 表示。图 3 总结了不同的元模型表示方法以及它们之间的关系。
图 3:RGen 元模型表示总结
RGen 元模型模型提供了类似于 EMF 的在元模型上的反射机制。反射机制对于程序员来说是非常有用的,例如,当实现一个自定义的模型序列器或者初始器的时候。事实上元 - 元模型是 ECore 用来确保大量的建模架构的可交换性:使用 RGen 元模型生成器,任何 ECore 元模型能够直接在 RGen 中使用。
表 4 表示了元模型模型到 XML(1)的序列化过程,和使用元模型生成器来重新生产 RGen DSL 表示法(2)一样。注意在这两个例子中,元模型是被 StatemachineMetamodel 的 ecore 方法返回到根 EPackage 元素所引用。生成的元模型的 DSL 表示法可以从文件中读取和解释(3)。为了避免在初始的类、模块和重载的类、模块之间的名字冲突,解释是在名字空间中进行。如果不考虑在重载版本中的“instanceClassName”属性值,那么这两个模型是一样的。
<span># 列表 4:序列化元模型 </span> <span>File</span>.open(<span><span>"</span><span>StatemachineMetamodel.ecore</span><span>"</span></span>,<span><span>"</span><span>w</span><span>"</span></span>) <span>do</span> |f| ser = <span>RGen</span>::<span>Serializer</span>::<span>XMI20Serializer</span>.new ser.serialize(<span>StatemachineMetamodel</span>.ecore) <span># (1)</span> f.write(ser.result) <span>end</span> include <span>MMGen</span>::<span>MetamodelGenerator</span> outfile = <span><span>"</span><span>StatemachineModel_regenerated.rb</span><span>"</span></span> generateMetamodel(<span>StatemachineMetamodel</span>.ecore, outfile) <span># (2)</span> <span>module</span> <span>Regenerated</span> <span>Inside</span> = binding <span>end</span> <span>File</span>.open(outfile) <span>do</span> |f| eval(f.read, <span>Regenerated</span>::<span>Inside</span>) <span># (3)</span> <span>end</span> include <span>RGen</span>::<span>ModelComparator</span> assert modelEqual?( <span>StatemachineMetamodel</span>.ecore, <span># (4)</span> <span>Regenerated</span>::<span>StatemachineMetamodel</span>.ecore, [<span><span>"</span><span>instanceClassName</span><span>"</span></span>])
现在 RGen 提供了对运行时元模型动态改变的有限支持。尤其是,ECore 元模型模型的改变不会影响内存中的 Ruby 元模型类和模块。但是,现在我们仍然致力于实现 Ruby 元模型表示的动态版本。动态元模型包含了动态类和模块,紧密地和 EClass 还有 EPackage ECore 元素联系在一起。当 ECore 元素被修改的时候,动态类和模块将会立刻改变它们的行为,即使实例已经存在。这个特性能够支持下节将要讲到的模型转换的高级技术。
RGen 最大的一个优势便是使用内部 DSLs 以及和 Ruby 紧密关联所获得的好处。这样使得程序员能够程式化地创建元模型类和模块,然后调用类的方法来创建属性和引用。利用这个优势的一个应用程序便是一种 XML 初始器,它在运行时根据遇到的 XML 标签和属性,以及一系列的映射规则,动态地创建目标元模型。RGen 分发版包含了这个初始器的原型版。
另外一个有意思的是使用内部 DSL,可以将元模型嵌入到常规代码中。这个特性是非常用帮助的,因为代码有的时候必须要处理复杂的,内部有联系的数据结构。开发者也许会考虑(元模型)类,属性和引用的结构,然后在定义域内使用它们。使用这种元模型方法,开发者能够决定在运行时 5 自动地检查属性和引用。
许多现实世界的建模应用能够从一些元模型的使用中获益。举例来说,一个应用程序可能会有内部元模型和一些特定的输入输出元模型。模型转换经常被用来将一个元模型的实例转换成另外一个元模型的实例,如图 4。
图 4:模型转换
考虑上面介绍的 Statemachine 例子,UML1.3 的 Statechart 模型的输入可以通过模型转换来添加。RGen 包含了 UML1.3 版的元模型,以 RGen 的元模型 DSL 来表示。RGen 同样也包含了一个 XMI 初始器,允许直接通过一个 UML 工具存储的 XML 文件创建一个 UML 元模型的实例。图 5 给出了一个来自于 [6] 的输入状态图的例子。
图 5:UML 状态图样例
除开创建一个新的目标模型,模型转换也可以用来修改源模型。这种情况叫做在位模型转换,它要求在转换的过程中,元模型的元素能够被改变。这种特性现在还不被 RGen 支持,但是如之上所述,我们现在在做这方面的工作。
举例来说,在位模型转换能够用在需要向后兼容地读取老版输入模型的场合下。新版工具中的输入元模型的每次更改能够使用一个内建的在位模型转换来提供向后支持。每一次这种转换只是服务于元模型和模型的一些少少改变,但是他也许需要用于大规模数据场合。在同样的源模型上使用一系列的在位转换,输入模型的迁移能够非常高效 6 。
像之前解释过的元模型定义 DSL 一样,RGen 提供了一个内部的 DSL 来定义模型转换,RGen 模型转换规范包括了源模型的单个元模型类的转换规则。规则定义了目标元模型类以及目标属性和引用的赋值,后者包括了应用转换规则所得到的结果。
图 6 以一个例子给出了转换规则的定义和应用 7 :源元模型类 A 的规则指定了目标元模型 A’,定义了多引用 b’以及单引用 c’的赋值。b’的目标值是转换源引用 b 的元素的结果。这意味着使用了元模型类 B(见下文)的相关规则(b’:=trans(b))。在 RGen 中,数组的转换结果也是一个数组,每个元素都是各自转换过。同样地,c’的值被指定为每一个引用 c 的元素的转换结果(c’:=trans(c))。元模型类 C 的转换规则反过来指定了引用 b1’和 b2’的值,b1’和 b2‘是引用了源引用 b1 和 b2 的元素的转换结果(b1’:=trans(b1),,b2’:=trans(b2))。对于元模型类 B 的转换,在这个例子中不需要更多的赋值。
图 6:转换规则的定义和应用
作为一个内部 DSL,模型转换语言采用了 Ruby 明文作为具体的语法。每一个模型转换都是由一个 Ruby 类来定义,这个 Ruby 类是通过一些特定的类方法,继承于 RGen 的 Transformer 类。最重要的是,转换类方法定义了转换规则,将源和目标元模型作为参数。属性和引用的赋值由一个 Ruby Hash 对象来指定,它将属性和引用的名字映射到实际的目标对象。Hash 对象是由一个和转换方法调用有关的代码块创建,这个转换方法调用是在元模型元素的上下文中使用。
注意,转换规则能够递归地使用其他规则。RGen 转换机制关心的是整个使用过程,它缓存了每个转换的结果。一个规则的代码块的执行结束标志是任何递归使用的规则的代码块执行结束。当自定义代码被加到这个代码块的时候,确定性行为尤其重要。表 5 是一个模型转换例子,它从 UML 1.3 元模型转换成之前介绍过得状态图元模型:一个新类 UmlToStatemachine 是继承于 RGen Transformer 类,为了是的目标类名字尽可能短(1),目标元模型模块被包含在当前命名空间中。一个常规的 Ruby 实例方法(在这个例子中叫做 transform)作为转换的入口点。它调用 trans 转换方法,当所有的状态机元素在输入模型 8 (2)的时候触发转换。trans 方法寻找已定义的转换规则,使用转换类的方法从源对象的类开始,如果没有规则的话,那么沿着其继承的层级向上寻找。 UML13::StateMachine 的转换规则指定了将要被转换成 StateMachine(3)的实例的元素。注意源和目标元模型类都是普通的 Ruby 类对象,而且必须使用 Ruby 命名空间机制。相关的代码块创建了一个 Hash 对象,给属性“name”和引用“transitions”和 “topState”赋值。当在源模型元素上调用继承的方法的时候,这些值都会在代码块的上下文中计算出来。对于引用目标值,trans 方法会递归地调用。
<span>#表 5:状态机模型的转换样例 </span> <span>class</span> <span>UmlToStatemachine</span> < <span>RGen</span>::<span>Transformer</span> <span># (1)</span> include <span>StatemachineMetamodel</span> <span>def</span> <span>transform</span> trans(<span>:class</span> => <span>UML13</span>::<span>StateMachine</span>) <span># (2)</span> <span>end</span> transform <span>UML13</span>::<span>StateMachine</span>, <span>:to</span> => <span>Statemachine</span> <span>do</span> { <span>:name</span> => name, <span>:transitions</span> => trans(transitions), <span># (3)</span> <span>:topState</span> => trans(top) } <span>end</span> transform <span>UML13</span>::<span>Transition</span>, <span>:to</span> => <span>Transition</span> <span>do</span> { <span>:sourceState</span> => trans(source), <span>:targetState</span> => trans(target), <span>:trigger</span> => trigger && trigger.name, <span>:action</span> => effect && effect.script.body } <span>end</span> transform <span>UML13</span>::<span>CompositeState</span>, <span>:to</span> => <span>CompositeState</span> <span>do</span> { <span>:name</span> => name, <span>:subStates</span> => trans(subvertex), <span>:initState</span> => trans(subvertex.find { |s| s.incoming.any?{ |t| t.source.is_a?(<span>UML13</span>::<span>Pseudostate</span>) && <span># (4)</span> t.source.kind == <span>:initial</span> }})} <span>end</span> transform <span>UML13</span>::<span>StateVertex</span>, <span>:to</span> => <span>:stateClass</span>, <span>:if</span> => <span>:transState</span> <span>do</span> <span># (5)</span> { <span>:name</span> => name, <span>:outgoingTransitions</span> => trans(outgoing), <span>:incomingTransitions</span> => trans(incoming) } <span>end</span> method <span>:stateClass</span> <span>do</span> (<span>@current_object</span>.is_a?(<span>UML13</span>::<span>Pseudostate</span>) && <span># (6)</span> kind == <span>:shallowHistory</span>)? <span>HistoryState</span> : <span>SimpleState</span> <span>end</span> method <span>:transState</span> <span>do</span> !(<span>@current_object</span>.is_a?(<span>UML13</span>::<span>Pseudostate</span>) && kind == <span>:initial</span>) <span>end</span> <span>end</span>
引用几乎任何 Ruby 代码都能够在创建 Hash 对象的代码块中使用,所以一些高级的赋值成为了可能:在例子中 CompositeState 的目标模型有一个对初始状态的显式引用,尽管在源元模型中初始状态被标记为从一个“初始”伪状态引入转换而来。使用 Ruby 内建的 Array 方法,这个转换能够实现,首先寻找一个有这样引入转换的子状态,然后使用 trans 方法(4)转换。
转换方法能够选择性地使用一个方法,计算目标类对象。在上面的例子中,UML13::StateVertex 需要被转换成 SimpleState 或者是 HistoryState,取决于 stateClass 方法(5)的结果。当有更多而可选的方法参数的时候,规则可以有条件的制定出来。在例子中,规则不会在伪状态中使用,结果将会是 nil,因为没有规则应用。除开常规的 Ruby 方法,Transformer 类提供了方法类的方法,允许定义这样一类方法,这类方法的方法体是在当前转换的源对象(6)的上下文中被求值。为了防止二义性,当前转换源对象能够使用 @current_object 实例变量存取。
由于调用转换类方法是使用的常规代码,它也可以以更加复杂精密的形式调用,允许对转换定义“脚本化”。实现 Transformer 类的拷贝类方法的一个不错的例子如表 6:方法使用一个源,然后选择性地使用一个目标元模型类,假设它们相同或者有相同的属性和引用。然后它在一个代码块中调用 transform 方法,自动地为每一个给定的元模型创建一个右赋值 hash 对象,通过元模型反射 9 从中寻找它的属性和引用。
<span>#表 6:Transformer 的拷贝命令实现 </span> <span>def</span> <span>self</span>.copy(from, to=<span>nil</span>) transform(from, <span>:to</span> => to || from) <span>do</span> <span>Hash</span>[*<span>@current_object</span>.class.ecore.eAllStructuralFeatures.inject([]) {|l,a| l + [a.name.to_sym, trans(<span>@current_object</span>.send(a.name))] }] <span>end</span> <span>end</span>
拷贝类方法也可以在任何元模型类中使用。如果需要对一个元模型做一个实例的深层次拷贝(clone),这里是一个创建给定元模型的副本的一般方法。表 7 的例子是 UML 1.3 元模型的拷贝转换。
<span>#表 7:UML1.3 元模型的拷贝转换样例 </span> <span>class</span> <span>UML13CopyTransformer</span> < <span>RGen</span>::<span>Transformer</span> include <span>UML13</span> <span>def</span> <span>transform</span> trans(<span>:class</span> => <span>UML13</span>::<span>Package</span>) <span>end</span> <span>UML13</span>.ecore.eClassifiers.each <span>do</span> |c| copy c.instanceClass <span>end</span> <span>end</span>
RGen 框架中,另外一个转换机制的有意思的应用是元模型反射的实现。当调用 ecore 方法的时候,接受的类或者模块被注入到内建的 ECore 转换器中,然后被应用属性和引用的转换规则。因为机制是如此的灵活,不仅仅需要元模型类,而且需要将 Ruby 类作为“输入元模型”,所以这类实现是可能的。
在 RGen 框架中,除开对模型的转换盒修改,代码生成是另外一种非常重要的应用。代码生成可以被认为是一种特殊的转换,将模型转换成文本输出。
RGen 框架包含一个基于生成器机制的模板,这个模板和 oAW 中的很相似。其他一些基于模板、模板文件和输出文件关系的模板在 RGen 和 oAW 中都是不同的:一个模板文件可能包含多个模板,一个模板可能创建多个输出文件,每个输出文件的内容可能是多个模板生成的。
图 7 的是这样一个例子:在文件“fileA.tpl”中定义了两个模板“tplA1”和“tplA2”,在文件“fileC.tpl”中定义了模板 “tplC1”(关键词定义)。模板“tplA1”创建了一个输出文件“out.txt”(关键词文件),在文件中写入了一行文字。然后扩展模板 “tplA2”和“tplC1”的内容,输出到相同的输出文件中(关键词扩展)。因为模板“tplC2”在一个不同的文件中,所以它的名字必须带上模板文件的相对路径。
图 7:RGen 生成器模板
当 RGen 模板被扩展的时候,它们的内容会在模型元素的上下文中被求值。每一个模板都和一个元模型类相关联,这些元模型类都是使用:来定义属性和扩展元素。默认情况下,扩展命令会在当前上下文对模板进行扩展,但是不同的上下文元素可以使用:或者:foreach 属性来指定。在后面的例子中,一个数组中的每个元素都会按照其模板展开。模板同样也可以被重载,根据不同上下文类型和扩展的规则,自动地选择合适的模板。
RGen 模板机制是构建在 ERB(嵌入式 Ruby)之上的,它为 Ruby 提供了最基本的模板支持。ERB 是标准 Ruby 的一部分,允许使用标 签 <%,<%= 和 %> 将 Ruby 代码嵌入到任意的文本中去。Ruby 模板语言包括了 ERB 语法以及一些额外的关键词,可以像常规的 Ruby 方法一样实现。这样的话,模板语言成为了 RGen 的另外一个内部 DSL。构建在标准的 ERB 机制上,实现是非常轻量级的。
一个代码生成的主要内容便是格式化输出:因为模板本身应该是人类可读的,额外的空格会对输出的可读性造成影响。一些方法能够很好的处理这种情况。但是这些方法可能会需要一些时间,而且需要额外的工具来实现,并且,有的时候对于某种特定的输出不可用。
RGen 模板语言提供了一个简单的输出格式化器,不需要任何额外的工具:默认来说,行首和行尾的空格会被删除掉。开发者然后 可以通过显示的 RGen 命令来控制缩进和空行的创建:iinc 和 idenc 用来设置当前的缩进级别,nl 用来插入一个空行。经验表明,添加格式化命令所付 出的努力是值得的。尤其是当特殊的输出需要格式化的时候,这种方法就非常有用。
例 8 给出了一个从状态图例子中得到的完整的模板。它用来产生一个为每个复合状态创建的 C++ 抽象类的头文件。跟随着状态模式和 [6] ,能够从这个类得到每个子状态的状态类。
#表 8:状态机生成器模板例子 <span><span><%</span> define <span><span>'</span><span>Header</span><span>'</span></span>, <span>:for</span> => <span>CompositeState</span> <span>do</span> <span>%></span></span> # (1) <span><span><%</span> file abstractSubstateClassName+<span><span>"</span><span>.h</span><span>"</span></span> <span>do</span> <span>%></span></span> <span><span><%</span> expand <span><span>'</span><span>/Util::IfdefHeader</span><span>'</span></span>, abstractSubstateClassName <span>%></span></span> # (2) class <span><span><%=</span> stateClassName <span>%></span></span>; <span><span><%</span>nl<span>%></span></span> class <span><span><%=</span> abstractSubstateClassName <span>%></span></span> # (3) { public:<span><span><%</span>iinc<span>%></span></span> # (4) <span><span><%=</span>abstractSubstateClassName<span>%></span></span>(<span><span><%=</span>stateClassName<span>%></span></span> &cont, char* name); virtual ~<span><span><%=</span> abstractSubstateClassName <span>%></span></span>() {}; <span><span><%</span>nl<span>%></span></span> <span><span><%=</span> stateClassName <span>%></span></span> &getContext() {<span><span><%</span>iinc<span>%></span></span> return fContext;<span><span><%</span>idec<span>%></span></span> } <span><span><%</span>nl<span>%></span></span> char *getName() { return fName; }; <span><span><%</span>nl<span>%></span></span> virtual void entryAction() {}; virtual void exitAction() {}; <span><span><%</span>nl<span>%></span></span> <span><span><%</span> <span>for</span> t <span>in</span> (outgoingTransitions + allSubstateTransitions).trigger <span>%></span></span> # (5) virtual void <span><span><%=</span> t <span>%></span></span>() {}; <span><span><%</span> <span>end</span> <span>%></span></span> <span><span><%</span>nl<span>%></span></span><span><span><%</span>idec<span>%></span></span> private:<span><span><%</span>iinc<span>%></span></span> char* fName; <span><span><%=</span> stateClassName <span>%></span></span> &fContext;<span><span><%</span>idec<span>%></span></span> }; <span><span><%</span> expand <span><span>'</span><span>/Util::IfdefFooter</span><span>'</span></span>, abstractSubstateClassName <span>%></span></span> <span><span><%</span> <span>end</span> <span>%></span></span> <span><span><%</span> <span>end</span> <span>%></span></span>
模板最开始是定义其名字和上下文元模型类,然后打开一个输出文件(1)。所有的 C/C++ 头文件必须有防止重复包含的宏定义,这个宏定义通常就是文件名的大写。模板“IfDefHeader”就是生成这样的宏定义(2)。在下一行开始进行类的定义(3)以及在“public”关键字之后增加缩进级别(4)。除了这些基本的方法之外,需要为每一个对象的传出转换声明一个虚方法。这个方法会被一个简单遍历所有相关的转换的 Ruby for 循环实现(5)。在这个 for 循环体中创建了方法的生命。为了将触发器的属性值写到输出中,这里使用了 <%= 而不是 <%。在模板的最后,需要在页脚添加防止重复包含的宏定义。
一般来说,所有的不需要逐字拷贝的模板输出是从模型表示的信息中创建的。上下文模型元素的属性存取方法能够被直接读取,其他的模型元素能够通过引用的父辈方法得到。并且,能够在模型元素的数组上调用方法是非常有用的。
但是在很多情况下,模型中的信息需要在用来输出之前进行处理。如果计算过于复杂或者需要在不同的场合下应用,那么将其实现为一个单独的方法是一个不错的注意。在上述例子中,stateClassName,abstractSubstateClassName 和 allSubstateTransitions 是元模型类 CompositeState 的派生属性 / 派生引用,但是是以方法的形式表示。
这类派生属性或者派生引用可以作为常规的 Ruby 元模型类方法来实现。但是,因为 RGen 支持元模型类的多重继承,因此需要特别注意。用户定义的方法必须只能添加到一个特殊的“类模块”中,这个事每一个 RGen 元模型的组成部分,并且可以通过常量 constant ClassModule 存取。
<span>#表 9:状态机元模型扩展例子 </span> require <span><span>'</span><span>rgen/name_helper</span><span>'</span></span> <span>module</span> <span>StatemachineMetamodel</span> include <span>RGen</span>::<span>NameHelper</span> <span># (1)</span> <span>module</span> <span>CompositeState::ClassModule</span> <span># (2)</span> <span>def</span> <span>stateClassName</span> container ? firstToUpper(name)+<span><span>"</span><span>State</span><span>"</span></span> : firstToUpper(name) <span># (3)</span> <span>end</span> <span>def</span> <span>abstractSubstateClassName</span> <span><span>"</span><span>Abstract</span><span>"</span></span>+firstToUpper(name)+<span><span>"</span><span>Substate</span><span>"</span></span> <span>end</span> <span>def</span> <span>realSubStates</span> subStates.reject{|s| s.is_a?(<span>HistoryState</span>)} <span>end</span> <span>def</span> <span>allSubstateTransitions</span> realSubStates.outgoingTransitions + <span># (4)</span> realSubStates.allSubstateTransitions <span>end</span> <span>end</span> <span>end</span>
表 9 的例子是派生属性和派生引用的实现:首先 StatemachineMetamodel 包模块被打开,然后加入一个 helper 模块(1)。在这个包模块中,CompositeState 类的类模块被打开(2)。在类中的方法实现利用了来自于混合模块的常规父辈方法、其他的派生属性、引用以及可能的方法(3)。allSubstateTransitions 的方法利用了 RGen 允许在数组上调用元素方法的特性,递归地实现。
注意在表 9 中的方法没有像原始元模型类那样在同一个文件中定义。Ruby 的“open classes”特性即可在不同的文件中定义方法。虽然可以再同一个文件中定义方法,但是建议使用一个或多个元模型扩展文件。这样的话,使用了 helper 方法的元模型就不会“乱成一团”了,尽管这种方法经常只是在特定的场合下使用。
在这个例子中,扩展文件被命名为“statemachine_metamodel_ext.rb”,扩展方法被用来生成代码。在实际应用中,根据项目的规模,对于一般扩展使用“statemachine_metamodel_ext.rb”,对于生成特殊扩展使用 “statemachine_metamodel_gen_ext.rb”是非常有用的。因为保存在另外一个文件中,因此生成器逻辑非常清晰。
代码生成需要加载模板文件和扩展根模板。表 10 的例子便是入耳在状态图中完成这个:首先创建 DirectoryTemplateContainer 的实例(1)。容器需要知道输出目录,例如:输出文件(关键词文件)创建的目录。而且也需要知道作为引用的命名空间的元模型。然后指定模板目录,加载模板文件(2)。在这个例子中,用来作为代码生成的模型元素通过模型转换放到当前的 RGen 环境中。根上下文元素(例如元模型 Statemachine 的实例)从环境中得到(3)。代码生成便是从扩展根模板开始的(4)。
<span>#表 10:启动生成器 </span> outdir = <span>File</span>.dirname(<span>__FILE__</span>)+<span><span>"</span><span>/../src</span><span>"</span></span> templatedir = <span>File</span>.dirname(<span>__FILE__</span>)+<span><span>"</span><span>/templates</span><span>"</span></span> tc = <span>RGen</span>::<span>TemplateLanguage</span>::<span>DirectoryTemplateContainer</span>.new( <span># (1)</span> <span>StatemachineMetamodel</span>, outdir) tc.load(templatedir) <span># (2)</span> stateMachine = envSM.find(<span>:class</span> => <span>StatemachineMetamodel</span>::<span>Statemachine</span>) <span># (3)</span> tc.expand(<span><span>'</span><span>Root::Root</span><span>'</span></span>, <span>:foreach</span> => stateMachine) <span># (4)</span>
表 11 的是最终生成器输出的一部分:文件“AbstractOperatingSubstate.h”是由表 8 的模板生成。注意现在不需要任何的后续处理。
<span>// Listing 11: Example C++ output file "AbstractOperatingSubstate.h"</span> <span>#ifndef</span> ABSTRACTOPERATINGSUBSTATE_H_ <span>#define</span> ABSTRACTOPERATINGSUBSTATE_H_ class OperatingState; class AbstractOperatingSubstate { <span>public:</span> AbstractOperatingSubstate(OperatingState &context, <span>char</span>* name); virtual ~AbstractOperatingSubstate() {}; OperatingState &getContext() { <span>return</span> fContext; } <span>char</span> *getName() { <span>return</span> fName; }; virtual <span>void</span> entryAction() {}; virtual <span>void</span> exitAction() {}; virtual <span>void</span> powerBut() {}; virtual <span>void</span> modeBut() {}; <span>private:</span> <span>char</span>* fName; OperatingState &fContext; }; <span>#endif</span> <span>/* ABSTRACTOPERATINGSUBSTATE_H_ */</span>
Ruby 是一种能够让程序员以一种简洁的方式表达思想的语言,它能够高效地开发易于维护的软件。RGen 将建模和代码生成加入到 Ruby 中,使得开发者能够以一种简单而且熟悉的形式处理建模和代码生成。
根据简单的原则,RGen 框架在规模上是轻量级的,开发者无需关心依赖和规则。因此在日常的脚本中使用框架是非常灵活的。
作为一个动态解释语言,Ruby 在开发中引入了一些不同。一个最优秀的特性是类型检查上无需编译器支持。另外一个是编辑器支持自动完成。这些在 RGen 中也同样支持,因为 RGen 完全依赖于 Ruby 语言的特性。在特定情况下,类似于 oAW 的框架使用外部 DSLs 能够提供更好的编辑器支持。
Ruby 开发者和其他的动态类型语言例如 Python 和 Smalltalk 的开发者一样,也对使用的语言缺点如数家珍:缺失编译器检查需要更多的密集(单元)测试,不过这个是个好办法。缺失编辑器支持不能很好地利用动态语言特性,但是动态语言的优秀的内建编辑器支持是非常困难的。
尤其在大型项目中,就更加需要这些特性的力量了。当程序员需要添加(运行时)检查的时候,这些特性的好处就很明显了。但是,语言本身支持这些特性是因为它允许定义特定的项目检查 DSLs。
RGen 元模型定义语言也可以看做是一个 DSL:它可以用来定义属性和引用,在运行时进行检查。也就是说一个 RG 元模型能够作为一个大型 Ruby 应用程序的股价。元模型同样支持开发者常识理解,尤其是使用 ECore 活 UML,甚至是图形可视化工具的时候。RGen 添加的类型检查也是保证程序和模型的核心数据的是处于一致性状态。
RGen 几年前启动的时候,只是作为一个将建模领域和 Ruby 绑定到一起的试验。正如前面说提及的,它已经在汽车工业的咨询项目中成功地作为一个原型工具。现在这个源性工具已经成熟,被用来在汽车电子控制单元(ECU)中作为常规的开发工具。
在这个工具中,一些元模型包含了大概 600 个元模型类。例如,我们的一个模型包含了大概 7000 个元素,最后会在大约一分钟内生成 90000 行代码。实现表明在特殊领域,基于 RGen 的方法能够比基于 Java 或者 C#的更容易完成,考虑到运行时和内存耗费。而且,这个工具能够单独地部署成一个大概 2.5MB 的可执行文件,包含了 Ruby 解释器,而且在服务器系统上运行不需要安装 10 。
虽然 RGen 本身是基于内部 Ruby DSLs 的,但是它还不支持程序员创建新的内部 Ruby DSLs 的具体语法。它同样还没有提供外部 DSLs 例如生成解析器和与发起的具体语法。现在,自定义元模型(抽象语法)的实例需要被以文中提及的程式化方法或者使用已经存在的初始器来创建。这个话题当然属于未来的改进方案。
本文中所使用的完整的代码可以在 RGen 项目主页上下载 [1] 。
基于 Ruby 的 RGen 框架提供了对处理模型和元模型的支持,能够定义模型转换和文本输出生成。由于使用了内部 DSLs,它和 Ruby 语言紧密地结合在一起。遵循 Ruby 设计原则,它是轻量而且灵活的,支持高效地开发,提供了编写简介可维护的代码方法。
RGen 在用于汽车工业作为建模和代码生成工具的时候是非常成功的。实现表明这种方法的高效,尽管还有着很多缺点,例如缺失编译器检查和编辑器支持。它仍然在运行时和内存使用上表现出了杰出的性能,你都不敢相信 Ruby 是一门解释语言。
除了 RGen 的工业应用,这个框架仍然在实验中使用。一个正在开发的扩展是对动态元模型的支持,这种动态元模型会在实例已经存在的情况下,运行的时候会发生改变。
[1] RGen, http://ruby-gen.org
[2] Model Driven Architecture, http://www.omg.org/mda
[3] openArchitectureWare, http://www.openarchitectureware.org
[4] Ruby on Rails, http://www.rubyonrails.org
[5] Eclipse Modelling Framework, http://www.eclipse.org/modeling/emf
[6] Iftikhar Azim Niaz and Jiro Tanaka , “Code Generation From Uml Statecharts”, Institute of Information Sciences and Electronics, University of Tsukuba, 2003
[7] rubyscript2exe, http://www.erikveen.dds.nl/rubyscript2exe
1 Ruby 模块被广泛用作命名空间和聚合方法,允许在类中进行混合插入。
2 这是在 Ruby 中实现 DSLs 的多种方法中的一种,被用在 Ruby on Rails 中。在 Rails 应用中,ActiveRecord::Base 被用来以一种和 RGen 相似的方法来定义元模型。和 ActiveRecord 不同的是,RGen 元模型不会需要连接数据库,而且使用 ECore 作为元元模型。
3 在 Ruby 中,类和数据类型没有区别,因为几乎所有的东西都是对象。某些类例如 String,Integer 或者 Float 作为原生类型。
4 Ruby 允许“duck typing”,也就是说对象的类型取决于其方法的使用环境。
5 在类似于 Ruby 这样的动态类型语言中,类型检查也是非常重要的。但是在一个静态类型语言中,开发者能够决定那些需要类型检查,那些不需要。
6 事实上,在 Ruby on Rails 中数据库迁移做的是几乎同一件事情:每个建议可能修改数据库或者模式的内容。一系列的迁移步骤将会将数据亏得内容放入一个特定的模式(或者元模型)版本。
7 为了简化例子,源和目标模型 / 元模型的结构是相同的,只有类和引用角色的名字不同。在一个典型的引用中,这不是必要的。
8 当一个转换器的实例被创建之后,构造器捡回引用 RGen 环境对象表示的源和目标模型。这些对象都是各个模型的元素数组。
9 在一个数组上调用注入的数组方法(ECore Features)。它将每个元素和上一次调用的结果传到代码块中。这样的话,构建一个包含特征名字和值的数组然后传递到 hash 类方法就创建了一个新的 Hash 对象。
10 rubyscript2exe [7] 被用来将程序,解释器以及所需的库“编译”到一个 Windows 可执行程序中去,然后在启动的时候自动解压和运行其中的内容。
感谢刘申对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。
评论