SteKoe

Hi! Ich bin Stephan, Software-Entwickler, Hobby-Fotograf, Bogenschütze und Läufer.

OCL.js

28 August 2017

  1. Einleitung
    1. Shortcomings von Modellierungssprachen
    2. OCL ftw!
  2. Vom String zum AST
    1. Der Lexer
    2. Der Parser
  3. Verwendung der OCL.js

Einleitung

Während meines Studiums der Wirtschaftsinformatik habe ich mich auf die Modellierung mit Hilfe von speziellen Sprachen spezialisiert. Stets zentraler Augenmerk wurde auf den Aspekt Abstraktion gelegt, welchem alle Modellierungssprachen folgen. Ethymologisch betrachtet kommt das Wort Abstraktion bzw. das entsprechende Verb abstrahieren aus dem lateinischen und bedeutet so viel wie abziehen, entfernen oder trennen. Mit Hilfe von Modellierungssprachen trennt man sich von gewissen Dingen, nämlich von Details oder Aspekten, welche für den aktuell betrachteten Fall oder die Domäne nicht relevant sind.

Shortcomings von Modellierungssprachen

Modellierungssprachen wie beispielsweise die UML haben jedoch einen zentralen Nachteil: Die Ausdrucksmöglichkeiten sind begrenzt, sodass verschiedene Sachverhalte nicht durch die verwendete Sprache allein abgebildet werden können oder dies nur schwer möglich ist.

Das folgende Beispiel soll dies verdeutlichen. Abbildung 1 zeigt ein UML-Diagramm, welches beschreibt, dass eine Person keine oder mehrere Autos besitzen kann. Ein Auto hat immer nur genau einen Fahrzeughalter.

Image
Abb. 1: UML-Beispiel: Person besitzt Autos

Neben den Attributen der jeweiligen Objekten Person und Auto ist keinerlei weitere Semantik dem Diagramm zu entnehmen. Wie aber ist vorzugehen, wenn nun die Bedingung, dass eine Person mindestens 18 Jahre alt sein muss um Autos zu besitzen, abzubilden ist?

OCL ftw!

An dieser Stelle greift die OCL. Sie ergänzt die gegebene Modellierungssprache um weitere Konzepte, welche Constraints – also weitere Einschränkungen – abbilden können.

1. Beispiel: Der Besitzer eines Autos muss älter als 18 Jahre sein

Kommen wir zurück zu dem Beispiel, wo eine Person erst dann Autos besitzen darf, wenn diese über 18 Jahre alt ist. Mit Hilfe der gegebenen Modellierungssprache ist diese Einschränkung nur schwer oder gar nicht umsetzbar, da für die Prüfung Instanzdaten benötigt werden, welche auf der abstrakten Modellebene noch nicht bekannt sind.

context Person
    inv: self.age < 18 implies self.fleet->size() = 0

Die obige OCL-Regel legt eine Invariante für den context Person fest und bezieht sich somit auf alle Objekte des Typs Person. Die Invariante legt fest, dass wenn eine Person jünger als 18 Jahre alt ist, die Fuhrparkgröße ebenfalls 0 sein muss.

2. Beispiel: Eine Person kann nicht sein eigenes Elternteil sein

Ein weiteres Beispiel stellt folgendes Diagram dar:

Image
Abb. 2: UML-Beispiel: Eine Person hat zwei Elternteile

Eine Person hat zwei Elternteile. Was jedoch eine (biologische) Einschränkung darstellt ist, dass eine Person nicht sich selbst als Elternteil haben kann. Es ist schwer - wenn nicht unmöglich - dies allein über die gegebenen Konzepte von UML zu lösen. Mit Hilfe der folgenden, simplen OCL-Regel hingegen, wird die Einschränkung auf intuitive Weise festgehalten.

context Person
    inv: self.parents->forAll(p | p <> self)

Diese OCL-Regel definiert erneut eine Invariante für den context Person, welche prüft, dass für alle Elemente in der Collection parents gilt, dass diese nicht self (ich selbst) sind.

Obige Beispiele sollen zeigen, wie die OCL funktioniert und für welche Anwendungszwecke sie verwendet werden kann. Im folgenden Abschnitt wird erklärt, wie die Implementierung der OCL.js strukturiert ist.

Vom String zum AST

OCL ist eine textbasierte Sprache und muss daher zunächst von einem Text in eine ausführbare Form gebracht werden. Dies erfolgt über die klassische Verkettung von Lexer und Parser, welche einen String zerlegen und aus den zerlegten Einzelteilen einen Abstract-Syntax-Tree1 (AST) erzeugen (s. Abbildung 3).

