Ran Wei/MDE 系列
English
基于 Eclipse Epsilon 的模型驱动工程 — Ran Wei

模型到模型转换

用 ETL 规则、守卫条件、equivalent 与惰性规则在元模型之间转换。

4 小时4 个时段8 个可运行示例Epsilon Playground

完成后你能够

  • 用 ETL 将组件映射为图。
  • 有意识地使用 equivalent、守卫条件、惰性规则与主规则。
  • 检查生成的目标模型及其中省略的信息。

开始之前

建议先学习第 1 至 6 模块。除特别说明外,示例均可在 Playground 中运行。

下载本模块全部示例(zip)

目录

学习计划

4 小时

时间为估计值,包含运行示例的时间。进度保存在当前浏览器,中英文版本共享。

时段 160 分钟
定义源与目标映射
时段 260 分钟
转换轨迹、守卫条件与惰性规则
时段 360 分钟
主规则与目标模型检查
时段 460 分钟
练习
1

两个元模型与一种映射

组件模型描述端口与接线。普通图模型只需要节点和有向边。模型到模型转换(model-to-model transformation)在这两门语言之间翻译:源模型符合组件元模型,目标模型符合另一份图元模型。转换要决定保留什么、省略什么,以及怎样重新建立引用。

这里,每个 Architecture 转成 Graph,每个 Component 转成 Node,每个 Connector 转成 Edge。最简单的版本会丢掉端口名称。这是有意的:图算法可以在组件层面工作,不必知道组件语言中的端口细节。ETL(Epsilon Transformation Language)用规则定义这种映射。规则体沿用第 4 模块的 EOL 表达式。

2

编写 ETL 规则

第一个程序有三条规则。Architecture2Graph 创建根元素并填充其包含的节点与边;Component2Node 复制组件名称;Connector2Edge 把边接到源端口和目标端口所属组件对应的图节点上。

示例组件转换为图在 Playground 中打开 ↗
transform.etl
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();
}
graph.emf
@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
alarm.flexmi
<?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>
components.emf
@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;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
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 文件。

3

解析转换后的对应元素

ETL 保留一份转换轨迹(transformation trace),记录源元素及其产出的目标元素。equivalent() 取得某源元素经转换后的对应元素。下一个程序与第一个做相同映射,但在填充目标图和连接边的端点时,显式调用 equivalent()。

示例显式解析对应元素在 Playground 中打开 ↗
transform.etl
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();
}
graph.emf
@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
alarm.flexmi
<?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>
components.emf
@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;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
Graph name="Alarm"
  Node name="OrGate"
  Node name="AndGate"
  Node name="Siren"
  Edge source->OrGate target->AndGate
  Edge source->AndGate target->Siren

比较两次输出:目标图完全一样。::= 是目标特性接收对应目标元素时的简便写法。较复杂的表达式需要计算、筛选或组合对应元素时,显式 equivalent() 更合适。两种写法都不是按名称猜测对应关系;轨迹记录了哪条规则创建了什么结果。

4

守卫条件与惰性规则

守卫条件(guard)限制规则转换哪些源元素。下一个源模型的第二条连接把数字输出接到模拟输入。带守卫条件的连接器规则只在两端端口类型相同的情况下生成边。三个组件仍都生成节点,但只有兼容的连接生成边。

示例跳过不兼容的连接在 Playground 中打开 ↗
transform.etl
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();
}
graph.emf
@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
alarm.flexmi
<?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>
components.emf
@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;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
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,以匹配子类而非仅匹配完全相同的类型。

示例按需创建端口节点在 Playground 中打开 ↗
transform.etl
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;
}
graph.emf
@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
alarm.flexmi
<?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>
components.emf
@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;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
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 避免在没有规则请求时创建端口节点。它适合有条件或代价较高的结果,不应被用来隐藏本应总是存在的结果。

5

主规则与转换轨迹

有时不止一条规则转换同一种源类型。下一个示例中,Component2Node 创建普通节点,Component2Audit 创建另一个审计节点。@primary 让普通节点在 equivalent() 选取单个对应元素时排在第一位。

