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

生成代码与文本

用 EGL 模板和 EGX 规则从模型生成 Java、HTML 和图。

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

完成后你能够

  • 用 EGL 静态与动态片段生成文本。
  • 用 EGX 为每个模型元素生成文件。
  • 解释受保护区域如何影响重新生成。

开始之前

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

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

目录

学习计划

4 小时

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

时段 160 分钟
编写 EGL 模板
时段 260 分钟
用 EGX 生成多种格式
时段 360 分钟
受保护区域与产物审查
时段 460 分钟
练习
1

从模型到文本

一个模型可以成为多种输出的共同来源。报警器的组件名称和连接关系可以生成零件清单、HTML 报告、Graphviz 图描述或 Java 源代码。模型到文本转换(model-to-text transformation)从模型派生这些产物;模型变化后可以重新生成。但生成结果不会自动正确:生成器必须转义数据、使用合法标识符,并且只在模型满足必要规则时运行。

Epsilon 使用 EGL(Epsilon Generation Language)编写模板,用 EGX 协调模板执行与目标文件。两者都沿用第 4 模块的 EOL 导航。本模块示例下面的实际输出是生成的文本;对于 EGX,本地运行器还标出每个生成文件的名称。先从小型报警器入手,再尝试修改模型并重新运行模板。

2

编写 EGL 模板

EGL 模板混合文本和动态片段。[% ... %] 之外的文本原样复制。[% ... %] 执行 EOL 语句;[%= 表达式 %] 插入表达式的值。第一个示例循环访问每个组件,为它写一行端口数量。

示例纯文本零件清单在 Playground 中打开 ↗
generate.egl
Architecture: [%=Architecture.all.first().name%]
[% for (c in Component.all) { %]
- [%=c.name%]: ports=[%=c.ports.size()%]
[% } %]
其他文件: 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 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
Architecture: Alarm
- OrGate: ports=3
- AndGate: ports=3
- Siren: ports=1

模板有固定标题和动态列表。Architecture.all.first().name 提供架构名称。如果在 Flexmi 模型中加入组件,无需改动模板就会多出一行。生成的文件是派生产物:应以模型和模板为源,修改后重新生成。

核心思想

模板的静态文字决定输出的形状;动态 EOL 表达式提供从模型取得的值。两者共同组成可读的生成文档。

3

生成多种格式

EGL 不局限于一种目标语言。下面的模板把 HTML 标签作为静态文字,并把组件名称与数量插入表格单元格。

示例HTML 状态表在 Playground 中打开 ↗
generate.egl
<h2>[%=Architecture.all.first().name%]</h2>
<table>
<tr><th>Component</th><th>Inputs</th><th>Outputs</th></tr>
[% for (c in Component.all) { %]
<tr><td>[%=c.name%]</td><td>[%=c.ports.select(p | p.isTypeOf(InPort)).size()%]</td><td>[%=c.ports.select(p | p.isTypeOf(OutPort)).size()%]</td></tr>
[% } %]
</table>
其他文件: 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 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
<h2>Alarm</h2>
<table>
<tr><th>Component</th><th>Inputs</th><th>Outputs</th></tr>
<tr><td>OrGate</td><td>2</td><td>1</td></tr>
<tr><td>AndGate</td><td>2</td><td>1</td></tr>
<tr><td>Siren</td><td>1</td><td>0</td></tr>
</table>

Playground 可能渲染 HTML;下方的实际输出显示精确的标记文本。真实的生成器在把名称插入 HTML 前,应先做转义。教学模型只使用简单名称,因此这里聚焦 EGL 结构,而非完整的 HTML 转义库。

下一个模板生成 Graphviz DOT,一种图的文本描述。每个组件成为节点;每个连接器成为其端点端口所属组件之间的箭头。