Image
Abb. 3: Vom String zum AST

Der Lexer

Ein Lexer übernimmt die Aufgabe die als Text eingegebene OCL-Regel in einzelne Token zu zerlegen. Hierfür wird definiert, welche Teile des eingegebenen Strings in Token übersetzt werden. Das folgende Listing zeigt einen Auszug aus der OCL.js Lexer-Definition.

\s+                             /* ignore whitespaces */
\-?[0-9][0-9]*                  return 'integer'
"context"                       return 'context'
"inv"                           return 'inv'
"->"                            return '->'
"("                             return '('
")"                             return ')'
"."                             return '.'
":"                             return ':'
[a-zA-Z][a-zA-Z0-9]*            return 'simpleName'
">"                             return '>'

Mit Hilfe der oben angeführten Lexer-Definition, kann folgende OCL-Regel in einzelne Token zerlegt werden.

context Car inv:
    self.licensePlateNumber->size() > 0

Wurde die OCL-Regel in Token zerlegt, ergibt sich folgendes Bild:

context simpleName inv:
    simpleName.simpleName->simpleName() > integer

Der Parser

Die Aufgabe des Parsers ist nun, die einzelnen Token gemäß einer gegebenen Grammatik in einen Abstract-Syntax-Tree zu wandeln. Ein Auszug der OCL-Grammatik ist im folgenden Listing zu sehen.

classifierContextDecl
    : context simpleName inv
        { $$ = new ContextExpression($2, $3) }
    ;

inv
    : inv simpleNameOptional : oclExpression
        { $$ = new InvariantExpression($4, $2) }
    ;

oclExpression
    : pathName
        { $$ = new Expression.VariableExpression($1) }
    | oclExpression . simpleName
        { $$ = new Expression.VariableExpression([$1.variable, $3])
    | oclExpression -> simpleName
        { $$ = functionCallExpression($3, $$); }
    ;

Trifft eine der gegebenen Grammatikregeln auf die kombination der gegebenen Token zu, wird entsprechend eine Expression instanziiert. Sind alle Token verarbeitet, ergibt sich ein AST, welches im folgenden Listing zu sehen ist. Abbildung 4 zeigt den AST zudem in Form eines Baumdiagramms.

ContextExpression[name:”Car”]
  - InvariantExpression[name:””]
     - OperationCallExpression[operator:”>”]
        - SizeExpression[name=”size”]
           - VariableExpression[variable:“self.licensePlateNumber”]
        - NumberExpression[number:”0”]

Die einzelnen Expressions / Bestandteile des AST folgem dem Interpreter-Pattern2 und implementieren jeweils eine evaluate()-Funktion. Mit Hilfe dieser Funktion können die Invarianten gegen eine gegebene Menge an Objekte evaluiert werden.

Image
Abb. 4: Resultierender AST

Verwendung der OCL.js

Die Installation der OCL.js ist denkbar einfach: Mit Hilfe des Package-Managers NPM lässt sie sich ganz einfach dem Projekt hinzufügen:

npm install ocl.js

Anschließend lässt sich in einer JavaScript-Datei die ‘ocl.js’ importieren, welche eine OclEngine exportiert. Die OclEngine stellt eine API bereit, welche hier näher dokumentiert ist. Über die Funktion addOclExpression lassen sich neue OCL-Regeln der Engine bekannt machen. Anschließend kann der evaluate-Funktion ein Objekt übergeben werden, welches von der OCL-Engine geprüft wird.

Als Ergebnis erhält man ein ResultObject, welches das Ergebnis der Evaluation enthält (true oder false) sowie die Namen der fehlgeschlagenen Invarianten.

// Import the OclEngine
import OclEngine from 'ocl.js';

// Create an instance of the OclEngine
const oclEngine = OclEngine.create();


// Define an OCL expression
const myOclExpression = `
    context Car inv:
        self.licensePlateNumber->size() > 0
`;
// Add the expression to the engine and let the lexer / parser run
oclEngine.addOclExpression(myOclExpression);

// Pass in an object to evaluate against the given OCL expressions
// registered in the OclEngine instance.
const resultObj = oclEngine.evaluate({});

// Get the actual result of the evaluation (true / false)
const result = resultObj.getResult();

// The names of failed invariants. May be "anonymous" if
// the invariant was not given a name.
const failedConstraints = result.getNamesOfFailedInvs();

  1. https://de.wikipedia.org/wiki/Abstrakter_Syntaxbaum 

  2. https://de.wikipedia.org/wiki/Interpreter_(Entwurfsmuster)