Lesson 6 -- Multiplicities & Constraints

Lesson 6
Adding Semantic Rules to the Metamodel:
Using Multiplicities and Constraints

Introduction to Lesson 6
6.1 Multiplicity
6.2 Constraints

Introduction to Lesson 6

When you first ran the interpreter of Lesson 3 on a larger model, you probably detected inconsistencies: ports or perimeters not connected, or connected too many times. It would be advantageous if these logical errors could be detected as early as possible, preferably at the exact moment when the user makes the inconsistent modification.

This lesson introduces multiplicities and constraints, the GME consistency checking mechanisms.

6.1 Multiplicity

Whenever an association is defined in the metamodel, a sequence of symbols, such as "0..*", appears at both ends. This is the multiplicity specification, which determines the acceptable number of associations in which an object can participate. The usual format is <min>.. <max> (or <minmax> if the two values are the same). "*" means infinity, so the default value, "0..*" gives practically no limit on the number of associations.

Here is an example from the networking paradigm: Interfaces should connect to at most one network or perimeter. (Unconnected interfaces will be allowed, since router ports sometimes remain temporarily or permanently idle.) To indicate this in the metamodel, change the destination multiplicity of the Connection association from "0..*" to "0..1". To achieve this, right click on the association line on the destination side and change its Cardinality attribute as described before. Also, make sure that each Perimeter is connected to exactly one Network, or to another Perimeter. Set the "dst" multiplicity of the NetworkEquiv association to "1". It is useless to allow Perimeters without associated Networks, so 0 is not permitted in this case.

Fig 6.1 Multiplicities on associations in the metamodel

On the association line, multiplicities are indicated at the opposite end from the object to which they apply. For example, in the paragraph above, a multiplicity rule that applies to interfaces is actually indicated at the "network" side of the connection.

Internally, GME treats multiplicities as a special kind of constraint for which a simple specification mechanism is provided. Generic constraints are more powerful than multiplicites, but are more difficult to specify. As soon as a new version of the paradigm is generated, the necessary constraint settings will automatically take effect. These are "warning" type constraints; they are not "enforced", but violations are reported to the user in the form of a dialog box. Adherence to the constraints is checked in response to the following events:

- The parent model of the object for which the limited multiplicity is in effect (i.e. interfaces and perimeters in our example) is closed in the editor or collapsed in the browser.
- An interpreter is started and the object for which the multiplicity is in effect is visible, either as an open model or as a child of an open model.

6.2 Constraints

Multiplicities specify the number of objects that can participate in a particular association, which is a typical consistency rule. However, there are many other plausible situations in which more general validity rules need to be expressed. These rules are provided by a general and functionally rich language, originally defined as part of the standard UML notation: OCL, the Object Constraint Language.

6.2.1 Object Constraint Language (OCL)

GME includes the full OCL 1.4 with some extensions to the original language. OCL is based on predicates, sentences that are either true or false and must evaluate to "true" in order to satisfy the constraint. As an example, study the following constraint:

> `self.parts( Fuse )->forAll( b : Fuse | b.amps <= 60 );`

This constraint will be satisfied only if all the "Fuse" children of the object (such as a Fusebox) have an "amps" value not greater than 60. Another constraint, slightly more complex, requires that at least five red or green fuses exist in the fusebox:

> `self.parts( Fuse )->select( b : Fuse | b.color = "red" or b.color = "green" )->size > 5`

The examples above illustrate the following principal features of this language:

- The OCL syntax seems rather unusual at first, but is actually fairly simple. The language uses a number of operators (like "=", "<", and "->") and a reasonable set of built-in oprations (like "parts( )", "select( )", and "forAll( )"). Most of the extensions in GME are actually used to navigate the GME model tree. With moderate practice in understanding operators, expressions can easily be read as natural-language predicates or sentences.
- OCL is purely functional. In other words, the only product of an operation or feature call is its return value. There are no variable assigments or other side effects. Long expression chains and function call sequences are typical characteristics of functional languages.
- Expressions at the top level of a statement are always of boolean type, but subexpressions can be strings, integers, booleans, FCOs, folders, or collections (Sets, Bags, Sequences) of these types.
- Feature calls in expression chains are applied to the result of the subexpression that precedes the "->" or "." operator. Expressions have an implicit object that serves as their context and their default argument. This object is always the one that the constraint is attached to (in this case, the Fusebox). It serves as the default context of (sub)expressions if nothing else is specified. So "`self.parts( Fuse )`" selects children of the current fusebox, but "`b.color`" specifies another object - the one temporarily referred to as "b" - so the current model is not used.