示例Graphviz 连接图在 Playground 中打开 ↗
generate.egl
digraph Alarm {
[% for (c in Component.all) { %]
  "[%=c.name%]";
[% } %]
[% for (wire in Connector.all) { %]
  "[%=wire.source.eContainer().name%]" -> "[%=wire.target.eContainer().name%]";
[% } %]
}
其他文件: 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 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
digraph Alarm {
  "OrGate";
  "AndGate";
  "Siren";
  "OrGate" -> "AndGate";
  "AndGate" -> "Siren";
}

按本系列使用的模型驱动工程术语,DOT 在这里是另一种具体输出,并非新的领域模型。它省略了端口细节:即使两个报警器模型的端口名称不同,也可能生成相同的组件级图。要有意识地决定目标产物需要哪些信息。生成的图便于审查,而文本预览也能让边的具体数据清楚可见。

4

用 EGX 为每个元素生成文件

单个 EGL 模板自然地产生一段文本。EGX 为选定的模型元素运行模板,并决定每个结果保存到哪里。下面的规则对每个 Component 运行一次,把该组件作为 c 传给 template.egl,并用 c.name 命名输出文件。

示例每个组件生成一个 Java 文件在 Playground 中打开 ↗
generate.egx
rule Component2Java
  transform c : Component {
  template: "template.egl"
  target: c.name + ".java"
}
template.egl
public final class [%=c.name%] {
  public static final int INPUTS = [%=c.ports.select(p | p.isTypeOf(InPort)).size()%];
  public static final int OUTPUTS = [%=c.ports.select(p | p.isTypeOf(OutPort)).size()%];
}
其他文件: 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 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
--- AndGate.java ---
public final class AndGate {
  public static final int INPUTS = 2;
  public static final int OUTPUTS = 1;
}
--- OrGate.java ---
public final class OrGate {
  public static final int INPUTS = 2;
  public static final int OUTPUTS = 1;
}
--- Siren.java ---
public final class Siren {
  public static final int INPUTS = 1;
  public static final int OUTPUTS = 0;
}

实际输出有三个文件标题和三个 Java 类。template 指明 EGL 模板;target 计算文件名。模板用 EOL 的 select 统计输入和输出端口。用于生产环境时,应先验证每个组件名称都是安全且唯一的 Java 标识符,并转义插入源代码的文本。这里固定的示例名称符合这些要求。

EGX 规则还支持守卫条件、参数及其他工作流程控制。EGX 参考文档有详细说明。把“生成哪些文件”的决定放在 EGX,把“每个文件写什么”放在 EGL,各自更易修改。

5

受保护区域

重新生成通常会覆盖生成文件中的手工修改。EGL 可以标记受保护区域(protected region),让文件生成流程在已有目标文件中保留指定片段。示例用 out.preserve 生成名为 manual-note 的区域。

示例标记受保护区域在 Playground 中打开 ↗
generate.egl
public final class AlarmNotes {
[%=out.preserve("/*", "*/", "manual-note", true, "  // Add project-specific notes here")%]
}
其他文件: 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 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
public final class AlarmNotes {
/* protected region manual-note on begin */
  // Add project-specific notes here
/* protected region manual-note end */
}

实际输出展示了标记及初始内容,但没有展示与现有文件合并:本模块的本地运行器处理模板时,没有旧目标文件可供合并;Playground 也不是 Eclipse 的文件生成工作流程。要在本地项目中测试保留行为,先生成文件,只编辑标记区域,再启用合并并向同一目标重新生成。应明确区分生成内容和手写内容;受保护区域不能代替对模型或模板改动的审查。EGL 合并文档解释了标记与合并行为。

6

审查生成结果

使用生成文本前,应问清它依赖什么。Java 生成器假定名称可以作类标识符;HTML 生成器假定名称能安全插入标记;Graphviz 生成器忽略端口和信号类型。模板本身不验证连接器是否合理。如果输出依赖这些假设,应先运行第 5 模块的 EVL 规则。

