为什么验证需要规则
元模型规定连接器的源端是输出端口、目标端是输入端口。但只靠这种结构,无法判断两端是否属于不同组件,或信号类型是否兼容。第 4 模块能够查询这些事实;本模块进一步说明一个有效架构必须满足什么。Epsilon 的 EVL(Epsilon Validation Language)在模型元素上运行规则并报告违反规则之处。
EVL 在 check、guard 和 message 中使用 EOL 表达式。context Connector 把规则应用到每个连接器。约束(constraint)的检查失败时报错误;较轻微的问题由建议性检查(critique)报警告。示例既有有效模型,也有故意出错的模型。示例下方的实际输出是本地运行器的文本报告。在 Playground 中,EVL 的控制台保持空白,结果以注释显示在经过验证的模型图上。
约束与建议性检查
这个报警器有一处接线错误:一个连接器从 AndGate.sound 接回 AndGate.armed。NoSelfLoop 约束比较源端口和目标端口所属的组件。Connected 建议性检查则问每个输入是否被某个连接器指向。
context Connector {
constraint NoSelfLoop {
check: self.source.eContainer() <> self.target.eContainer()
message: "A connector joins " + self.source.eContainer().name + " to itself"
}
}
context InPort {
critique Connected {
check: Connector.all.exists(c | c.target = self)
message: self.eContainer().name + "." + self.name + " is not connected"
}
}
其他文件: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" external="true" type="DIGITAL"/>
<inPort name="window" external="true" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" external="true" 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.AndGate.armed"/>
</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 {
attr boolean external;
}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Error [NoSelfLoop]: A connector joins AndGate to itself
Warning [Connected]: OrGate.door is not connected
Warning [Connected]: OrGate.window is not connected
Warning [Connected]: Siren.sound is not connected
错误指出自环;警告列出没有连接器的输入。OrGate.door 和 OrGate.window 本来就要从模型外部接收信号,因此相对于预期架构,这两条警告是误报。错误的自环还使 Siren.sound 失去连接。好的验证规则集需要领域区分,而不只是更多检查。
消息应当帮助人定位问题。self.source.eContainer().name 指出连接器源端口所属的组件;只说“连接器无效”会更难处理。违反规则确实使模型不适用于当前任务时,用约束;值得检查但不一定让模型无效时,用建议性检查。
守卫条件与消息
这些示例的元模型给 InPort 增加了布尔属性 external。守卫条件(guard)于是可以把规则限制在内部输入上。guard: not self.external 会让 Connected 跳过 door、window 和 armed,但仍检查内部的 open 与 sound。
context Connector {
constraint NoSelfLoop {
check: self.source.eContainer() <> self.target.eContainer()
message: "A connector joins " + self.source.eContainer().name + " to itself"
}
}
context InPort {
critique Connected {
guard: not self.external
check: Connector.all.exists(c | c.target = self)
message: self.eContainer().name + "." + self.name + " is not connected"
}
}
其他文件: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" external="true" type="DIGITAL"/>
<inPort name="window" external="true" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" external="true" 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 {
attr boolean external;
}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
All constraints are satisfied.
连线正确的报警器通过了全部检查。守卫条件回答什么时候检查;check 回答对于适用的元素,什么必须成立。守卫条件既可以放在单条规则上,也可以放在整个上下文上。守卫条件过宽会掩盖真实错误。练习 4 会比较有无守卫条件的版本。
另一条规则检查信号兼容性。下一个模型把 Siren.sound 设为模拟信号,而驱动它的输出是数字信号。两个端口都是元模型允许的实例,连接器的结构类型也正确;领域规则仍拒绝这一配对。
context Connector {
constraint CompatibleTypes {
check: self.source.type = self.target.type
message: self.source.eContainer().name + "." + self.source.name +
" and " + self.target.eContainer().name + "." + self.target.name +
" have different signal types"
}
}
其他文件: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" external="true" type="DIGITAL"/>
<inPort name="window" external="true" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" external="true" 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 {
attr boolean external;
}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Error [CompatibleTypes]: AndGate.sound and Siren.sound have different signal types
这说明验证不同于基本载入。模型可以被表示和查询,但如果下游生成器依赖连接的含义,就应先确认规则通过。
依赖与修复
有时一条规则只有在另一条通过后才有意义。这个草稿模型中,一个组件名称只有两个字母,另一个没有名称。HasName 检查前提;LongEnough 只有在 self.satisfies("HasName") 为真时才检查长度。前提规则标记为 @lazy,由依赖它的规则在需要时触发。
context Component {
@lazy
constraint HasName {
check: self.name.isDefined() and self.name <> ""
message: "A component has no name"
}
constraint LongEnough {
guard: self.satisfies("HasName")
check: self.name.length() >= 3
message: self.name + " is shorter than three characters"
}
}
其他文件: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Draft">
<component name="AB"/>
<component name=""/>
</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 {
attr boolean external;
}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Error [LongEnough]: AB is shorter than three characters
Error [HasName]: A component has no name
输出有一条关于短名称的消息和一条关于缺失名称的消息。它没有试图计算不存在的名称的长度。依赖让报告更集中,也能避免检查表达式自身出错。EVL 参考文档还介绍了多个前提条件可用的 satisfiesAll 与 satisfiesOne。
EVL 规则可以提供带标题和 EOL do 代码块的修复(fix)。下一条建议性检查发现小写的 siren,并提出一个具体的重命名方案。本地运行器只报告检查结果,不执行修复。在 Eclipse 的交互式验证流程中,用户可以选择所提供的修复;Playground 的验证图会显示问题,却不提供 Eclipse 中的交互式修复流程。
context Component {
critique Capitalised {
check: self.name.matches("[A-Z].*")
message: self.name + " should start with a capital letter"
fix {
title: "Rename siren to Siren"
do { self.name = "Siren"; }
}
}
}
其他文件: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Draft">
<component name="siren"/>
</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 {
attr boolean external;
}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Warning [Capitalised]: siren should start with a capital letter
修复是可执行代码,规则作者必须确保它确实解决问题。这里的修复刻意针对 siren 练习;拿它修复每个小写组件都不合适。不要以为只要声明了修复,验证过程就会自动修改模型。
运行并查看 EVL
对于每个示例,先阅读 Flexmi 模型,预测哪些元素应该失败。然后在 Playground 中运行,并切换到经过验证的模型图,把图中的注释与课程示例下的实际输出对照。这两个界面的表达方式不同:本地运行器打印 Error [规则名] 或 Warning [规则名];Playground 把结果标在受影响的模型元素上。
如果实际输出中出现模型载入警告,应先排查它,再解释 EVL 结果。未能解析的引用可能使 EVL 表达式因错误原因失败。本模块的验证示例中,引用都能解析;故意制造的是领域规则违反。只有不涉及连接器的命名草稿模型例外。
设计验证规则集
围绕项目必须作出的决定编写规则。对于这个架构,第一组实用规则可以要求连接器两端属于不同组件、信号类型相容,以及每个内部输入都有入站连接。给每条规则稳定的名称和可操作的消息。建议性规则用 critique。用守卫条件界定适用范围;一条检查依赖另一条时,用依赖关系。
EVL 全部通过,只能证明已编写的规则通过,不能证明所有可能性质。规则集可能并不完整。既要检查应该通过的模型,也要检查应该失败的模型,包括外部输入和空名称这样的边界情况。第 6 模块会从模型生成产物;如果生成结果依赖这些假设,应先运行验证。
练习
约束还是建议性检查? 给自环连接器和一个本应来自外部、却没有内部连接器的输入分类,并解释严重程度的选择。
查看解答
自环违反预期的接线规则,应作为约束错误。没有内部连接器的输入可能来自外部,因此在模型尚未标明是否为外部输入前,用警告或带守卫条件的建议性检查更合适。
守卫条件还是检查? 在 guard: not self.external 和 check: Connector.all.exists(c | c.target = self) 中,哪个表达式决定是否适用,哪个决定是否通过?
查看解答
守卫条件决定是否对此输入运行该规则。只有守卫条件通过,才由检查表达式决定该输入是否符合规则。
依赖。 长度规则为什么要依赖 HasName?依赖示例报告哪两个问题?
查看解答
缺失的名称没有可供检查的长度。依赖示例报告 AB 少于三个字符,以及另一个组件没有名称;它不会为无名称组件给出误导性的长度结果。
跳过外部输入。 从错误接线示例的 Connected 建议性检查出发,加一个守卫条件来跳过外部输入,并使用连线正确的报警器。EVL 报告什么?
查看解答
带守卫条件的检查只作用于内部输入,而它们在本模型中都已连接:
context Connector {
constraint NoSelfLoop {
check: self.source.eContainer() <> self.target.eContainer()
message: "A connector joins " + self.source.eContainer().name + " to itself"
}
}
context InPort {
critique Connected {
guard: not self.external
check: Connector.all.exists(c | c.target = self)
message: self.eContainer().name + "." + self.name + " is not connected"
}
}
其他文件: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" external="true" type="DIGITAL"/>
<inPort name="window" external="true" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" external="true" 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 {
attr boolean external;
}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
All constraints are satisfied.
修复信号类型。 在类型不匹配示例中,把 Siren.sound 改为 DIGITAL,重新运行 CompatibleTypes。
查看解答
每个连接器两端现在都是相同的信号类型,因此约束通过:
context Connector {
constraint CompatibleTypes {
check: self.source.type = self.target.type
message: self.source.eContainer().name + "." + self.source.name +
" and " + self.target.eContainer().name + "." + self.target.name +
" have different signal types"
}
}
其他文件: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" external="true" type="DIGITAL"/>
<inPort name="window" external="true" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" external="true" 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 {
attr boolean external;
}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
All constraints are satisfied.
检查修复。 在命名示例中,预测用户在 Eclipse 工作流程中选择修复后的名称。然后直接编辑 Flexmi 模型并重新运行规则。仅仅声明修复,会改变原模型吗?
查看解答
修复的 do 代码块将名称设为 Siren。声明它并不会在验证时自动执行;下面的参考模型明确应用了这一改动,因此检查通过:
context Component {
critique Capitalised {
check: self.name.matches("[A-Z].*")
message: self.name + " should start with a capital letter"
fix {
title: "Rename siren to Siren"
do { self.name = "Siren"; }
}
}
}
其他文件: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Draft">
<component name="Siren"/>
</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 {
attr boolean external;
}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
All constraints are satisfied.
自测
context Connector 选取什么?self.satisfies("HasName")?fix 会自动修改模型吗?