两个元模型与一种映射
组件模型描述端口与接线。普通图模型只需要节点和有向边。模型到模型转换(model-to-model transformation)在这两门语言之间翻译:源模型符合组件元模型,目标模型符合另一份图元模型。转换要决定保留什么、省略什么,以及怎样重新建立引用。
这里,每个 Architecture 转成 Graph,每个 Component 转成 Node,每个 Connector 转成 Edge。最简单的版本会丢掉端口名称。这是有意的:图算法可以在组件层面工作,不必知道组件语言中的端口细节。ETL(Epsilon Transformation Language)用规则定义这种映射。规则体沿用第 4 模块的 EOL 表达式。
编写 ETL 规则
第一个程序有三条规则。Architecture2Graph 创建根元素并填充其包含的节点与边;Component2Node 复制组件名称;Connector2Edge 把边接到源端口和目标端口所属组件对应的图节点上。
rule Architecture2Graph
transform a : Source!Architecture
to g : Target!Graph {
g.name = a.name;
g.nodes ::= a.components;
g.edges ::= a.connectors;
}
rule Component2Node
transform c : Source!Component
to n : Target!Node {
n.name = c.name;
}
rule Connector2Edge
transform c : Source!Connector
to e : Target!Edge {
e.source ::= c.source.eContainer();
e.target ::= c.target.eContainer();
}
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
attr String name;
val Node[*] nodes;
val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
attr String label;
ref Node source;
ref Node target;
}
其他文件: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Graph name="Alarm"
Node name="OrGate"
Node name="AndGate"
Node name="Siren"
Edge source->OrGate target->AndGate
Edge source->AndGate target->Siren
阅读实际输出中的目标树:它有名为 Alarm 的图、三个节点与两条边。ETL 程序下方可见目标元模型。Source!Component 与 Target!Node 分别指定不同模型中的类型,避免把源组件误当成目标节点。g.nodes ::= a.components 使用 ETL 的特殊赋值:填入每个源组件经转换后的对应元素,而非源组件本身。
转换不只是导出文本。它创建另一种语言的模型,可以继续用于其他模型管理任务。本地运行器打印目标模型的包含树供你检查;本课程不把目标持久化为 XMI 文件。
解析转换后的对应元素
ETL 保留一份转换轨迹(transformation trace),记录源元素及其产出的目标元素。equivalent() 取得某源元素经转换后的对应元素。下一个程序与第一个做相同映射,但在填充目标图和连接边的端点时,显式调用 equivalent()。
rule Architecture2Graph
transform a : Source!Architecture
to g : Target!Graph {
g.name = a.name;
for (c in a.components) { g.nodes.add(c.equivalent()); }
for (wire in a.connectors) { g.edges.add(wire.equivalent()); }
}
rule Component2Node
transform c : Source!Component
to n : Target!Node { n.name = c.name; }
rule Connector2Edge
transform c : Source!Connector
to e : Target!Edge {
e.source = c.source.eContainer().equivalent();
e.target = c.target.eContainer().equivalent();
}
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
attr String name;
val Node[*] nodes;
val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
attr String label;
ref Node source;
ref Node target;
}
其他文件: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Graph name="Alarm"
Node name="OrGate"
Node name="AndGate"
Node name="Siren"
Edge source->OrGate target->AndGate
Edge source->AndGate target->Siren
比较两次输出:目标图完全一样。::= 是目标特性接收对应目标元素时的简便写法。较复杂的表达式需要计算、筛选或组合对应元素时,显式 equivalent() 更合适。两种写法都不是按名称猜测对应关系;轨迹记录了哪条规则创建了什么结果。
守卫条件与惰性规则
守卫条件(guard)限制规则转换哪些源元素。下一个源模型的第二条连接把数字输出接到模拟输入。带守卫条件的连接器规则只在两端端口类型相同的情况下生成边。三个组件仍都生成节点,但只有兼容的连接生成边。
rule Architecture2Graph
transform a : Source!Architecture
to g : Target!Graph {
g.name = a.name;
g.nodes ::= a.components;
g.edges ::= a.connectors;
}
rule Component2Node
transform c : Source!Component
to n : Target!Node {
n.name = c.name;
}
rule Connector2Edge
transform c : Source!Connector
to e : Target!Edge {
guard: c.source.type = c.target.type
e.source ::= c.source.eContainer();
e.target ::= c.target.eContainer();
}
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
attr String name;
val Node[*] nodes;
val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
attr String label;
ref Node source;
ref Node target;
}
其他文件: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="ANALOG"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Graph name="Alarm"
Node name="OrGate"
Node name="AndGate"
Node name="Siren"
Edge source->OrGate target->AndGate
这是一项策略选择,不能代替验证。默默丢弃无效连接,对下游任务可能有危险。项目可以先用 EVL 拒绝模型;也可以明确地只转换有效连接,同时报告被省略的连接。这个示例单独展示守卫条件的效果。
惰性规则(lazy rule)只有在请求对应元素时才运行。下一个示例不把组件、而把端口映射成图节点。Architecture2Graph 为每个端口请求 Port2Node。由于 Port 是抽象类、实例实际属于 InPort 或 OutPort,规则需要 @greedy,以匹配子类而非仅匹配完全相同的类型。
rule Architecture2Graph
transform a : Source!Architecture
to g : Target!Graph {
g.name = a.name;
for (p in Port.all) { g.nodes.add(p.equivalent("Port2Node")); }
}
@lazy
@greedy
rule Port2Node
transform p : Source!Port
to n : Target!Node {
n.name = p.eContainer().name + "." + p.name;
}
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
attr String name;
val Node[*] nodes;
val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
attr String label;
ref Node source;
ref Node target;
}
其他文件: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Graph name="Alarm"
Node name="OrGate.door"
Node name="OrGate.window"
Node name="OrGate.open"
Node name="AndGate.open"
Node name="AndGate.armed"
Node name="AndGate.sound"
Node name="Siren.sound"
结果有七个节点,标签同时包含所属组件名与端口名。若没有 @greedy,在 Epsilon 2.8.0 中这条规则不适用于具体的端口子类,请求到的对应元素便会缺失。@lazy 避免在没有规则请求时创建端口节点。它适合有条件或代价较高的结果,不应被用来隐藏本应总是存在的结果。
主规则与转换轨迹
有时不止一条规则转换同一种源类型。下一个示例中,Component2Node 创建普通节点,Component2Audit 创建另一个审计节点。@primary 让普通节点在 equivalent() 选取单个对应元素时排在第一位。
rule Architecture2Graph
transform a : Source!Architecture
to g : Target!Graph {
g.name = a.name;
for (c in a.components) { g.nodes.add(c.equivalent()); }
}
@primary
rule Component2Node
transform c : Source!Component
to n : Target!Node { n.name = c.name; }
rule Component2Audit
transform c : Source!Component
to n : Target!Node { n.name = "audit:" + c.name; }
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
attr String name;
val Node[*] nodes;
val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
attr String label;
ref Node source;
ref Node target;
}
其他文件: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Graph name="Alarm"
Node name="OrGate"
Node name="AndGate"
Node name="Siren"
Node name="audit:OrGate"
Node name="audit:AndGate"
Node name="audit:Siren"
图包含三个普通节点。非惰性的审计规则也照常运行;因为没有图包含审计节点,本地目标打印器将它们显示为独立根元素。主规则不会禁止其他规则。它只控制单元素 equivalent() 查询时,哪个对应元素排在前面。如果目标模型只应有图中节点,就要删除审计规则,或有意为审计结果安排容器。ETL 参考文档详细说明规则调度、equivalent() 和 equivalents()。
检查目标模型
转换即便运行成功,也可能丢掉需要的信息。基本图省略端口名称和信号类型;带守卫条件的图省略一条不兼容的连接;主规则示例则有额外的、未被包含的审计节点。把目标元模型和实际输出的目标树放在一起看,才能知道转换后究竟有什么。
对于真实映射,应事先写明预期不变量:节点数、哪些连接器成为边、名称是否保持唯一,以及每条边的端点是否都属于目标图。如果映射依赖正确接线,应先验证源模型;完成转换后也要检查或验证目标模型。第 10 模块会把这套做法用于另一领域。
练习
映射类型。 列出分别变成 Graph、Node 和 Edge 的源类型。基本目标模型省略了哪些源信息?
查看解答
Architecture 变成 Graph,Component 变成 Node,Connector 变成 Edge。基本目标模型省略端口、端口名称和信号类型,只保留组件名称及组件级连接。
理解 ::=。 为什么 g.nodes ::= a.components 有意义,而把 a.components 直接赋给目标的 Node 包含引用是错误的?
查看解答
右侧是源模型中的 Component 元素。ETL 的 ::= 解析出它们转换后的 Node 对应元素,再填入 g.nodes。直接赋值会混用不兼容元模型中的元素。
理解主规则。 在主规则示例中,图的 nodes 只有普通节点,为什么实际输出仍打印了审计节点?
查看解答
Component2Audit 不是惰性规则,因此仍为每个组件运行。@primary 只让普通节点成为 equivalent() 首选的对应元素,不会禁用审计规则。审计节点没有容器,便作为独立目标根元素出现。
保留边标签。 为目标 Edge 的 label 属性赋值,格式为“源端口 to 目标端口”。检查结果中是否有两条带标签的边。
查看解答
本模块示例的目标元模型已经声明了 label。这条规则为它赋值,同时保留两端的对应关系:
rule Architecture2Graph
transform a : Source!Architecture
to g : Target!Graph {
g.name = a.name;
g.nodes ::= a.components;
g.edges ::= a.connectors;
}
rule Component2Node
transform c : Source!Component
to n : Target!Node {
n.name = c.name;
}
rule Connector2Edge
transform c : Source!Connector
to e : Target!Edge {
e.label = c.source.name + " to " + c.target.name;
e.source ::= c.source.eContainer();
e.target ::= c.target.eContainer();
}
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
attr String name;
val Node[*] nodes;
val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
attr String label;
ref Node source;
ref Node target;
}
其他文件: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Graph name="Alarm"
Node name="OrGate"
Node name="AndGate"
Node name="Siren"
Edge label="open to open" source->OrGate target->AndGate
Edge label="sound to sound" source->AndGate target->Siren
测试带守卫条件的模型。 在类型正确的报警器上运行带守卫条件的转换。结果应该有几条边?为什么?
查看解答
两个连接器都通过类型守卫条件,因此都生成边:
rule Architecture2Graph
transform a : Source!Architecture
to g : Target!Graph {
g.name = a.name;
g.nodes ::= a.components;
g.edges ::= a.connectors;
}
rule Component2Node
transform c : Source!Component
to n : Target!Node {
n.name = c.name;
}
rule Connector2Edge
transform c : Source!Connector
to e : Target!Edge {
guard: c.source.type = c.target.type
e.source ::= c.source.eContainer();
e.target ::= c.target.eContainer();
}
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
attr String name;
val Node[*] nodes;
val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
attr String label;
ref Node source;
ref Node target;
}
其他文件: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Graph name="Alarm"
Node name="OrGate"
Node name="AndGate"
Node name="Siren"
Edge source->OrGate target->AndGate
Edge source->AndGate target->Siren
只请求输出端口。 修改惰性规则示例,让图只接收 OutPort 实例对应的节点,标签使用完整的 组件.端口 名称。还需要 @greedy 吗?
查看解答
现在规则只匹配具体的 OutPort 类型,因此不需要 @greedy。图包含两个输出端口:
rule Architecture2Graph
transform a : Source!Architecture
to g : Target!Graph {
g.name = a.name;
for (p in OutPort.all) { g.nodes.add(p.equivalent("Port2Node")); }
}
@lazy
rule Port2Node
transform p : Source!OutPort
to n : Target!Node {
n.name = p.eContainer().name + "." + p.name;
}
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
attr String name;
val Node[*] nodes;
val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
attr String label;
ref Node source;
ref Node target;
}
其他文件: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Graph name="Alarm"
Node name="OrGate.open"
Node name="AndGate.sound"
自测
Source!Component 指什么?::= 做什么?Connector2Edge 的守卫条件控制什么?Port2Node 规则为什么使用 @greedy?@primary 保证什么?