示例选择主对应元素在 Playground 中打开 ↗
transform.etl
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; }
graph.emf
@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
alarm.flexmi
<?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>
components.emf
@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;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
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()。

6

检查目标模型

转换即便运行成功,也可能丢掉需要的信息。基本图省略端口名称和信号类型;带守卫条件的图省略一条不兼容的连接;主规则示例则有额外的、未被包含的审计节点。把目标元模型和实际输出的目标树放在一起看,才能知道转换后究竟有什么。

对于真实映射,应事先写明预期不变量:节点数、哪些连接器成为边、名称是否保持唯一,以及每条边的端点是否都属于目标图。如果映射依赖正确接线,应先验证源模型;完成转换后也要检查或验证目标模型。第 10 模块会把这套做法用于另一领域。

7

练习

练习 1★★★概念5 分钟

映射类型。 列出分别变成 Graph、Node 和 Edge 的源类型。基本目标模型省略了哪些源信息?

查看解答

Architecture 变成 Graph,Component 变成 Node,Connector 变成 Edge。基本目标模型省略端口、端口名称和信号类型,只保留组件名称及组件级连接。

练习 2★★★概念5 分钟

理解 ::=。 为什么 g.nodes ::= a.components 有意义,而把 a.components 直接赋给目标的 Node 包含引用是错误的?

查看解答

右侧是源模型中的 Component 元素。ETL 的 ::= 解析出它们转换后的 Node 对应元素,再填入 g.nodes。直接赋值会混用不兼容元模型中的元素。

练习 3★★★概念5 分钟

理解主规则。 在主规则示例中,图的 nodes 只有普通节点,为什么实际输出仍打印了审计节点?

查看解答

Component2Audit 不是惰性规则,因此仍为每个组件运行。@primary 只让普通节点成为 equivalent() 首选的对应元素,不会禁用审计规则。审计节点没有容器,便作为独立目标根元素出现。

练习 4★★★编程10 分钟

保留边标签。 为目标 Edge 的 label 属性赋值,格式为“源端口 to 目标端口”。检查结果中是否有两条带标签的边。

查看解答

本模块示例的目标元模型已经声明了 label。这条规则为它赋值,同时保留两端的对应关系:

参考解答为图的边加标签在 Playground 中打开 ↗
transform.etl
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();
}
graph.emf
@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
alarm.flexmi
<?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>
components.emf
@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;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
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
练习 5★★★编程10 分钟

测试带守卫条件的模型。 在类型正确的报警器上运行带守卫条件的转换。结果应该有几条边?为什么?

查看解答

两个连接器都通过类型守卫条件,因此都生成边:

参考解答只保留有效连接在 Playground 中打开 ↗
transform.etl
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();
}
graph.emf
@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
alarm.flexmi
<?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>
components.emf
@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;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
Graph name="Alarm"
  Node name="OrGate"
  Node name="AndGate"
  Node name="Siren"
  Edge source->OrGate target->AndGate
  Edge source->AndGate target->Siren
练习 6★★★编程10 分钟

只请求输出端口。 修改惰性规则示例,让图只接收 OutPort 实例对应的节点,标签使用完整的 组件.端口 名称。还需要 @greedy 吗?

查看解答

现在规则只匹配具体的 OutPort 类型,因此不需要 @greedy。图包含两个输出端口:

参考解答惰性生成输出端口节点在 Playground 中打开 ↗
transform.etl
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;
}
graph.emf
@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
alarm.flexmi
<?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>
components.emf
@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;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
Graph name="Alarm"
  Node name="OrGate.open"
  Node name="AndGate.sound"
8

自测

1
ETL 转换产出什么?
2
Source!Component 指什么?
3
这里的 ETL ::= 做什么?
4
Connector2Edge 的守卫条件控制什么?
5
惰性的 Port2Node 规则为什么使用 @greedy?
6
示例中的 @primary 保证什么?
7
转换后应该检查什么?
9

延伸阅读