See Appendix B (the OCL language summary) in the GME Users Manual for a detailed definition of the GME OCL language.

There is a simple mechanical way to convert OCL expressions into grammatically correct natural language predicates, although the resulting sentence may be rather hard to understand. This is mainly because natural languages do not have good "bracketing" mechanisms. For example, "this dwarf is sleepy or hungry and angry" may translate to either "(sleepy or hungry) and angry", or "sleepy or (hungry and angry)" ). For the same reason, converting natural language predicates into OCL is not a straightforward task, althought OCL uses the usual precedence and associativity rules of operators and expressions.

6.2.2 A constraint example

The process of creating a constraint will be illustrated with the networking example. Let us consider the following problem: The modeling environment introduced Perimeters to represent "central" or "backbone" Networks in lower-level diagrams. So far, however, nothing has kept the user of the modeling environment from connecting a Perimeter to a Network in the same diagram. This is clearly not what Perimeters should be used for. Let's see if a constraint will help us solve the problem. The natural language constraint predicate would look something like this:

"For all network equivalence relationships, the parent of the source Perimeter must be a sibling of the destination."

We can simplify the language by avoiding the use of the "sibling" predicate and getting rid of some unnecessary information. The result is:

"For all network equivalence relationships, the parent of the parent of the source must be equal to the parent of the destination."

The predicate now contains a very limited set of functions:

- selection of a group of objects by kind/role
- "for all" iterator
- accessing the source, destination and parent
- comparison of objects

Since the OCL reference contains functions for all of these concepts, the first OCL constraint can be assembled: `

> self.attachingConnections( EquivalenceConn )->forAll( c |
>   c.connectionPoints( "src" )->theOnly().target().parent().parent() =
>   c.connectionPoints( "dst" )->theOnly().target().parent()

` This OCL expression will now be used as a constraint.

6.2.3 Designing a constraint

An OCL expression becomes a constraint when it is specified in the metamodel along with some important additional information. First of all, we need to choose which metaobject the constraint should be attached to. Since a constraint usually specifies the configuration of several objects, this is not always a trivial decision, although the complexity of the constraint expression often varies depending on the object to which it is assigned. As a general rule, a constraint should be bound to the object around which the problem is centered. In the networking example, the problem focuses on the equivalence connection (NetworkEquiv) itself. From the perspective of the connection, the OCL expression looks simpler:

> `self.connectionPoints( "src" )->theOnly().target().parent().parent() =
> self.connectionPoints( "dst" )->theOnly().target().parent()`

The automatic simplifying of the expression is a good indication that our choice for the attached object is the right one.

Next, we must determine when a constraint needs to be checked. Optimally, constraints should be checked on the fly, whenever a possibly relevant change occurs. Since the constraint translates into a constraint-checking algorithm which needs to be run in order to do the test, continuous checking is not possible (or, at the very least, not efficient). Instead, the metamodeler should specify the particular operations during which this constraint could be violated. GME classifies all modifying operations into the following categories.

