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

查询模型

用 EOL 导航和查询模型:集合、一阶操作、类型上的操作以及修改模型。

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

完成后你能够

  • 沿属性和引用从模型元素导航。
  • 使用 select、collect、exists、forAll 和 sortBy。
  • 编写可复用的 EOL 操作并修改模型属性。

开始之前

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

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

目录

学习计划

4 小时

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

时段 160 分钟
探索并查询模型
时段 260 分钟
集合与可复用操作
时段 360 分钟
修改模型并设计查询
时段 460 分钟
练习
1

导航模型

前三个模块建立了一门建模语言,并用它编写了报警器模型。现在,我们可以提问,而不必逐一阅读 XML 元素。EOL(Epsilon Object Language)是 Epsilon 用来导航和修改模型的基础语言。EVL、EGL、EGX 和 ETL 都沿用它的表达式,因此这里学到的查询会贯穿后续模块。

Component.all 得到元模型中该类型的所有实例。对于每个组件,c.name 读取属性,c.ports 沿包含引用导航。点号表示“从当前值访问某个特性或操作”。下面的查询打印组件及各自拥有的端口数量。

示例导航组件与端口在 Playground 中打开 ↗
query.eol
for (c in Component.all) {
  (c.name + ": " + c.ports.size() + " port(s)").println();
}
其他文件: 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 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
OrGate: 3 port(s)
AndGate: 3 port(s)
Siren: 1 port(s)

程序依赖元模型,而不是把 OrGate 或 Siren 写死。换成第 3 模块的恒温器模型,同一程序也会以相同方式导航。Port.all 包括 InPort 和 OutPort 的实例,因为它们继承 Port。普通引用也能用于导航:connector.source.eContainer().name 先到达源端口,再到其所属组件。

核心思想

查询使用模型类型和特性来表达问题。模型改变但仍采用同一个元模型时,可以再次运行这个问题,而不用改写查询。

2

筛选与映射集合

Component.all 是一个集合。EOL 提供处理集合的操作。select(c | condition) 保留条件为真的元素;collect(c | expression) 为每个元素产生一个值。这里的 exists(p | p.isTypeOf(OutPort)) 判断某组件是否至少有一个输出端口。示例筛选出能产生输出的组件,按名称排序后打印。

示例筛选并映射输出在 Playground 中打开 ↗
query.eol
var producing = Component.all.select(c | c.ports.exists(p | p.isTypeOf(OutPort)));
for (c in producing.sortBy(c | c.name)) {
  c.name.println();
}
其他文件: 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
OrGate

从里向外阅读:c.ports 取得一个组件的端口;exists 对这些端口回答“有吗”;select 保留答案为真的组件;sortBy(c | c.name) 返回排序后的集合,并不会修改源模型中元素的名称或位置。Siren 只有输入端口,所以没有出现在输出中。

常见错误是本应筛选却使用 collect。collect(c | c.name) 把组件映射成名称,每个输入仍对应一个结果;select 保留原组件,之后仍能导航其端口。需要时可以组合两者:先选元素,再收集要报告的值。

3

量化判断与排序

模型查询常问“是否全部如此”或“是否至少有一个”,而不一定产出列表。forAll 在每个元素都满足条件时为真;exists 在至少一个满足条件时为真。下一个示例检查所有连接器的两端信号类型是否相同,以及是否有输入未被任何连接器指向。

示例对集合提问在 Playground 中打开 ↗
query.eol
("All connectors typed: " + Connector.all.forAll(c | c.source.type = c.target.type)).println();
("Any unconnected input: " + InPort.all.exists(p |
  not Connector.all.exists(c | c.target = p))).println();
其他文件: 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 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
All connectors typed: true
Any unconnected input: true

第一个答案为真:报警器中的每个连接器都连接数字端口。第二个答案也为真:door、window 和 armed 在架构内部没有入站连接。这里它们本来就来自外部,因此“未连接”是一项观察,不能自动判为错误。第 5 模块会增加所需的信息和规则,以决定哪些输入必须连接。

sortBy 根据竖线后的表达式排列集合。对名称而言,它使用字符串顺序。报告往往需要稳定的顺序,而模型的书写顺序不一定适合展示。还要注意排序依据:Port.all.sortBy(p | p.name) 可能包含来自不同组件的同名端口。若要唯一标签,还应加入所属组件的名称。

4

定义操作

多个查询重复同一计算时,可以把它命名为操作。EOL 中的 operation Component inputCount() : Integer 作用于 Component。操作体内的 self 是接收这次调用的组件。示例统计输入端口数,并按名称顺序逐个打印。

示例复用组件操作在 Playground 中打开 ↗
query.eol
for (c in Component.all.sortBy(c | c.name)) {
  (c.name + ": " + c.inputCount() + " input(s)").println();
}

