Ran Wei/MDE Series
中文
Model-Driven Engineering with Eclipse Epsilon — Ran Wei

What is model-driven engineering?

Meet the core ideas of MDE (models, metamodels, conformance and model management) and run your first Epsilon programs in the browser.

3 hours3 sessions9 runnable examplesEpsilon Playground

By the end you can

  • Explain what a model is, and tell descriptive models from prescriptive ones.
  • Describe a metamodel as the abstract syntax of a modelling language, and say what it means for a model to conform to it.
  • Distinguish abstract from concrete syntax, and modelling languages from programming languages.
  • Name the four core model-management tasks and the Epsilon language for each.
  • Open, run and change an example in the Epsilon Playground.

Before you start

Some programming experience in any language, and a recent Chrome, Edge or Firefox. No modelling background and no installation are needed.

Download all examples of this module (zip)

Contents

Study plan

3 hours

Times are estimates and include running the examples. Progress is saved in this browser and shared by both language editions.

Session 160 min
Models and metamodels
Session 260 min
Languages and model management
Session 360 min
Practice
1

Why model-driven engineering?

Software projects rarely fail for lack of code. They get into trouble when the people building a system lose track of what it is supposed to be. The requirements live in one document, the design in another, and the code drifts away from both. Every change has to be made in several places by hand, and sooner or later the copies disagree.

Model-driven engineering (MDE) tackles this problem with two ideas. The first is abstraction: describe the system in a language made for the problem, and leave out the details that do not matter for the question at hand. The second is automation: make those descriptions precise enough for tools to process them. Tools can then check the descriptions, analyse them, and produce other artefacts from them, such as code, documentation or test data. The descriptions are called models. In MDE they are primary engineering artefacts, not pictures that sit beside the “real” work.

A small example shows the payoff. JHipster is a code generator for web applications. You describe your application in its textual language, JDL. The description says which entities exist (in the example in Kolovos’s post, which is based on one by Matt Raible, a blog with blogs, posts and tags), which attributes they have, how they relate to each other, and a few configuration choices such as the database to use. From a description that fits on one screen, JHipster generates a complete application that you can run: the database tables, the server-side code with its web API, and a browser-based user interface. Changing the model and generating again is far quicker than changing all of those layers by hand. And the layers stay consistent with each other, because they all come from one source. (Adapted from Dimitris Kolovos, Minimal JHipster JDL Monolith Example, 2021.)

The same idea appears wherever systems are large, long-lived or safety-critical. In the automotive and aerospace industries, engineers design control software as Simulink models and generate the code from them. Systems engineers describe whole systems in SysML, as the site’s SysML v2 and MBSE series show. And many teams define small domain-specific languages, like JDL, for a single job.

Key idea

In MDE, models are not documentation that drifts out of date. They are inputs that tools read, check and transform, so keeping them right pays off directly.

This module gives you the vocabulary and a first taste of the tools. Sections 2 to 4 explain what models, metamodels and modelling languages are. Section 5 introduces the four core tasks that tools perform on models, using Eclipse Epsilon. Section 6 shows how to work with the examples in the Epsilon Playground, in your browser.

2

Models

What exactly is a model? Kolovos’s definition is deliberately broad: a model is “a description of a system of interest”. The system can be anything you want to reason about: a building, a payroll application, the cruise control of a car, or the burglar alarm used throughout this module. The description can be a drawing, a structured text file, or a set of objects inside a tool.

Models are classified by the purpose they are built for:

  • A descriptive model describes a system that already exists. The floor plan of an existing building is descriptive. So is a class diagram that a team reverse-engineers from a legacy code base to find their way around it.
  • A prescriptive model describes a system that does not exist yet, and says how it should be. An architect’s plan for a house still to be built is prescriptive. So is the design of software you are about to write.

