作者 Martin Thiede 译者 杨晨 发布于 2009年7月28日 上午11时14分
本文介绍了支持以“Ruby方式”进行建模和代码生成的RGen框架[1] 。从MDA和MDD[2] (但是除开那些不严格遵守这些方法的行为)的意义上说,我使用了“建模”这个名词:模型是元模型的实例。元模型即是(或者是很大程度上接近于)领域特定语言(DSL)。模型转换被用来将模型转换成不同元模型的实例,代码生成是一种将模型转换成文本输出的特殊转换方法。
8月8日Glassfish水族馆馆长JimJiang主讲"JSF演义"
Adobe Flash平台技术开发精华,汇集RIA/Flash精华内容
InfoQ独家提供Adobe Flash平台相关工具高速下载
Adobe Flash Builder4 beta,前50名下载用户可额外获得免费账号
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(,,……)的返回值,这个方法是一个全局方法,用于在全局中构建一个中间超类。
# 表1:元模型的状态机样例 module StatemachineMetamodel extend RGen::MetamodelBuilder::ModuleExtension # (1) class ModelElement < RGen::MetamodelBuilder::MMBase # (2) has_attr 'name', String end class Statemachine < ModelElement; end # (3) class State < ModelElement; end class SimpleState < State; end class CompositeState < State; end class Transition < ModelElement has_attr 'trigger', String # (4) has_attr 'action', String end Statemachine.contains_one_uni 'topState', State # (5) Statemachine.contains_many_uni 'transitions', Transition CompositeState.contains_many 'subStates', State, 'container' CompositeState.has_one 'initState', State State.one_to_many 'outgoingTransitions', Transition, 'sourceState' State.one_to_many 'incomingTransitions', Transition, 'targetState' end
元模型的属性和引用由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数组,通过中转调用一个未知方法,存取其中的元素,然后将输出放入一个单独的集合中。
# 表2:状态机元模型的初始化样例。 s1 = StatemachineMetamodel::State.new # (1) s1.name = 'SourceState' # (2) assert_equal 'SourceState', s1.name include StatemachineMetamodel # (3) s2 = State.new(:name => 'TargetState1') # (4) t1 = Transition.new s1.addOutgoingTransitions(t1) # (5) assert_equal s1, t1.sourceState t1.targetState = s2 # (6) assert_equal [t1], s2.incomingTransitions s3 = State.new(:name => 'TargetState2') t2 = Transition.new(:sourceState => s1, :targetState => s3) # (7) 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)。
# 表3:存取ECore元模型 smPackage = StatemachineMetamodel.ecore assert smPackage.is_a?(ECore::EPackage) # (1) assert_equal 'StatemachineMetamodel', smPackage.name stateClass = StatemachineMetamodel::State.ecore assert stateClass.is_a?(ECore::EClass) # (2) assert_equal 'State', stateClass.name assert smPackage.eClassifiers.include?(stateClass) # (3) assert stateClass.eSuperClasses.first.eAttributes.name.include?('name') # (4)
作为一个普通的模型,元模型模型能够被任何可用的序列器和初始器序列化和初始化。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”属性值,那么这两个模型是一样的。
# 列表4:序列化元模型 File.open("StatemachineMetamodel.ecore","w") do |f| ser = RGen::Serializer::XMI20Serializer.new ser.serialize(StatemachineMetamodel.ecore) # (1) f.write(ser.result) end include MMGen::MetamodelGenerator outfile = "StatemachineModel_regenerated.rb" generateMetamodel(StatemachineMetamodel.ecore, outfile) # (2) module Regenerated Inside = binding end File.open(outfile) do |f| eval(f.read, Regenerated::Inside) # (3) end include RGen::ModelComparator assert modelEqual?( StatemachineMetamodel.ecore, # (4) Regenerated::StatemachineMetamodel.ecore, ["instanceClassName"])
现在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方法会递归地调 用。
#表5:状态机模型的转换样例 class UmlToStatemachine < RGen::Transformer # (1) include StatemachineMetamodel def transform trans(:class => UML13::StateMachine) # (2) end transform UML13::StateMachine, :to => Statemachine do { :name => name, :transitions => trans(transitions), # (3) :topState => trans(top) } end transform UML13::Transition, :to => Transition do { :sourceState => trans(source), :targetState => trans(target), :trigger => trigger && trigger.name, :action => effect && effect.script.body } end transform UML13::CompositeState, :to => CompositeState do { :name => name, :subStates => trans(subvertex), :initState => trans(subvertex.find { |s| s.incoming.any?{ |t| t.source.is_a?(UML13::Pseudostate) && # (4) t.source.kind == :initial }})} end transform UML13::StateVertex, :to => :stateClass, :if => :transState do # (5) { :name => name, :outgoingTransitions => trans(outgoing), :incomingTransitions => trans(incoming) } end method :stateClass do (@current_object.is_a?(UML13::Pseudostate) && # (6) kind == :shallowHistory)? HistoryState : SimpleState end method :transState do !(@current_object.is_a?(UML13::Pseudostate) && kind == :initial) end end
引用几乎任何Ruby代码都能够在创建Hash对象的代码块中使用,所以一些高级的赋值成为了可能:在例子中CompositeState的目标模 型有一个对初始状态的显式引用,尽管在源元模型中初始状态被标记为从一个“初始”伪状态引入转换而来。使用Ruby内建的Array方法,这个转换能够实 现,首先寻找一个有这样引入转换的子状态,然后使用trans方法(4)转换。
转换方法能够选择性地使用一个方法,计算目标类对象。在上面的例子中,UML13::StateVertex需要被转换成SimpleState或 者是 HistoryState,取决于stateClass方法(5)的结果。当有更多而可选的方法参数的时候,规则可以有条件的制定出来。在例子中,规则不 会在伪状态中使用,结果将会是nil,因为没有规则应用。除开常规的Ruby方法,Transformer类提供了方法类的方法,允许定义这样一类方法, 这类方法的方法体是在当前转换的源对象(6)的上下文中被求值。为了防止二义性,当前转换源对象能够使用@current_object实例变量存取。
由于调用转换类方法是使用的常规代码,它也可以以更加复杂精密的形式调用,允许对转换定义“脚本化”。实现Transformer类的拷贝类方法的 一个不错的例子如表6:方法使用一个源,然后选择性地使用一个目标元模型类,假设它们相同或者有相同的属性和引用。然后它在一个代码块中调用 transform 方法,自动地为每一个给定的元模型创建一个右赋值hash对象,通过元模型反射9 从中寻找它的属性和引用。
#表6:Transformer的拷贝命令实现 def self.copy(from, to=nil) transform(from, :to => to || from) do Hash[*@current_object.class.ecore.eAllStructuralFeatures.inject([]) {|l,a| l + [a.name.to_sym, trans(@current_object.send(a.name))] }] end end
拷贝类方法也可以在任何元模型类中使用。如果需要对一个元模型做一个实例的深层次拷贝(clone),这里是一个创建给定元模型的副本的一般方法。表7的例子是UML 1.3元模型的拷贝转换。
#表7:UML1.3元模型的拷贝转换样例 class UML13CopyTransformer < RGen::Transformer include UML13 def transform trans(:class => UML13::Package) end UML13.ecore.eClassifiers.each do |c| copy c.instanceClass end end
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:状态机生成器模板例子 <% define 'Header', :for => CompositeState do %> # (1) <% file abstractSubstateClassName+".h" do %> <% expand '/Util::IfdefHeader', abstractSubstateClassName %> # (2) class <%= stateClassName %>; <%nl%> class <%= abstractSubstateClassName %> # (3) { public:<%iinc%> # (4) <%=abstractSubstateClassName%>(<%=stateClassName%> &cont, char* name); virtual ~<%= abstractSubstateClassName %>() {}; <%nl%> <%= stateClassName %> &getContext() {<%iinc%> return fContext;<%idec%> } <%nl%> char *getName() { return fName; }; <%nl%> virtual void entryAction() {}; virtual void exitAction() {}; <%nl%> <% for t in (outgoingTransitions + allSubstateTransitions).trigger %> # (5) virtual void <%= t %>() {}; <% end %> <%nl%><%idec%> private:<%iinc%> char* fName; <%= stateClassName %> &fContext;<%idec%> }; <% expand '/Util::IfdefFooter', abstractSubstateClassName %> <% end %> <% end %>
模板最开始是定义其名字和上下文元模型类,然后打开一个输出文件(1)。所有的C/C++头文件必须有防止重复包含的宏定义,这个宏定义通常就是文 件名的大写。模板“IfDefHeader”就是生成这样的宏定义(2)。在下一行开始进行类的定义(3)以及在“public”关键字之后增加缩进级别 (4)。除了这些基本的方法之外,需要为每一个对象的传出转换声明一个虚方法。这个方法会被一个简单遍历所有相关的转换的Ruby for循环实现(5)。在这个for循环体中创建了方法的生命。为了将触发器的属性值写到输出中,这里使用了<%=而不是<%。在模板的最 后,需要在页脚添加防止重复包含的宏定义。
一般来说,所有的不需要逐字拷贝的模板输出是从模型表示的信息中创建的。上下文模型元素的属性存取方法能够被直接读取,其他的模型元素能够通过引用的父辈方法得到。并且,能够在模型元素的数组上调用方法是非常有用的。
但是在很多情况下,模型中的信息需要在用来输出之前进行处理。如果计算过于复杂或者需要在不同的场合下应用,那么将其实现为一个单独的方法是一个不 错的注意。在上述例子中,stateClassName,abstractSubstateClassName和 allSubstateTransitions 是元模型类CompositeState的派生属性/派生引用,但是是以方法的形式表示。
这类派生属性或者派生引用可以作为常规的Ruby元模型类方法来实现。但是,因为RGen支持元模型类的多重继承,因此需要特别注意。用户定义的方 法必须只能添加到一个特殊的“类模块”中,这个事每一个RGen元模型的组成部分,并且可以通过常量constant ClassModule存取。
#表9:状态机元模型扩展例子 require 'rgen/name_helper' module StatemachineMetamodel include RGen::NameHelper # (1) module CompositeState::ClassModule # (2) def stateClassName container ? firstToUpper(name)+"State" : firstToUpper(name) # (3) end def abstractSubstateClassName "Abstract"+firstToUpper(name)+"Substate" end def realSubStates subStates.reject{|s| s.is_a?(HistoryState)} end def allSubstateTransitions realSubStates.outgoingTransitions + # (4) realSubStates.allSubstateTransitions end end end
表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)。
#表10:启动生成器 outdir = File.dirname(__FILE__)+"/../src" templatedir = File.dirname(__FILE__)+"/templates" tc = RGen::TemplateLanguage::DirectoryTemplateContainer.new( # (1) StatemachineMetamodel, outdir) tc.load(templatedir) # (2) stateMachine = envSM.find(:class => StatemachineMetamodel::Statemachine) # (3) tc.expand('Root::Root', :foreach => stateMachine) # (4)
表11的是最终生成器输出的一部分:文件“AbstractOperatingSubstate.h”是由表8的模板生成。注意现在不需要任何的后续处理。
// Listing 11: Example C++ output file "AbstractOperatingSubstate.h" #ifndef ABSTRACTOPERATINGSUBSTATE_H_ #define ABSTRACTOPERATINGSUBSTATE_H_ class OperatingState; class AbstractOperatingSubstate { public: AbstractOperatingSubstate(OperatingState &context, char* name); virtual ~AbstractOperatingSubstate() {}; OperatingState &getContext() { return fContext; } char *getName() { return fName; }; virtual void entryAction() {}; virtual void exitAction() {}; virtual void powerBut() {}; virtual void modeBut() {}; private: char* fName; OperatingState &fContext; }; #endif /* ABSTRACTOPERATINGSUBSTATE_H_ */
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对象。
感谢刘申 对本文的审校。
给InfoQ中文站投稿或者参与内容翻译工作,请邮件至[email protected] 。也欢迎大家加入到InfoQ中文站用户讨论组 中与我们的编辑和其他读者朋友交流。