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

Metamodelling

Design the component language in Emfatic, from classes and multiplicities to containment, opposites, inheritance and enumerations.

4 hours4 sessions8 runnable examplesEpsilon Playground

By the end you can

  • Write Emfatic classes, attributes and references for a small domain.
  • Choose containment, opposites and multiplicities deliberately.
  • Use abstract classes, inheritance and enumerations to remove duplication and restrict values.
  • Explain what an Ecore metamodel can and cannot enforce on its own.
  • Revise a metamodel and update a conforming Flexmi model.

Before you start

Module 1, or a basic understanding of models, metamodels and conformance. Every example runs in the browser.

Download all examples of this module (zip)

Contents

Study plan

4 hours

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

Session 160 min
Classes and bounds
Session 260 min
Relationships
Session 360 min
Reusable types
Session 460 min
Practice
1

Start with domain questions

In Module 1, you used a model of a burglar alarm without having to design its language. This time you will design that language. The aim is to make the useful statements easy to express: an architecture owns components, components own ports, and connectors join output ports to input ports. The metamodel is the contract that gives those words precise meanings.

Before writing a class, ask what a learner needs to say about a system. For the alarm, we need to name the architecture and its components, list the ports of each component, and connect ports. We do not yet need a class for every kind of logic gate. The model can distinguish an OrGate from a Siren by name while the metamodel calls both Component. Add a type only when it gives the language a useful distinction or rule.

We will use Emfatic, a compact textual notation for the Ecore metamodelling language. The metamodel lives in components.emf; a matching model lives in alarm.flexmi. Run each example in the Playground and inspect both files. The examples are small revisions of one language, so compare each metamodel with the previous one. This stepwise approach is inspired by Dimitris Kolovos’s Metamodelling with ChatGPT (2022); the classes and programs here were written for this series.

Key idea

A metamodel describes the kinds of elements and links allowed in models. A model records particular elements and links, such as the alarm’s OrGate and Siren.

2

Classes, attributes and multiplicity

Start with two classes. Architecture is the root of the model and contains Component elements. Both classes have a name attribute. An attribute stores a value, such as a string or number; a reference points to another model element. Emfatic uses attr, ref and val to make that difference visible. The line beginning @namespace gives the package a URI; the Flexmi file selects it with <?nsuri components?>.

ExampleA first architecture languageOpen in Playground ↗
components.emf
@namespace(uri="components", prefix="components")
package components;

class Architecture {
  attr String[1] name;
  val Component[*] components;
}

class Component {
  attr String[1] name;
}
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate"/>
  <component name="Siren"/>