Prescriptive models vary a great deal in how complete and precise they are. Kolovos places them on a continuum:

  • Models as sketches capture just enough for an informal discussion, like a free-form drawing on a whiteboard.
  • Models as blueprints capture enough for engineers to build the system from them, but leave some decisions to people. A UML class diagram that names the classes and their relationships, but not what their operations do, is a blueprint.
  • Models as programs are detailed and exact enough for tools to generate the implementation, or to verify properties of the system, almost without human help. A Simulink model from which control code is generated is a program in this sense.

The further along this continuum a model is, the more a tool can do with it. Much of MDE is about moving models from blueprint towards program, so that the tedious and error-prone parts of implementation can be automated.

Every model is also partial. It keeps the aspects that matter for its purpose and leaves out the rest. That is not a weakness; it is the point of building a model.

Analogy

A city’s metro map and its street map describe the same place. The metro map distorts distances and leaves out every street, which makes it useless for walking and ideal for planning a train journey. Neither is the model of the city: each is good for its own purpose.

(Adapted from Dimitris Kolovos, Model-Driven Engineering Terminology, 2022.)

3

Metamodels and conformance

A model is written in some language, and a language needs rules. The rules say which kinds of element a model may contain, which properties they have, and how they may be connected. In MDE, these rules are captured in a metamodel. Kolovos defines a metamodel as “an object-oriented specification of the abstract syntax of a language” (Model-Driven Engineering Terminology, 2022). A language’s abstract syntax is its concepts and the relationships between them, independent of how models are drawn or written down. (Kolovos treats the two terms as interchangeable.) Metamodels are themselves written in metamodelling languages. The most widely used are Ecore, part of the Eclipse Modeling Framework (EMF), and MOF, a standard of the Object Management Group.

This series uses one language throughout: a small language for component-and-connector architectures. An architecture contains components. Each component has input and output ports. Connectors carry a signal from an output port to an input port. The running example is inspired by the component-and-port metamodel that Dimitris Kolovos built with ChatGPT’s help in Metamodelling with ChatGPT (2022). His version has components, connections and typed ports; ours separates input ports from output ports and lets the architecture own its parts. Module 2 builds ours up step by step.

The first example below shows the metamodel written in Emfatic, a textual notation for Ecore. You will learn to write Emfatic in Module 2. For now, read it like a set of class declarations:

  • class Component { … } declares a kind of model element. abstract class Port declares a kind that cannot be created directly, and class InPort extends Port {} declares a more specific kind of port.
  • attr String name; declares an attribute, which holds a plain value.
  • val Component[*] components; declares a containment reference: an architecture owns its components, and [*] means that it can own any number of them.
  • ref OutPort source; declares a plain reference: a connector points at a port that a component owns.

The model is written in Flexmi, a compact XML-based notation for EMF models. Each XML element creates an element of the model: <architecture> creates an Architecture, <component> a Component, and <inPort> an InPort. XML attributes such as name="OrGate" set the attributes of that element. These model elements are instances of the types that the metamodel defines. A model conforms to its metamodel when each of its elements is an instance of one of those types and uses only the features that its type declares.

The program is a short query in EOL, the base language of Epsilon. Read the three files, press Open in Playground and run the example, then compare what you see with the captured output below.

ExampleA first model, metamodel and queryOpen in Playground ↗
components.emf
@namespace(uri="components", prefix="components")
package components;

// An architecture is a set of components wired together by connectors
class Architecture {
  attr String name;
  val Component[*] components;
  val Connector[*] connectors;
}

class Component {
  attr String name;
  val Port[*] ports;
}

abstract class Port {
  attr String name;
}

class InPort extends Port {}

class OutPort extends Port {}

// A connector carries a signal from an output port to an input port
class Connector {
  ref OutPort source;
  ref InPort target;
}
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <!-- The siren sounds when (door OR window) AND armed -->
  <component name="OrGate">
    <inPort name="door"/>
    <inPort name="window"/>
    <outPort name="open"/>
  </component>
  <component name="AndGate">
    <inPort name="open"/>
    <inPort name="armed"/>
    <outPort name="sound"/>
  </component>
  <component name="Siren">
    <inPort name="sound"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
