本文介绍了RGen框架[1] ,该框架支持处理建模和代码生成的“ Ruby方法”。 我在MDA / MDD [2]的意义上使用了“建模”一词(但除此之外,它并没有非常严格地遵循这种方法): 模型是元 模型的实例,而元 模型的实例又与特定领域语言(DSL)相关)。 模型转换用于将模型转换为不同的元模型的实例, 代码生成是一种特殊的转换,它将模型转换为文本输出。
RGen受openArchitectureWare(oAW) [3]的强烈启发,它是一个具有非常相似的应用范围的Java框架。 RGen背后的关键思想是不仅将Ruby用于框架内应用程序逻辑的实现,而且还将其用于元模型的定义,模型转换和代码生成。 RGen通过为每个上述方面提供内部DSL来促进这一点。 如其他项目所示,Ruby非常适合这种方法。 一个流行的例子是Ruby on Rails [4] ,其中包括多个内部Ruby DSL。 与此相反,oAW使用外部DSL定义模型转换和代码生成。
经验表明,RGen方法极其轻巧且灵活,因此可以实现高效开发和简单部署。 我发现这在一个咨询项目中特别有用,该项目原来我们缺乏工具支持,但没有计划进行工具开发。 使用Ruby和RGen,我们以最小的努力启动了所需的工具,以使事情顺利进行,然后说服人们他们将从开发此工具中受益匪浅。
诸如RGen和oAW之类的框架的典型应用是代码生成器(例如,用于嵌入式设备)以及用于构建和操纵模型的工具,这些模型通常表示为XML或自己的文本或图形语言。 在本文中,我将以“ UML状态图到C ++代码生成器”为例。 在现实世界中,我们仍在上述项目中使用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( , ,...) ,这是一种动态构建中间超类的全局方法。
# Listing 1: Statemachine metamodel example
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分别定义与一个或多个目标类的单向包含关系。 contains_one和contains_many是双向对应项。 has_one和has_many用于常规的单向引用,而one_to_one , one_to_many和many_to_many是它们的双向副本。 这些方法在源类对象上调用,目标角色为第一个参数,在双向引用的情况下,其后为目标类和源角色(5)。 注意,必须先定义目标类才能引用它。 因此,在大多数RGen元模型中,引用是在类定义之外和之后定义的。
实际创建模型时,将实例化元模型Ruby类。 属性值和参考目标存储在各个对象的实例变量中。 由于Ruby不允许直接访问实例变量,因此使用了访问器方法。 在惯用的Ruby中,getter方法的命名方式与相应的变量类似,而setter方法的名称相同,后缀“ =”。 可以在作业的左侧使用此类设置方法。
上述类方法通过元编程动态地构建所需的访问器方法。 除了仅访问实例变量之外,这些方法还检查其参数的类型,并在发生违规时引发异常。 这样,运行时类型检查就建立在Ruby之上,而Ruby本身并不进行这种检查4 。 对于多个引用,getter返回一个数组,并且有setter方法来添加和删除引用的对象。 对于双向引用,访问器方法会自动在关系的另一侧添加和删除相反的引用。
清单2显示了一个示例:常规Ruby类实例化机制(1)可用于创建对象以及用于方便地设置属性和引用的特殊构造函数(4)。 请注意,必须使用包名称来限定类名称。 通过将包模块包含在当前名称空间中,可以避免重复(3)。 设置状态s1的名称属性,并检查getter方法的结果(2)。 将传出转换添加到s1 (一对多),并检查自动创建向后引用(一对一)(5)。 通过显式分配一个值(至1),将转换的目标状态设置为s2 ,并声明数组结果包含一个元素t1 (至1)(6)。 创建第二目标状态,并使用另一个转换将其连接到源状态。 最后,将s1的所有传出跃迁的目标状态声明为s2和s3 (7)。 注意方法targetState上调用outgoingTransitions这是一个阵列的结果。 这种简明的表示法是可能的,因为RGen通过将对未知方法的调用中继到所包含元素的能力并将输出收集到一个集合中,从而扩展了Ruby数组。
# Listing 2: Example instantiation of the statemachine metamodel
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元模型的版本。 可以在表示元模型元素的Ruby类或模块上使用ecore方法访问元模型RGen模型。
清单3显示了一个示例:在StatemachineMetamodel模块上调用代表包的ecore方法将生成 EPackage(1)的实例,在State类上对其进行调用将生成EClass的实例(2)。 请注意,EPackage和EClass都属于同一模型,实际上,名为“ State”的EClass是EPackage中名为“ StatemachineMetamodel”的分类器之一(3)。 可以像任何RGen模型一样导航元模型RGen模型。 示例代码断言,“状态”类的超类具有名为“名称”的属性(4)。
# Listing 3: Accessing the ECore metamodel
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类模型或来自UML类模型)的源或目标。 下一节将介绍模型转换。 最后,可以使用RGen元模型生成器将元模型模型转换回其RGen元模型DSL表示形式。 图3总结了不同的元模型表示及其关系。
图3:RGen元模型表示摘要
RGen元模型模型在类似于EMF的元模型上提供了反射功能。 在很多情况下,例如在实现自定义模型序列化程序或实例化程序时,对于程序员来说,准备好进行反射非常有用。 元元模型是ECore的事实确保了与许多现有建模框架的互换性:使用RGen元模型生成器,可以在RGen中直接使用任何ECore元模型。
清单4显示了将元模型模型序列化为XMI(1)以及使用元模型生成器重新生成RGen DSL表示形式(2)的过程。 请注意,在两种情况下,元模型都由StatemachineMetamodel的ecore方法返回的根EPackage元素引用 。 从文件中读取生成的元模型的DSL表示并进行评估(3)。 为了避免在原始类和模块与重载的类和模块之间发生名称冲突,评估是在另一个作为名称空间的Regenered模块的范围内进行的。 除了“ instanceClassName”属性的值(在重新加载的版本中包含其他名称空间)之外,这两个模型都是等效的(4)。
# Listing 4: Serializing the metamodel
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方法的一大优势是通过使用内部DSL以及与Ruby语言的紧密耦合而获得的灵活性。 例如,这允许以编程方式创建元模型类和模块,并调用类方法以创建属性和引用。 利用此功能的应用程序是一个通用的XML实例化程序,它根据遇到的XML标记和属性以及将它们映射到元模型元素的一组规则来动态创建目标元模型。 RGen发行版包括这种实例化器的原型版本。
内部DSL方法带来的另一个有趣的可能性是将元模型嵌入到常规代码中。 当代码需要处理复杂的,相互关联的数据结构时,这将非常有用。 开发人员可以根据(元模型)类,属性和引用来考虑结构,并在受影响的宿主类或模块的范围内对它们进行编程。 通过使用元模型方法,开发人员还决定在运行时5自动检查属性和引用类型。
使用多个元模型可以使许多现实世界的建模应用程序受益。 作为示例,应用程序可以具有内部元模型以及几个特定于输入/输出的元模型。 模型转换用于将一个元模型的实例转换为另一个元模型的实例,如图4所示。
图4:模型转换
在上面介绍的Statemachine示例的情况下,可以通过模型转换来添加UML 1.3 Statechart模型的输入。 RGen包含以RGen的元模型DSL表示的UML 1.3元模型的版本。 它还包括一个XMI实例化程序,该实例化程序允许直接从兼容的UML工具存储的XML文件中创建UML元模型的实例。 图5显示了从[6]中获取的示例输入statechart.。
图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':= trans(b1) , b2':=反(b2) )。 对于元模型类B的转换,在此示例中,不需要其他分配。
图6:转换规则的定义和应用
作为内部DSL,模型转换语言采用普通的Ruby作为具体语法。 每个模型转换都通过特殊的类方法在从RGen Transformer类派生的Ruby类中定义。 最重要的是, 转换类方法定义转换规则,将源和目标元模型类对象作为参数。 属性和引用的分配由Ruby Hash对象指定,该对象将属性和引用的名称映射到实际的目标对象。 哈希对象由与转换方法调用关联的代码块创建,该代码块在源模型元素的上下文中求值。
请注意,转换规则可以递归使用其他规则。 RGen转换程序机制通过缓存单个转换的结果来防止总体评估停止。 在执行任何递归使用的规则的代码块之前,规则的代码块的执行已完全完成。 当将自定义代码添加到代码块时,这种确定性行为尤其重要。
清单5显示了从UML 1.3元模型到上面介绍的示例状态图元模型的模型转换:从RGen Transformer类派生了一个新类UmlToStatemachine ,并且目标元模型模块包含在当前名称空间中,目的是使目标类名简短( 1)。 常规的Ruby实例方法(在此示例中为transform )充当转换入口点。 它调用反式变压器的方法,引发在输入模型中所有的statemachine元素的变换8 (2)。 trans方法查找使用transform类方法定义的变换规则,该变换规则从源对象的类开始,如果找不到规则,则沿继承层次结构向上移动。 UML13 :: StateMachine有一个转换规则,该规则指定将此类元素转换为Statemachine (3)的实例。 注意,源和目标元模型类都是常规的Ruby类对象,必须使用Ruby名称空间机制。 附加到此转换调用的代码块创建一个Hash对象,该对象将值分配给属性“名称”以及引用“ transitions”和“ topState”。 通过调用源模型元素上的访问器方法来计算这些值,该模型元素自动是代码块的上下文。 对于参考目标值,递归调用trans方法。
# Listing 5: Statemachine model transformation example
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对象的代码块中使用,因此可以进行非常强大的分配:示例目标元模型中的复合状态具有对初始状态的显式引用,而源元模型中的初始状态通过具有从“初始”伪状态的传入转换。 这种转换是使用Ruby的内置Array方法实现的,方法是首先找到具有此类传入转换的子状态,然后使用trans方法对其进行转换(4)。
代替目标类对象, transform方法可以选择采用一种方法来计算目标类对象。 在示例中,应根据方法stateClass (5)的结果将UML13 :: StateVertex转换为SimpleState或HistoryState 。 通过其他可选方法参数,可以将规则设置为条件规则。 在该示例中,该规则未用于初始伪状态,并且nil将是结果,因为没有其他规则适用。 除了常规的Ruby方法, Transformer类还提供了方法类方法,该方法类方法允许定义一种方法,其主体在当前转换源对象的上下文中进行评估(6)。 如有歧义,也可以使用@current_object实例变量访问当前转换源对象。
由于对transform类方法的调用是常规代码,因此也可以以更复杂的方式调用它,从而可以“编写”转换的定义。 一个很好的例子是清单6所示的Transformer类本身的复制类方法的实现:该方法采用源元模型类,也可以选择目标元模型类,并假设它们是相同的或具有相同的属性和引用。 然后,它使用代码块调用transform类方法,该代码块通过元模型反射9通过查找其属性和引用为每个给定的源对象自动创建正确的分配哈希。
# Listing 6: Implementation of the transformer copy command
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
复制类方法也可以应用于元模型的每个类。 这是为给定的元模型创建复制转换器的通用方法,可用于对该元模型的实例进行深层复制(克隆)。 清单7显示了用于UML 1.3元模型的示例复制变换器。
# Listing 7: Example copy transformer for the UML 1.3 metamodel
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”(关键字define )。 模板“ tplA1”创建一个输出文件“ out.txt”(关键字文件 ),并为其生成一行文本。 它将模板“ tplA2”和“ tplC1”的内容进一步扩展到相同的输出文件中(关键字expand )。 由于模板“ tplC1”驻留在其他文件中,因此其名称必须前面带有模板文件的相对路径。
图7:RGen生成器模板
扩展RGen模板后,将在上下文模型元素的上下文中评估其内容。 每个模板在定义时都使用:for属性与元模型类关联,并且只能针对该类型的上下文元素进行扩展。 默认情况下, expand命令在当前上下文中扩展模板,但是可以使用:for或:foreach属性指定不同的上下文元素。 在后一种情况下,需要一个数组,并针对该数组的每个元素扩展模板。 模板也可能因指定不同的上下文类型而超载,并且展开后会自动为给定的上下文元素选择正确的模板。
RGen模板机制建立在ERB(嵌入式Ruby)的基础上,该ERB提供了对Ruby的基本模板支持。 ERB是标准Ruby发行版的一部分,并允许使用标签<% , <%=和%>将Ruby代码嵌入任意文本中。 RGen模板语言由ERB语法以及作为常规Ruby方法实现的其他关键字组成。 这样,模板语言构成了RGen中的另一个内部DSL。 基于标准的ERB机制,该实现非常轻巧。
代码生成的一个主要问题是输出的格式设置:由于模板本身应为开发人员可读,因此添加了额外的空格,这以后会干扰输出的可读性。 一些方法通过漂亮的打印机提供输出来解决此问题。 但是,这需要花费更多时间,并且需要额外的工具,该工具对于某些类型的输出可能不可用。
RGen模板语言提供了一种无需设置任何其他工具即可格式化输出的简单方法:默认情况下,将删除行首和空白行中的空格。 然后,开发人员可以通过显式的RGen命令控制缩进和空行的创建: iinc和idec用于设置当前缩进级别, nl用于插入空白行。 经验表明,添加到格式化命令所需的工作是可以忍受的。 特别是当需要格式化特殊类型的输出时,此方法非常有用。
清单8显示了statechart示例中的完整模板。 它用于生成为每个复合状态创建的抽象C ++类的头文件。 根据状态模式和[6] ,将为每个子状态从该类派生一个状态类。
# Listing 8: Statemachine generator template example<% 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元模型类的一部分,并且可以通过常量ClassModule进行访问。
# Listing 9: Example statemachine metamodel extension
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包模块,并混合一个助手模块(1)。 在包模块中,打开了CompositeState类的类模块(2)。 方法的实现发生在使用常规访问器方法,其他派生属性和引用以及可能来自混合模块的方法的类模块中(3)。 allSubstateTransitions的方法实现是递归的,并且极大地受益于RGen功能,该功能允许在数组上调用元素方法(4)。
请注意,清单9中的方法未在与原始元模型类相同的文件中定义。 相反,Ruby的“开放类”功能用于从其他文件中贡献方法。 尽管可以将方法放在同一文件中,但通常建议使用一个或多个元模型扩展文件。 这样,元模型就不会被通常仅用于特定目的的辅助方法“抛弃”。
在示例中,扩展文件名为“ statemachine_metamodel_ext.rb”,扩展方法用于代码生成。 根据实际情况中的项目规模,对于常规扩展名使用“ statemachine_metamodel_ext.rb”,对于生成器特定扩展名使用“ statemachine_metamodel_gen_ext.rb”可能会有用。 通过将第二个文件与模板文件一起保存,可以将生成器逻辑完全分开。
为了开始生成代码,必须加载模板文件,并且需要扩展根模板。 清单10在statechart示例中显示了如何完成此操作:首先创建DirectoryTemplateContainer的实例(1)。 容器需要知道输出目录,即在其中创建输出文件(关键字文件 )的目录。它还需要知道用作模型名称空间的元模型,以引用模板中的元模型类。 接下来,通过指定模板目录(2)来加载模板。 在该示例中,用于代码生成的模型元素已通过前面的模型转换填充到RGen环境中。 从环境中检索根上下文元素(即,元模型类Statemachine的实例)(3),并通过扩展每个根元素的根模板来开始代码生成(4)。
# Listing 10: Starting the generator
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中的模板生成的,用于示例模型的“ Operating”状态。 请注意,无需额外的后处理即可获得此结果。
// 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添加了建模和代码生成支持,从而使开发人员能够以类似简单,类似于Ruby的方式处理模型和代码生成。
遵循简单性原则,RGen框架在大小,依赖项和开发人员反对的规则方面都是轻量级的。 这带来了极大的灵活性,允许将该框架用于日常脚本以及大型应用程序。
Ruby作为一种动态的解释语言,在开发中带来了一些固有的差异。 最突出的问题之一是缺少有关类型检查的编译器支持。 另一个是,编辑器支持(例如自动完成)通常仅在最低级别上可用。 这也适用于RGen,因为它完全依赖Ruby语言功能。 特别是,使用外部DSL的oAW之类的框架可以提供更好的编辑器支持。
像其他动态类型化语言(如Python和Smalltalk)一样,Ruby开发人员也很好地解决了这些缺点:缺少编译器检查通常可以通过更密集的(单元)测试来弥补,这还是一个好策略。 通过使用动态语言功能,可以部分弥补缺少编辑器支持的不足,这些动态语言功能使良好的编辑器支持一开始就很难构建。
但是,尤其是在较大的项目中,需要利用这些功能的强大功能。 这必须由程序员通过在需要时添加(运行时)检查来完成。 但是,语言本身支持此任务,因为它允许定义项目特定的检查DSL。
可以将RGen元模型定义语言视为这种DSL:它可以用于定义在运行时检查的属性和引用类型。 这意味着RGen元模型可以充当大型Ruby应用程序的框架。 元模型还支持开发人员的普遍理解,特别是在使用符合ECore或UML或什至是图形可视化工具的可视化时。 RGen添加的类型检查是确保程序的核心数据(模型)处于一致状态的第一步。
RGen于几年前开始,它是将建模领域和Ruby语言整合在一起的实验。 如引言中所述,在汽车行业的咨询项目中,它已成功用于原型工具。 到目前为止,该原型工具已经成熟,目前正用于汽车电子控制单元(ECU)的常规开发。
在此工具中,使用了几个元模型,其中最大的元模型包含约600个元模型类。 例如,我们的一个模型包含大约70000个元素,这些元素在大约一分钟内被加载,转换并最终变成大约90000行输出代码。 经验表明,在运行时和内存使用方面,基于RGen的方法可以轻松地与其他基于Java或C#的方法竞争。 另一个好处是,该工具可以部署为2.5MB可执行文件,其中包括Ruby解释器,并且可以在主机系统上运行而无需安装10 。
尽管RGen本身基于内部Ruby DSL,但尚不支持程序员创建新的内部Ruby DSL的具体语法。 它还不支持外部DSL的具体语法,例如生成解析器或语法。 当前,需要按照本文所示的方式以编程方式创建自定义元模型(抽象语法)的实例,或者通过现有实例或自定义实例化程序来创建它们。 这个主题肯定会受到将来的改进。
本文使用的示例应用程序的完整源代码可在RGen项目页面[1]上找到 。
基于Ruby的RGen框架为处理模型和元模型,定义模型转换以及生成文本输出提供了支持。 它使用内部DSL与Ruby语言紧密结合。 遵循Ruby设计原则,它轻巧灵活,并通过提供编写简洁,可维护的代码的方式来支持高效开发。
RGen已成功用于汽车行业项目中,是从简单的原型演变而来的建模和代码生成工具的基础。 尽管由于缺少编译器检查和编辑器支持而经常提到诸如Ruby之类的语言的缺点,但经验显示该方法的效率。 它还表明,尽管Ruby是一种解释型语言,但仍可以在运行时和内存使用方面实现良好的性能。
除了实际使用RGen外,该框架仍用于实验。 当前正在开发的一种扩展是对动态元模型的支持,可以在实例已经存在的情况下在运行时对其进行更改。
[1] RGen, http: //ruby-gen.org
[2]模型驱动架构, http://www.omg.org/mda
[3] openArchitectureWare, http://www.openarchitectureware.org
[4] Ruby on Rails, http://www.rubyonrails.org
[5] Eclipse建模框架, http://www.eclipse.org/modeling/emf
[6] Iftikhar Azim Niaz和Jiro Tanaka,“从Uml Statecharts生成代码”,筑波大学信息科学与电子研究所,2003年
[7] rubyscript2exe, http: //www.erikveen.dds.nl/rubyscript2exe
1 Ruby模块通常用作名称空间和分组方法,允许将混入类。
2这是在Ruby中实现DSL的几种方法之一,Ruby on Rails也使用这种方法。 在Rails应用程序中, ActiveRecord :: Base的子类用于以类似于RGen的方式定义元模型。 与ActiveRecord相比,RGen元模型不绑定到数据库,而是将ECore用作元模型。
3在Ruby中,类和数据类型之间没有区别,因为几乎所有内容都是对象。 诸如String,Integer或Float之类的类用作基本类型。
4 Ruby允许进行鸭子输入 ,这意味着不是对象的类型而是其方法使它成为特定用途的对或错对象。
5在像Ruby这样的动态类型语言中,类型检查也很重要。 但是,与静态类型语言相反,开发人员可以决定哪些检查是必需的,哪些检查不是必需的。
6实际上,Ruby on Rails中的数据库迁移几乎具有相同的作用:每次迁移都可能修改数据库内容和架构。 一系列迁移步骤将数据库内容带到特定的架构(或元模型)版本
7为了简化示例,源模型和目标模型/元模型的结构相同,只是类和参考角色名称不同。 在典型的应用中,不一定是这种情况。
8创建变压器的实例时,构造函数将引用由RGen 环境对象表示的源模型和目标模型,这些对象基本上是各个模型的所有元素的数组。
9在具有所有属性和引用(ECore“功能”)的数组上调用注入数组方法。 它将每个元素与上次调用的块结果一起传递给块。 这样,将构建一个包含要素名称和值的数组,并最终将其传递给hash []类方法,该方法创建一个新的哈希对象。
10 rubyscript2exe [7]用于将程序,解释器和所需的库“编译”为Windows可执行文件,该文件可在启动时自动提取并运行其内容。
翻译自: https://www.infoq.com/articles/thiede-ruby-modelling/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1