实用的生成流程分三步:验证模型、以确定方式生成,然后检查或测试产物。这里的输入输出都足够小,可以逐项对照。真实项目还应把生成纳入构建,并在目标环境中测试代码或文档。第 9 模块会介绍怎样在 Playground 之外运行 Epsilon。

7

练习

练习 1★★★概念5 分钟

静态还是动态? 在 HTML 表格模板中,将 <tr><td>、[%=c.name%] 和 [% for (c in Component.all) { %] 分别归为静态文字、值插入或可执行模板代码。

查看解答

<tr><td> 是原样复制的静态文字。[%=c.name%] 插入当前组件的值。for 代码块是可执行 EOL 代码,控制后续文字输出多少次。

练习 2★★★概念5 分钟

丢失了什么? Graphviz 示例为每个组件创建节点、为每个连接器创建边。举出源模型中未被生成图记录的两个事实。

查看解答

图省略了具体端口名称及其 DIGITAL/ANALOG 类型,也没有指出每个连接器究竟连接哪些端口。生成器有意选择组件级视图。

练习 3★★★概念5 分钟

谁负责什么决定? Java 示例中,哪个文件决定输出文件名,哪个文件决定类的内容?使用这些类之前应检查什么?

查看解答

EGX 规则计算 c.name + ".java";template.egl 写类的内容。应验证类名合法且唯一,并检查生成代码所依赖的模型规则。在真实工作流程中还应编译或测试生成的 Java。

练习 4★★★编程10 分钟

简洁 HTML。 编写 EGL 模板,生成一个无序列表,每个组件占一项,并显示端口数量。

查看解答

循环包住静态的 <li> 行,表达式插入其中的取值:

参考解答生成简洁 HTML 列表在 Playground 中打开 ↗
generate.egl
<ul>
[% for (c in Component.all) { %]
<li>[%=c.name%] (ports: [%=c.ports.size()%])</li>
[% } %]
</ul>
其他文件: 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 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
<ul>
<li>OrGate (ports: 3)</li>
<li>AndGate (ports: 3)</li>
<li>Siren (ports: 1)</li>
</ul>
练习 5★★★编程10 分钟

每个组件一个摘要。 编写 EGX 规则,为每个组件调用一次 template.egl,写入 <名称>.txt。每个文件应报告该组件的端口数量。

查看解答

规则选择目标文件,模板生成内容。本地输出展示三个独立文件:

参考解答为每个组件生成文本文件在 Playground 中打开 ↗
generate.egx
rule Component2Summary
  transform c : Component {
  template: "template.egl"
  target: c.name + ".txt"
}
template.egl
[%=c.name%]: ports=[%=c.ports.size()%].
其他文件: 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 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
--- AndGate.txt ---
AndGate: ports=3.
--- OrGate.txt ---
OrGate: ports=3.
--- Siren.txt ---
Siren: ports=1.
练习 6★★★编程10 分钟

给边加标签。 修改 DOT 模板,使每个连接器箭头都标上其源端口与目标端口的名称。

查看解答

新增的 label 使用 wire.source.name 与 wire.target.name:

参考解答标注 Graphviz 边在 Playground 中打开 ↗
generate.egl
digraph Alarm {
[% for (wire in Connector.all) { %]
  "[%=wire.source.eContainer().name%]" -> "[%=wire.target.eContainer().name%]" [label="[%=wire.source.name%] to [%=wire.target.name%]"];
[% } %]
}
其他文件: 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 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
digraph Alarm {
  "OrGate" -> "AndGate" [label="open to open"];
  "AndGate" -> "Siren" [label="sound to sound"];
}
8

自测

1
EGL 标记之外的文字会怎样?
2
[%=c.name%] 做什么?
3
Java 示例中的 EGX 负责什么?
4
每个生成的 Java 类的内容在哪里定义?
5
DOT 示例省略了什么?
6
受保护区域示例证明编辑过的目标文件被保留了吗?
7
当输出依赖有效连接时,生成之前应做什么?
9

延伸阅读