tour.eol
// Print every component with the names of its ports
for (c in Component.all) {
  (c.name + ": " + c.ports.collect(p | p.name).concat(", ")).println();
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
OrGate: door, window, open
AndGate: open, armed, sound
Siren: sound

The output has one line per component, listing the names of its ports in order. Component.all gives every element of type Component in the model, so the query never mentions the alarm itself. It would work on any model that conforms to the same metamodel; Exercise 6 asks you to try exactly that.

Look at how the connectors refer to ports. Alarm.OrGate.open is a fully qualified name: it starts at the root element, follows the names of the elements that contain the port, and ends at the port itself. Flexmi resolves references by name, so two ports can share the name open as long as their full paths differ.

Pitfall

Say that a model conforms to its metamodel, or that it is an instance of it. Kolovos notes that writers often say a model “implements”, “depends on” or “uses” its metamodel. Those verbs describe other relationships and confuse readers.

What happens when a model does not conform? The next example uses the same metamodel and the same program, but its model contains a typo in the last connector: the target is Alarm.Siren.sond instead of Alarm.Siren.sound.

ExampleA model that does not conformOpen in Playground ↗
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <!-- The siren sounds when (door OR window) AND armed -->
  <component name="OrGate">
    <inPort name="door"/>
    <inPort name="window"/>
    <outPort name="open"/>
  </component>
  <component name="AndGate">
    <inPort name="open"/>
    <inPort name="armed"/>
    <outPort name="sound"/>
  </component>
  <component name="Siren">
    <inPort name="sound"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sond"/>
</architecture>
More files: tour.eol, components.emf
tour.eol
// Print every component with the names of its ports
for (c in Component.all) {
  (c.name + ": " + c.ports.collect(p | p.name).concat(", ")).println();
}
components.emf
@namespace(uri="components", prefix="components")
package components;

// An architecture is a set of components wired together by connectors
class Architecture {
  attr String name;
  val Component[*] components;
  val Connector[*] connectors;
}

class Component {
  attr String name;
  val Port[*] ports;
}

abstract class Port {
  attr String name;
}

class InPort extends Port {}

class OutPort extends Port {}

// A connector carries a signal from an output port to an input port
class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
Model warning (line 18): Could not resolve target Alarm.Siren.sond for reference target (target)
OrGate: door, window, open
AndGate: open, armed, sound
Siren: sound

Read the captured output from the top. Flexmi could not find an element called Alarm.Siren.sond, so it reports a warning that names the line in the model and the reference it could not set. It then leaves that reference empty and carries on. The model loads, the program runs, and it prints exactly the same three lines as before. The connection to the siren is broken, but nothing in the query’s own output reveals it.

That is worth remembering: a program can run on a model that does not conform and still produce plausible-looking results, so treat every warning as a defect to fix. Flexmi is forgiving by design, which makes models quick to write. Module 3 shows where that helps and where it hides mistakes.

If you run this example in the Playground, you will not see the warning at all: the Playground does not report references that Flexmi could not resolve, so its console shows only the three lines. Press the diagram button on the model panel instead. In the diagram, the first connector has two arrows, to the two ports it joins, but the second has only one, to the AND gate’s sound port, because its target was never set.

4

Abstract and concrete syntax

The metamodel says what an alarm model contains: components, ports, connectors and their names. It says nothing about how a model looks. That is the job of a concrete syntax, a notation for writing or showing the models of a language. In Kolovos’s account, a language has one abstract syntax, its metamodel, and any number of concrete syntaxes: textual, diagrammatic, tabular, tree-based, or combinations of these.

The alarm already appears in several concrete forms in this module:

  • as the Flexmi text you have just read;
  • as a diagram: open the first example in the Playground and press the diagram button on the model panel, and the Playground draws the components, ports and connectors as boxes and lines;
  • as a table: in Section 5, a template turns the model into an HTML parts list.

All three show the same model elements. Changing the notation changes nothing about what the model says.

This is why Kolovos argues that, strictly speaking, languages are not textual or graphical: only their concrete syntaxes are. Calling a language textual or graphical is a useful shorthand: it means that its default or dominant concrete syntax is textual or graphical. SysML v2 makes the point well: one abstract syntax has both a textual and a graphical notation (see the site’s SysML v2 series).

What, then, separates a modelling language from a programming language? Most languages we would call programming languages, such as Java, are textual and Turing-complete, while most modelling languages, such as UML and SysML, are graphical and not Turing-complete. But the pattern has exceptions: BPEL is graphical and Turing-complete, and SQL-92 is textual and not. And if Turing-completeness were the test, “programming language” would simply become another name for “Turing-complete language”. Kolovos proposes a distinction based on intent instead. Call a language a modelling language if people mostly use it without intending to get something executable out of their work: to understand a domain, explore a design or explain it to others. Call it a programming language if people mostly use it intending to produce something that runs. Under this view, the same language can serve either purpose. You can write a few Java classes just to understand a domain, and you can generate executable code from a UML class diagram.

(Adapted from Dimitris Kolovos, Model-Driven Engineering Terminology, 2022.)

5

Model management with Epsilon

Once models are precise enough for tools to read, you can automate the work around them. MDE calls this model management. Four tasks are at its core:

  • Querying: extracting information from a model, such as which components have no outputs.
  • Validation: checking rules that a well-formed model must follow but that the metamodel cannot express on its own.
  • Model-to-text transformation: generating text, such as code, documentation or reports, from a model.
  • Model-to-model transformation: producing a model in another language from a model.

Further tasks build on these: comparing two models, merging them, and migrating models when their metamodel changes. Module 9 points to Epsilon’s languages for them.

Eclipse Epsilon is a family of languages for model management. Each language is designed for one task, and all of them build on one base language, EOL (the Epsilon Object Language). Expressions you learn in one of them therefore work in all the others.

Task Epsilon language Taught in
Querying EOL Module 4
Validation EVL Module 5
Model-to-text transformation EGL, EGX Module 6
Model-to-model transformation ETL Module 7

You have already seen a query. The three examples below preview validation and the two kinds of transformation. Do not worry about every detail of their syntax yet: each later module teaches one language properly.

The first example is a validation program written in EVL. Its model is a variant of the alarm with a wiring mistake: the second connector feeds the AND gate’s output back into its own armed input.

ExampleValidation: check the wiringOpen in Playground ↗
rules.evl
// A connector must join two different components
context Connector {
  constraint NoSelfLoop {
    check: self.source.eContainer() <> self.target.eContainer()
    message: "A connector joins " + self.source.eContainer().name + " to itself"
  }
}

// Every input should be driven by something
context InPort {
  critique Connected {
    check: Connector.all.exists(c | c.target = self)
    message: self.eContainer().name + "." + self.name + " is not connected"
  }
}
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <!-- The siren sounds when (door OR window) AND armed -->
  <component name="OrGate">
    <inPort name="door"/>
    <inPort name="window"/>
    <outPort name="open"/>
  </component>
  <component name="AndGate">
    <inPort name="open"/>
    <inPort name="armed"/>
    <outPort name="sound"/>
  </component>
  <component name="Siren">
    <inPort name="sound"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.AndGate.armed"/>
</architecture>
More files: components.emf
components.emf
@namespace(uri="components", prefix="components")
package components;

// An architecture is a set of components wired together by connectors
class Architecture {
  attr String name;
  val Component[*] components;
  val Connector[*] connectors;
}

class Component {
  attr String name;
  val Port[*] ports;
}

abstract class Port {
  attr String name;
}

class InPort extends Port {}

class OutPort extends Port {}

// A connector carries a signal from an output port to an input port
class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
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

An EVL program groups its rules by the type they apply to: context Connector and context InPort. Each rule has a check, an expression that must be true for every element of that type, and a message to report for each element where it is false. When a constraint fails, EVL reports an error. When a critique fails, it reports a warning, for problems that deserve attention but do not make the model wrong.

The captured output shows one error and three warnings. The error is the self-loop. Siren.sound is not connected any more, because the faulty connector goes to the AND gate instead. OrGate.door and OrGate.window are flagged too, although they are meant to be fed by sensors outside the architecture: the language has no way to say so, and Exercise 5 asks you to fix that. In the Playground, the console stays empty for EVL programs: the error and the warnings appear as notes on a diagram of the model instead.

The next example generates text from the original, correctly wired alarm.

ExampleModel-to-text: a parts listOpen in Playground ↗
parts.egl
<h2>[%=Architecture.all.first().name%]: parts list</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>
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <!-- The siren sounds when (door OR window) AND armed -->
  <component name="OrGate">
    <inPort name="door"/>
    <inPort name="window"/>
    <outPort name="open"/>
  </component>
  <component name="AndGate">
    <inPort name="open"/>
    <inPort name="armed"/>
    <outPort name="sound"/>
  </component>
  <component name="Siren">
    <inPort name="sound"/>
  </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;

// An architecture is a set of components wired together by connectors
class Architecture {
  attr String name;
  val Component[*] components;
  val Connector[*] connectors;
}

class Component {
  attr String name;
  val Port[*] ports;
}

abstract class Port {
  attr String name;
}

class InPort extends Port {}

class OutPort extends Port {}

// A connector carries a signal from an output port to an input port
class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
<h2>Alarm: parts list</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>

An EGL template is text with dynamic sections. Everything outside [% %] is copied to the output unchanged. Code inside [% %] runs; here it is a loop over the components. And [%= %] inserts the value of an expression. The template therefore produces one table row per component, counting its input and output ports with the same kind of expression as the query in Section 3. The Playground renders the generated HTML as a table; the captured output below shows the HTML itself.

This is the idea behind JHipster on a small scale. The output is derived from the model, so when the model changes, you generate the output again instead of editing it by hand.

The last example transforms the alarm into a model of a different language: a plain directed graph, whose metamodel is shown under the program.

ExampleModel-to-model: from components to a graphOpen in Playground ↗
components2graph.etl
// Each architecture becomes a graph
rule Architecture2Graph
  transform a : Source!Architecture
  to g : Target!Graph {
  g.name = a.name;
  g.nodes ::= a.components;
  g.edges ::= a.connectors;
}

// Each component becomes a node
rule Component2Node
  transform c : Source!Component
  to n : Target!Node {
  n.name = c.name;
}

// Each connector becomes an edge between the components it joins
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;

// A plain directed graph
class Graph {
  attr String name;
  val Node[*] nodes;
  val Edge[*] edges;
}

class Node {
  attr String name;
}

class Edge {
  ref Node source;
  ref Node target;
}
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <!-- The siren sounds when (door OR window) AND armed -->
  <component name="OrGate">
    <inPort name="door"/>
    <inPort name="window"/>
    <outPort name="open"/>
  </component>
  <component name="AndGate">
    <inPort name="open"/>
    <inPort name="armed"/>
    <outPort name="sound"/>
  </component>
  <component name="Siren">
    <inPort name="sound"/>
  </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;

// An architecture is a set of components wired together by connectors
class Architecture {
  attr String name;
  val Component[*] components;
  val Connector[*] connectors;
}

class Component {
  attr String name;
  val Port[*] ports;
}

abstract class Port {
  attr String name;
}

class InPort extends Port {}

class OutPort extends Port {}

// A connector carries a signal from an output port to an input port
class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
Graph name="Alarm"
  Node name="OrGate"
  Node name="AndGate"
  Node name="Siren"
  Edge source->OrGate target->AndGate
  Edge source->AndGate target->Siren

ETL rules say how elements of the source model become elements of the target model. Source!Component and Target!Node name types in the two models. The rule Component2Node turns each component into a node with the same name, and Connector2Edge turns each connector into an edge between the nodes for the two components it joins. The operator ::= assigns the transformed counterpart of an element, made by whichever rule transforms it, so g.nodes ::= a.components fills the graph with the nodes made from the components.

The captured output prints the target model as a tree: one graph, three nodes and two edges. The ports have disappeared, because the graph language has no use for them. Model-to-model transformation lets each task work in the language that suits it: a graph algorithm needs nodes and edges, not ports.

6

Working in the Playground

Every example in this series opens in the Epsilon Playground, a web page for editing and running Epsilon programs with nothing to install. When you run a program, the Playground sends the program, model and metamodel to a server at the University of York, which runs Epsilon and sends back the result, so do not paste anything private into it. When you press Open in Playground, the example’s files load into panels:

  • the program panel holds the Epsilon program (EOL, EVL, EGL, EGX or ETL);
  • the model panel holds the Flexmi model and the metamodel panel the Emfatic metamodel, and both can show a diagram instead of text;
  • the console shows what the program prints, and any errors, but not warnings about the model itself, such as references that Flexmi could not resolve: check the model diagram for those;
  • depending on the language, further panels show generated text, rendered HTML, the validated model or the target model of a transformation.

To run the example, press the Run button on the program panel, or press Ctrl+S (Cmd+S on a Mac). You can edit any panel and run again as often as you like; trying a change and seeing what happens is the fastest way to learn. The Playground’s examples menu lists every example in this series, grouped by module, so you can move between examples without returning to the lesson.

Keep two things in mind:

  • Nothing you change is saved. Use the Download button to keep your work before you close the tab.
  • The output printed under each example on these pages was captured from Epsilon 2.8.0 when the page was built. The Playground may run a newer version of Epsilon and word some messages differently, and it leaves out warnings about the model. If a result surprises you, compare it with the captured output.
Tip

Keep the lesson open in one browser tab and the Playground in another, and switch between them as you read.

7

Exercises

Exercise 1★★★conceptual6 min

Descriptive or prescriptive? For each model, say whether it is descriptive or prescriptive. For each prescriptive model, also say whether it is a sketch, a blueprint or a program.

  1. An architect’s floor plan for a house that has not been built yet.
  2. A class diagram reverse-engineered from an existing code base to help new developers find their way.
  3. A Simulink model from which a car’s cruise-control code is generated.
  4. A whiteboard drawing a team makes while discussing how a new feature should work, photographed and then wiped.
Show solution
  1. Prescriptive, blueprint. It describes a house that does not exist yet, in enough detail for builders to work from, though they still settle many details themselves.
  2. Descriptive. It records a system that already exists. The sketch–blueprint–program scale applies to prescriptive models, so it does not place this one.
  3. Prescriptive, program. It is complete and precise enough for a tool to generate the code from it.
  4. Prescriptive, sketch. It is about a feature that does not exist yet, and it captures just enough for one informal discussion.

Whether a model is descriptive or prescriptive depends on the purpose it is built for, not on how it looks: a floor plan can be either, depending on whether the house already exists.

Exercise 2★★★conceptual5 min

Spot the misused terms. Which statements use MDE vocabulary correctly? Rewrite the others.

  1. “Our model implements the UML metamodel.”
  2. “alarm.flexmi conforms to components.emf.”
  3. “Port is a model element of the alarm model.”
  4. “The metamodel defines the abstract syntax of the components language.”
Show solution
  1. Misused. Models conform to metamodels; “implements” describes a class and an interface in a programming language. Better: “Our model conforms to the UML metamodel.”
  2. Correct.
  3. Misused. Port is a type (an abstract class) in the metamodel. The alarm model’s elements are instances, such as the component OrGate or its input port door. Better: “door is a model element of the alarm model, and its type is InPort.”
  4. Correct.
Exercise 3★★★coding8 min

Query: components with no outputs. Open the first example of this module in the Playground and change the program so that it prints the names of the components that have no output ports. Hint: combine select, exists and isTypeOf(OutPort).

Show solution

Select the components for which no port is an output port, then collect their names:

SolutionComponents with no outputsOpen in Playground ↗
no-outputs.eol
// Components that only receive signals: none of their ports is an output
var sinks = Component.all.select(c | not c.ports.exists(p | p.isTypeOf(OutPort)));
sinks.collect(c | c.name).println();
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <!-- The siren sounds when (door OR window) AND armed -->
  <component name="OrGate">
    <inPort name="door"/>
    <inPort name="window"/>
    <outPort name="open"/>
  </component>
  <component name="AndGate">
    <inPort name="open"/>
    <inPort name="armed"/>
    <outPort name="sound"/>
  </component>
  <component name="Siren">
    <inPort name="sound"/>
  </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;

// An architecture is a set of components wired together by connectors
class Architecture {
  attr String name;
  val Component[*] components;
  val Connector[*] connectors;
}

class Component {
  attr String name;
  val Port[*] ports;
}

abstract class Port {
  attr String name;
}

class InPort extends Port {}

class OutPort extends Port {}

// A connector carries a signal from an output port to an input port
class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
Sequence {"Siren"}

Only Siren qualifies: it receives a signal but produces none. Module 4 covers select, exists and collect in depth.

Exercise 4★★★coding4 min

Repair the model. Open the example “A model that does not conform” in the Playground and press the diagram button on the model panel. Find the connector that has only one arrow, fix the model so that both connectors have two, and run the program again. Compare its output with the original.

Show solution

The last connector names a port that does not exist: Alarm.Siren.sond should be Alarm.Siren.sound. With the name corrected, Flexmi resolves the reference. In the Playground’s diagram, the second connector now has an arrow to the siren’s sound port, and in the build the warning disappears from the captured output:

SolutionThe repaired modelOpen in Playground ↗
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <!-- The siren sounds when (door OR window) AND armed -->
  <component name="OrGate">
    <inPort name="door"/>
    <inPort name="window"/>
    <outPort name="open"/>
  </component>
  <component name="AndGate">
    <inPort name="open"/>
    <inPort name="armed"/>
    <outPort name="sound"/>
  </component>
  <component name="Siren">
    <inPort name="sound"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
More files: tour.eol, components.emf
tour.eol
// Print every component with the names of its ports
for (c in Component.all) {
  (c.name + ": " + c.ports.collect(p | p.name).concat(", ")).println();
}
components.emf
@namespace(uri="components", prefix="components")
package components;

// An architecture is a set of components wired together by connectors
class Architecture {
  attr String name;
  val Component[*] components;
  val Connector[*] connectors;
}

class Component {
  attr String name;
  val Port[*] ports;
}

abstract class Port {
  attr String name;
}

class InPort extends Port {}

class OutPort extends Port {}

// A connector carries a signal from an output port to an input port
class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
OrGate: door, window, open
AndGate: open, armed, sound
Siren: sound

The program’s own output does not change, because the program never looks at connectors. That is why conformance problems matter: a program can run on a broken model and still produce plausible-looking results.

Exercise 5★★★mini-project12 min

Telling external inputs apart. The Connected critique in “Validation: check the wiring” warns about door and window, but those inputs come from sensors outside the architecture, so they are not supposed to be connected. Change the metamodel so that a model can say which inputs are external, mark door, window and armed as external, and add a guard so that the critique skips external inputs. Use the correctly wired alarm from the first example as your model.

Show solution

Add a boolean attribute to InPort, set it in the model, and guard the critique with it:

SolutionMarking external inputsOpen in Playground ↗
components.emf
@namespace(uri="components", prefix="components")
package components;

// An architecture is a set of components wired together by connectors
class Architecture {
  attr String name;
  val Component[*] components;
  val Connector[*] connectors;
}

class Component {
  attr String name;
  val Port[*] ports;
}

abstract class Port {
  attr String name;
}

class InPort extends Port {
  // True for inputs that come from outside the architecture
  attr boolean external;
}

class OutPort extends Port {}

// A connector carries a signal from an output port to an input port
class Connector {
  ref OutPort source;
  ref InPort target;
}
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <!-- The siren sounds when (door OR window) AND armed -->
  <component name="OrGate">
    <inPort name="door" external="true"/>
    <inPort name="window" external="true"/>
    <outPort name="open"/>
  </component>
  <component name="AndGate">
    <inPort name="open"/>
    <inPort name="armed" external="true"/>
    <outPort name="sound"/>
  </component>
  <component name="Siren">
    <inPort name="sound"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
rules.evl
// A connector must join two different components
context Connector {
  constraint NoSelfLoop {
    check: self.source.eContainer() <> self.target.eContainer()
    message: "A connector joins " + self.source.eContainer().name + " to itself"
  }
}

// Every internal input should be driven by something
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"
  }
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
All constraints are satisfied.