Constraint checking events, mnemonics and entries in the Constraint attributes dialog
Description | Event ID | Attribute name
close event: GME close model | OBJEVENT_CLOSEMODEL | On close model
The object has been created | OBJEVENT_CREATED | On create
The object has been destroyed (limited access is available) | OBJEVENT_DESTROYED | On delete
A new child added | OBJEVENT_NEWCHILD | On new child
A child removed/ moved away | OBJEVENT_LOSTCHILD | On lost child
Object has been moved | OBJEVENT_PARENT | On move
Subtype, instance created | OBJEVENT_SUBT_INST | On derive
object has been connected | OBJEVENT_CONNECTED | On connect
object has been disconnected | OBJEVENT_DISCONNECTED | On disconnect
Name, etc. has been changed | OBJEVENT_PROPERTIES | On change attribute
Attribute changed | OBJEVENT_ATTR | On change property
ref pointer, set member, conn endpoint change | OBJEVENT_RELATION | On change assoc.
object has been referenced | OBJEVENT_REFERENCED | On refer
object reference has been released | OBJEVENT_REFRELEASED | On unrefer
object has been included in set | OBJEVENT_SETINCLUDED | On include in set
object has been excluded from set | OBJEVENT_SETEXCLUDED | On exclude from set
Registry changed | OBJEVENT_REGISTRY | N/A
Basetype relation broken/added (???) | OBJEVENT_BASE | N/A
Anything under the object "Position" regnode | OBJEVENT_POSITION | N/A

These events can be selected or deselected in the Attributes dialog for Constraint objects in the metamodeling environment (Fig 6.2).

According to the list, OBJEVENT_RELATION seems to be the most suitable operation. This event occurs when a new connection is created, or when an existing one is modified. (It is also triggered if any of the target objects is moved in the modeling hierarchy.)

A third piece of information to specify is the scope (or depth) of the constraint. Sometimes the event is not generated for the object that the constraint belongs to, but to a descendant of that object. An example of this situation is when a constraint is attached to a model, which mandates some specific attribute configuration for the children of the model (e.g. "order number is unique for each child"). The events are generated for the children, while the constraint is associated with the model (it could also be associated with the children, but that may result in a clumsier constraint expression).

Depth has 3 possible values:

1. 0 - the event must be generated in the the constraint owner in order to check the constraint.
2. 1 - the constraint is checked if the event is generated in either the constraint owner object or any of its immediate children.
3. "Any" - the event may be sent to any of the descendants of the constraint owner (including itself) for the constraint to be checked.

If a constraint is attached to a non-container (anything other than a model or folder), the depth setting does not matter.

Our final task is deciding what to do if a constraint check fails. These actions are specified through constraint priority, which also determines the order of constraint tests so that grave violations can be dealt with before the less serious ones. Priorities range from 1 (highest) to 10 (lowest). A priority of 1 means that the constraint is enforced; if a violation takes place, it must be dealt with. Lower priorities are given to "informational" constraints, where the user can ignore the violation if desired.

Generally, a constraint should be informational if it may produce false alarms, or if there are certain situations in which the constraint may be legally violated (for example, a new object with a non-zero multiplicity is created and is not yet connected to anything). Also, constraints fired at the "close model" phase should be strictly informational.

In the networking example, there is no compelling reason to be tolerant; therefore, the highest priority will be specified.

6.2.4 Adding a constraint to the metamodel

We now have enough information to be able to add the first constraint to the networking metamodel (Fig 6.2):

1. Open the metamodel and the ParadigmSheet.
2. Switch to the "Constraints" aspect, and insert a constraint near the existing metaentity named "NetworkEquiv". Associate the two objects by drawing a connection between them.
3. Assign a name to the constraint, something like "EquivPointsToUpperLevel". Open the attributes dialog, and enter the OCL expression as seen above. Click the checkbox for the "On change assoc" event, and set Priority to "1". Depth is irrelevant in this case, although 0 is the logical choice. The description should be something similar to "NetworkEquiv relation must point to object at a higher level." (The default parameter Attribute is only used for constraint functions, which are not covered by this tutorial.)
4. The metamodel has been successfully updated; interpret and register the new paradigm.

Fig 6.2 Specifying a constraint in the metamodel

Open a network diagram and update the paradigm, either directly or through XML export/import. When the File/Register Components... dialog is opened, we see that the constraint manager has been activated (!). Test the constraint by attempting to connect Perimeters and Networks in the "right" way and the "wrong" way. Also, try moving either end of the connection to see if the model behaves as expected.

This constraint was the first to be added to the model, but it is certainly not the only possible one. Challenge yourself by creating extra constraints that fulfill the following requirements:

- Every router has at least one Ethernet port.
- No two hosts are allowed to have the same IP address.
- Every diagram, except the root diagram, must have at least one Perimeter object.

<< Previous Lesson |  | Complete List |  | Next Lesson >>