operation Component inputCount() : Integer {
  return self.ports.select(p | p.isTypeOf(InPort)).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: 2 input(s)
OrGate: 2 input(s)
Siren: 1 input(s)

主程序语句写在操作定义之前。这在 EOL 中很重要:第一个操作定义之后的独立语句不会作为主程序执行。操作可以在另一个 EOL 查询中复用;使用同一模型时,也能用于 EVL 约束或 EGL 模板。简短的操作名称还可以比一遍遍重复嵌套的 select 表达式更清楚地说明领域含义。

操作不一定要附着在元模型类上。EOL 还可以定义不带上下文、像函数一样调用的操作,或定义作用于 String 等内置类型的操作。当问题自然地属于某个元素时,就使用上下文,例如“这个组件有多少输入?”EOL 文档详细说明了操作分派和这里使用的集合操作。

5

修改模型

EOL 不仅能读取模型,也能更新已载入的模型。示例找到 Siren,给它的 name 赋新值,然后打印改变后的组件名称集合。

示例在内存中修改模型在 Playground 中打开 ↗
query.eol
var siren = Component.all.selectOne(c | c.name = "Siren");
siren.name = "Bell";
("Renamed: " + siren.name).println();
("Components now: " + Component.all.collect(c | c.name).sortBy(n | n).concat(", ")).println();
其他文件: 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 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
Renamed: Bell
Components now: AndGate, Bell, OrGate

这次赋值只在本次运行的内存模型中生效。本系列的本地运行器不会保存 Flexmi 文件;关闭页面后,Playground 也不会保留你的修改。如果不改动示例文本而重新运行,原始模型又会载入 Siren。若需要持久修改,应编辑 Flexmi 源文件,或在本地项目中采用适合保存的模型格式。

修改名称还可能使按路径书写的引用失效。如果在 Flexmi 文本中把 Siren 改名,必须同步更新 Alarm.Siren.sound 这样的连接器路径。上面的查询修改的是已经载入的对象,不会改写源文件中的引用文本。要区分编辑源文本、修改已载入模型和保存模型资源。

6

设计实用查询

好的查询回答一个明确问题,并暴露其假设。“哪些组件没有输出?”可以直接检查端口;“哪些输入错误地未连接?”则需要更多领域信息:哪些输入来自外部,以及模型是否表示这些外部连接。不要把方便计算的数量直接当成架构有效性的证明。

写 EOL 表达式前,先确定起始类型、经过元模型的导航路径,以及答案的形式。需要子集时用 select,需要报告值时用 collect,需要布尔答案时用 exists 或 forAll,展示顺序重要时用 sortBy。重复出现且有明确领域含义的表达式可以定义成操作。这些选择会让后续模块的验证和生成规则更易读。

7

练习

练习 1★★★概念5 分钟

选择操作。 对每个问题选择 select、collect、exists 或 forAll:(a)所有组件的名称;(b)有输出端口的组件;(c)是否存在缺少目标的连接器;(d)是否每个组件都有名称。

查看解答

(a)用 collect 将组件映射为名称。(b)用 select 保留组件的子集。(c)用 exists,一个缺少目标的连接器就足够。(d)用 forAll,条件必须对每个组件成立。必要时再组合导航和排序。

练习 2★★★概念5 分钟

阅读查询。 解释 Component.all.select(c | c.ports.exists(p | p.isTypeOf(OutPort))) 在报警器模型中返回什么,以及 Siren 为何不在结果中。

查看解答

它返回至少有一个具体类型为 OutPort 的端口的 Component 元素:OrGate 和 AndGate。Siren 只有 InPort,所以内部的 exists 为假,select 将它排除。

练习 3★★★概念5 分钟

观察还是规则? 量化判断示例发现了未连接的输入。这能证明报警器无效吗?验证规则还需要知道什么?

查看解答

不能。door、window 和 armed 由当前架构外部提供。规则需要知道哪些输入是外部输入,或以其他方式定义哪些输入必须由内部连接器驱动。第 5 模块会增加这样的区分。

练习 4★★★编程9 分钟

寻找接收端。 打印没有输出端口的组件名称。从 Component.all 开始,组合 select 和 exists,再打印保留的名称。

查看解答

对 exists 取反,只保留不存在 OutPort 的组件:

参考解答列出没有输出的组件在 Playground 中打开 ↗
query.eol
for (c in Component.all.select(c |
    not c.ports.exists(p | p.isTypeOf(OutPort)))) {
  c.name.println();
}
其他文件: 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 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
Siren
练习 5★★★编程10 分钟

统计出站连接。 按字母顺序列出每个组件,并统计源端口属于该组件的连接器个数。没有出站连接的组件应打印零。

查看解答

对于每个组件,按源端口的容器筛选 Connector.all,然后取得集合大小:

参考解答统计每个组件的连接数在 Playground 中打开 ↗
query.eol
for (c in Component.all.sortBy(c | c.name)) {
  (c.name + ": " + Connector.all.select(k | k.source.eContainer() = c).size()).println();
}
其他文件: 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: 1
OrGate: 1
Siren: 0
练习 6★★★编程11 分钟

命名可复用标签。 定义作用于 Port 的操作,返回 组件名.端口名,然后按端口名称顺序打印所有标签。报警器中有两个同名的 open 端口;标签怎样区分它们?

查看解答

操作使用 self.eContainer().name 和 self.name。两个标签分别是 OrGate.open 和 AndGate.open:

参考解答用操作标注端口在 Playground 中打开 ↗
query.eol
for (p in Port.all.sortBy(p | p.name)) {
  p.label().println();
}

operation Port label() : String {
  return self.eContainer().name + "." + self.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 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
AndGate.armed
OrGate.door
OrGate.open
AndGate.open
AndGate.sound
Siren.sound
OrGate.window
8

自测

1
Component.all 返回什么?
2
哪个操作保留满足条件的组件?
3
哪个操作为每个输入元素产生一个报告值?
4
Connector.all.forAll(c | c.source.type = c.target.type) 返回真说明什么?
5
带上下文的 EOL 操作中的 self 指什么?
6
在本系列的运行器中,修改示例把 Siren 改名后,Flexmi 文件会怎样?
7
报告为什么可能使用 sortBy(c | c.name)?
9

延伸阅读