</architecture>
More files: inspect.eol
inspect.eol
for (c in Component.all) {
  c.name.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
Siren

Try changing Siren to Bell in the Flexmi panel. The query prints the new name, because Component.all selects elements by their metamodel type, not by a hard-coded list of names. Add another <component name="Battery"/> and run it again.

The brackets in the metamodel state multiplicity: how many values a feature may have. String[1] says exactly one name. Component[*] says zero or more components. When the brackets are omitted, Emfatic’s Ecore default is zero or one, not exactly one. Common forms are:

Emfatic Meaning Example
no brackets or [?] zero or one an optional description
[1] exactly one a required name
[*] zero or more an optional list of parts
[+] one or more a nonempty list of parts
[2..4] two to four a bounded group

The next metamodel changes the component feature from [*] to [+]. Compare the two files and run the query.

ExampleOne or more componentsOpen in Playground ↗
components.emf
@namespace(uri="components", prefix="components")
package components;

class Architecture {
  attr String[1] name;
  val Component[+] components;
}

class Component {
  attr String[1] name;
}
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate"/>
  <component name="Siren"/>
</architecture>
More files: inspect.eol
inspect.eol
("Components: " + Component.all.size()).println();
for (c in Component.all) { c.name.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
Components: 2
OrGate
Siren

The output counts the two components. It does not prove that the lower bound is enforced every time a Flexmi file is loaded. Ecore records structural bounds, but this Playground workflow does not automatically perform a complete validity check on every model. If you remove both components and the query still runs, the query reports zero; the model has violated the declared lower bound. In Module 5 you will write explicit EVL checks for rules that matter to your project. Likewise, [1] records the requirement for a name but should not be mistaken for an automatic test of every input file.

Pitfall

Do not infer conformance from a successful query. A tool may load and query a model even when it violates a declared bound. Check the model against the requirements you rely on.

3

Containment and ordinary references

val Component[*] components; is a containment reference. An architecture owns its components; deleting that architecture removes the contained elements from that model. A component can have one container, which gives the model a tree-shaped ownership structure. In Flexmi, nested XML elements express this ownership: <component> appears inside <architecture>.

A connector needs a different relationship. Its source is a port owned by a component, and its target is another port. The connector must point to those ports without owning or moving them. Emfatic uses ref for this ordinary, non-containment reference. The next example adds ports and connectors. Architecture contains components and connectors; each component contains ports; each connector refers to two ports.

ExampleOwning parts and linking portsOpen in Playground ↗
components.emf
@namespace(uri="components", prefix="components")
package components;

class Architecture {
  attr String[1] name;
  val Component[*] components;
  val Connector[*] connectors;
}

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

abstract class Port {
  attr String[1] name;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref OutPort source;
  ref InPort target;
}
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door"/>
    <outPort name="open"/>
  </component>
  <component name="Siren">
    <inPort name="sound"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.Siren.sound"/>
</architecture>
More files: inspect.eol
inspect.eol
for (c in Connector.all) {
  (c.source.name + " -> " + c.target.name).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
open -> sound

Follow the model’s nesting. OrGate contains the open output port, while Siren contains the sound input port. The connector sits under the architecture and names both ports with full paths such as Alarm.OrGate.open. If source were val OutPort source, the connector would claim ownership of a port that the component already owns. That is the wrong domain relationship.

ref OutPort source; and ref InPort target; also restrict the kinds of ports at each end. They do not, by themselves, say that a connector must join distinct components, that two ports have compatible signal types, or that every required input is connected. Those are examples of well-formedness rules for Module 5’s validation language.

A good way to choose between val and ref is to ask where the element lives. Does the architecture contain a component? Yes: val. Does a connector merely identify a port already inside a component? Yes: ref.

4

Opposite references

The first three metamodels let you navigate from an architecture to its components: a.components. Suppose a query starts at a component and needs its architecture. You could walk up the containment tree with EOL’s eContainer(), but a named relationship makes the domain easier to read. An opposite reference connects two declarations as inverse views of the same relationship.

In Emfatic, #architecture on components names its opposite, and #components on architecture points back. The former remains containment (val); the latter is an ordinary reference (ref). The model still writes each component only once, nested under its architecture. EMF maintains the inverse link.

ExampleNavigate an opposite referenceOpen in Playground ↗
components.emf
@namespace(uri="components", prefix="components")
package components;

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;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref OutPort source;
  ref InPort target;
}
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door"/>
    <outPort name="open"/>
  </component>
  <component name="Siren">
    <inPort name="sound"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.Siren.sound"/>
</architecture>
More files: inspect.eol
inspect.eol
for (c in Component.all) {
  (c.name + " belongs to " + c.architecture.name).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 belongs to Alarm
Siren belongs to Alarm

The program starts with each component and prints its architecture.name. There is no architecture="Alarm" attribute in the Flexmi model. The inverse is supplied by the metamodel relationship, not by duplicating the data in the model.

Opposites are useful when both directions are common questions. They also add a commitment: the two ends must stay consistent. In a larger language, do not add inverses to every link merely because you can; add them when navigation or an invariant needs them. This syntax follows the Emfatic reference documentation.

5

Inheritance and enumerations

The port declarations introduce inheritance. InPort and OutPort both extend Port, so both inherit its name. The abstract keyword prevents a bare Port from being created: a port in a model must be an input or an output. A query of Port.all still includes instances of both subclasses, because they are ports too.

Now we need to distinguish signal kinds. A free-form String would allow spellings such as digital, DIGITL or on/off with no shared vocabulary. An enumeration defines a finite set of named values. The example adds PortType with DIGITAL and ANALOG, and adds a type attribute to Port. Because InPort and OutPort inherit from Port, both receive that attribute.

ExampleAbstract ports and signal typesOpen in Playground ↗
components.emf
@namespace(uri="components", prefix="components")
package components;

enum PortType {
  DIGITAL;
  ANALOG;
}

class Architecture {
  attr String[1] name;
  val Component[*]#architecture components;
  val Connector[*] connectors;
}

class Component {
  attr String[1] name;
  ref Architecture#components architecture;
  val Port[*] ports;
}

abstract class Port {
  attr String[1] name;
  attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref OutPort source;
  ref InPort target;
}
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door" type="DIGITAL"/>
    <outPort name="open" type="DIGITAL"/>
  </component>
  <component name="Siren">
    <inPort name="sound" type="DIGITAL"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.Siren.sound"/>
</architecture>
More files: inspect.eol
inspect.eol
for (p in Port.all) {
  (p.name + ": " + p.type).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
door: DIGITAL
open: DIGITAL
sound: DIGITAL

The output lists each port’s type. The alarm uses digital signals. Open the thermostat solution later in this module to see analog readings and a digital command in the same metamodel.

An enum constrains the available values, but it does not decide which connected ports are compatible. A connector that runs from an analog output to a digital input can still be represented. Whether that is legal depends on the domain; a validation rule can compare source.type with target.type.

There is a design choice here. The separate InPort and OutPort classes make connector ends type-specific. An alternative is one Port class with a direction enum. That would use fewer classes but move the source/target direction rule into validation. Neither design is universally correct; use the distinctions your tools need to enforce or navigate.

6

Evolving the language

A metamodel can change as the domain becomes clearer. In this module, the first version knew only architectures and components. Later versions added ports, wiring, inverse navigation and signal types. Each revision changed the language, and some revisions required corresponding changes to the model. For instance, after adding PortType, the model sets each port’s type.

Treat a change as three questions:

  1. Which statements become possible or impossible? ref InPort target excludes an output port as a connector target; an enum excludes unknown signal names.
  2. What existing models must change? Adding a feature can require values in old models. Renaming a feature can leave old Flexmi attributes unresolved or silently fuzzy-matched.
  3. Which rules still need validation? Ecore describes structure and types; domain rules such as no self-loop and matching signal types require more work.

A useful review is to open the metamodel diagram in the Playground and trace containment from Architecture down to ports, then trace the ordinary references from Connector. The drawing is another view of the same metamodel. Module 3 will focus on writing and reading the models that conform to it.

7

Exercises

Exercise 1★★★conceptual5 min

Attribute, containment or reference? For each fact, choose attr, val or ref: (a) a component has a display name; (b) an architecture owns components; (c) a connector points to an output port already owned by a component.

Show solution

(a) attr String name; stores a plain value. (b) val Component[*] components; gives the architecture ownership of its components. (c) ref OutPort source; points to an existing port without moving ownership to the connector.

Exercise 2★★★conceptual5 min

Read the bounds. What do String[1], Component[+] and Connector[*] allow? If a Flexmi model with no components still runs a query, has the lower-bound requirement been satisfied?

Show solution

They mean exactly one string, one or more components, and zero or more connectors. A model with zero components violates Component[+] even if the query executes. A successful run is not a complete validity check.

Exercise 3★★★conceptual6 min

Draw the ownership tree. In the references example, who contains OrGate, who contains its open port, and who contains the connector? Why should the connector’s source be ref?

Show solution

Alarm contains OrGate; OrGate contains open; Alarm also contains the connector. The connector refers to open, which already has a container. Making source a containment reference would misstate who owns that port.

Exercise 4★★★coding8 min

Add a description. Starting from the classes example, add an optional String description to Component. Describe the two components in the Flexmi model and print each name with its description. Which keyword belongs before String?

Show solution

An optional plain value is an attribute. With no brackets its multiplicity defaults to zero or one. The complete files and output are here:

SolutionAdd a component descriptionOpen in Playground ↗
components.emf
@namespace(uri="components", prefix="components")
package components;

class Architecture {
  attr String[1] name;
  val Component[*] components;
}

class Component {
  attr String[1] name;
  attr String description;
}
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate" description="Combines inputs"/>
  <component name="Siren" description="Sounds the alarm"/>
</architecture>
More files: inspect.eol
inspect.eol
for (c in Component.all) {
  (c.name + ": " + c.description).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: Combines inputs
Siren: Sounds the alarm
Exercise 5★★★mini-project11 min

Type a thermostat. Starting from the types example, make a Thermostat architecture with a Sensor output reading of type ANALOG, a Controller input raw of type ANALOG, and an output command of type DIGITAL. Connect reading to raw. Run a query that prints each port and its type.

Show solution

The same metamodel describes this second architecture. The connector uses fully qualified paths from Thermostat:

SolutionType the thermostat portsOpen in Playground ↗
components.emf
@namespace(uri="components", prefix="components")
package components;

enum PortType {
  DIGITAL;
  ANALOG;
}

class Architecture {
  attr String[1] name;
  val Component[*]#architecture components;
  val Connector[*] connectors;
}

class Component {
  attr String[1] name;
  ref Architecture#components architecture;
  val Port[*] ports;
}

abstract class Port {
  attr String[1] name;
  attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref OutPort source;
  ref InPort target;
}
alarm.flexmi
<?nsuri components?>
<architecture name="Thermostat">
  <component name="Sensor">
    <outPort name="reading" type="ANALOG"/>
  </component>
  <component name="Controller">
    <inPort name="raw" type="ANALOG"/>
    <outPort name="command" type="DIGITAL"/>
  </component>
  <connector source="Thermostat.Sensor.reading" target="Thermostat.Controller.raw"/>
</architecture>
More files: inspect.eol
inspect.eol
for (p in Port.all) {
  (p.name + ": " + p.type).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
reading: ANALOG
raw: ANALOG
command: DIGITAL
Exercise 6★★★coding10 min

Navigate from a port to its owner. Starting from the opposites example, give Component.ports an opposite called Port.component. Print component.name and name for each port. Does the Flexmi model need an extra component attribute?

Show solution

Pair val Port[*]#component ports; with ref Component#ports component;. The Flexmi nesting already states ownership, so no extra model attribute is needed:

SolutionNavigate from port to ownerOpen in Playground ↗
components.emf
@namespace(uri="components", prefix="components")
package components;

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[*]#component ports;
}

abstract class Port {
  attr String[1] name;
  ref Component#ports component;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref OutPort source;
  ref InPort target;
}
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door"/>
    <outPort name="open"/>
  </component>
  <component name="Siren">
    <inPort name="sound"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.Siren.sound"/>
</architecture>
More files: inspect.eol
inspect.eol
for (p in Port.all) {
  (p.component.name + "." + p.name).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
OrGate.open
Siren.sound
8

Self-check quiz

1
What does attr String name; declare when no multiplicity appears?
2
Which declaration expresses that an architecture owns its components?
3
Why is Connector.source an ordinary ref?
4
What does #architecture identify in val Component[*]#architecture components;?
5
Can a model instantiate an abstract class Port directly?
6
What does an enum help express?
7
A query runs on a model with zero components, while the metamodel says Component[+]. What can you conclude?
9

Further reading