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.
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.
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.
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.)
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 Portdeclares a kind that cannot be created directly, andclass 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.
@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;
}
<?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>
// Print every component with the names of its ports
for (c in Component.all) {
(c.name + ": " + c.ports.collect(p | p.name).concat(", ")).println();
}
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.
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.
<?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
// Print every component with the names of its ports
for (c in Component.all) {
(c.name + ": " + c.ports.collect(p | p.name).concat(", ")).println();
}
@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;
}
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.
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.)
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.
// 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"
}
}
<?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
@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;
}
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.
<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
<?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>
@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;
}
<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.
// 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();
}
@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
<?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>
@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;
}
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.
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.
Keep the lesson open in one browser tab and the Playground in another, and switch between them as you read.
Exercises
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.
- An architect’s floor plan for a house that has not been built yet.
- A class diagram reverse-engineered from an existing code base to help new developers find their way.
- A Simulink model from which a car’s cruise-control code is generated.
- A whiteboard drawing a team makes while discussing how a new feature should work, photographed and then wiped.
Show solution
- 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.
- Descriptive. It records a system that already exists. The sketch–blueprint–program scale applies to prescriptive models, so it does not place this one.
- Prescriptive, program. It is complete and precise enough for a tool to generate the code from it.
- 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.
Spot the misused terms. Which statements use MDE vocabulary correctly? Rewrite the others.
- “Our model implements the UML metamodel.”
- “
alarm.flexmiconforms tocomponents.emf.” - “
Portis a model element of the alarm model.” - “The metamodel defines the abstract syntax of the components language.”
Show solution
- Misused. Models conform to metamodels; “implements” describes a class and an interface in a programming language. Better: “Our model conforms to the UML metamodel.”
- Correct.
- Misused.
Portis a type (an abstract class) in the metamodel. The alarm model’s elements are instances, such as the componentOrGateor its input portdoor. Better: “dooris a model element of the alarm model, and its type isInPort.” - Correct.
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:
// 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
<?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>
@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;
}
Sequence {"Siren"}
Only Siren qualifies: it receives a signal but produces none. Module 4 covers select, exists and collect in depth.
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:
<?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
// Print every component with the names of its ports
for (c in Component.all) {
(c.name + ": " + c.ports.collect(p | p.name).concat(", ")).println();
}
@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;
}
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.
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:
@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;
}
<?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>
// 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"
}
}
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.
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
<?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
// Print every component with the names of its ports
for (c in Component.all) {
(c.name + ": " + c.ports.collect(p | p.name).concat(", ")).println();
}
@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;
}
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.
Self-check quiz
alarm.flexmi and components.emf correctly?Alarm.Siren.sond. What happens when the build runs it on Epsilon 2.8.0?Further reading
- Dimitris Kolovos, Model-Driven Engineering Terminology (2022): the source of this module’s vocabulary, with more on how the terms are commonly misused.
- Dimitris Kolovos, Metamodelling with ChatGPT (2022): the metamodel that inspired this series’ running example. Module 2 builds on it.
- Dimitris Kolovos, Minimal JHipster JDL Monolith Example (2021): a whole web application generated from a short textual model.
- Dimitris Kolovos, public lectures (video playlist).
- The Epsilon Playground documentation and the Epsilon documentation.
- Marco Brambilla, Jordi Cabot and Manuel Wimmer, Model-Driven Software Engineering in Practice, 2nd edition (Morgan & Claypool, 2017): a book-length introduction to the field.