The critique now applies only to internal inputs, and those are all connected, so every check passes. Notice the order of the changes. The rule needed information that the model could not express, so the metamodel changed first, and the model and the program followed. Much of MDE work goes in this direction.

Exercise 6★★★mini-project10 min

A second architecture. Using the same metamodel, write a Flexmi model of a thermostat: a Sensor with an output port reading, a Filter with an input raw and an output smoothed, and a Display with an input value, wired in that order. Run the first example’s query on your model.

Show solution
SolutionA second architecture: a thermostatOpen in Playground ↗
thermostat.flexmi
<?nsuri components?>
<architecture name="Thermostat">
  <!-- A sensor reading is smoothed and then shown on a display -->
  <component name="Sensor">
    <outPort name="reading"/>
  </component>
  <component name="Filter">
    <inPort name="raw"/>
    <outPort name="smoothed"/>
  </component>
  <component name="Display">
    <inPort name="value"/>
  </component>
  <connector source="Thermostat.Sensor.reading" target="Thermostat.Filter.raw"/>
  <connector source="Thermostat.Filter.smoothed" target="Thermostat.Display.value"/>
</architecture>
More files: tour.eol, components.emf
tour.eol
// Print every component with the names of its ports
for (c in Component.all) {
  (c.name + ": " + c.ports.collect(p | p.name).concat(", ")).println();
}
components.emf
@namespace(uri="components", prefix="components")
package components;

// An architecture is a set of components wired together by connectors
class Architecture {
  attr String name;
  val Component[*] components;
  val Connector[*] connectors;
}

class Component {
  attr String name;
  val Port[*] ports;
}

abstract class Port {
  attr String name;
}

class InPort extends Port {}

class OutPort extends Port {}

// A connector carries a signal from an output port to an input port
class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
Sensor: reading
Filter: raw, smoothed
Display: value

References use fully qualified names that start with the architecture’s name, such as Thermostat.Sensor.reading. The same query works unchanged on a new model because it was written against the metamodel, not against one particular model.

8

Self-check quiz

1
A metamodel…
2
Which phrase describes the relationship between alarm.flexmi and components.emf correctly?
3
A team writes a model of a legacy payroll system to understand it before replacing it. What kind of model is it?
4
In the terminology this module uses, what separates a modelling language from a programming language?
5
In EVL, what is the difference between a constraint and a critique?
6
You want to generate an HTML report from a model. Which Epsilon language fits?
7
In “A model that does not conform”, the model refers to Alarm.Siren.sond. What happens when the build runs it on Epsilon 2.8.0?
9

Further reading