Aperçu du langage OCL

Plan de ce cours :

Références pour ce cours :

Introduction

OCL = Object Contraint Language.

Exemple de diagramme de classe imprécis:

imprecise class diagram

Décoré avec OCL :

more precise class diagram with OCL expressions

Ou dans la version textuelle :

-- le nombre de siège est positif
context AirPlane
inv : self.numberOfSeats >= 0 -- ou numberOfSeats >= 0

-- initialement aucun passager enregistré
context Flight::passengers : Set(Person)
init : Set{}

context Flight::numberOfPassengers : Integer
derive : passengers->size()

context Flight::availableSeats() : Integer
body : capacity - numberOfPassengers  

Comme les autres formalismes de spécification :

On parle de contraintes ou d'expressions OCL.

Contraintes
Expressions

Par la suite on parlera d'expressions au sens large.

Notions de base

Contexte de définition

Version graphique
Version textuelle
Nommage de l'instance contextuelle
-- toute personne a un nom
context p : Person
inv : p.name <> ''

context Person
inv : name <> ''

context Person
inv : self.name <> ''

Accès aux propriétés d'un objet

Notation pointée. Dans le contexte de Flight :

Plus de détails sur la navigation d'un diagramme ensuite.

Spécification d'une propriété

Notation quatre points ::

Exemples :

context Flight::capacity : Integer
context Flight::availableSeats() : Integer
context Flight::ajouterPassager(name : String)
context Flight::passengers : Set(Person)

Éléments de spécification

Les contrats de base, mais aussi des facilités proches d'un langage de programmation (plus que de spécification).

Commentaires

-- un joli commentaire qui va jusqu'en fin de ligne

Expressions de valeurs

Spécification du corps d'une opération

Pour opération de type query (sans effets de bord), description exacte du résultat par le mot-clé body.

context Compte::getSolde() : Integer
body : self.solde  

Valeurs initiales et dérivées

Pour spécifier la valeur initiale/dérivée d'un attribut ou d'une extrémité d'association. Mot-clé init et derive.

-- solde initialisé à 0
context Compte::solde : Integer
init : 0

-- un compte rémunéré
context Compte::remuneration : Integer
derive : self.solde * 0.005 

Contraintes et contrats

Possibité de nommer les contrats (comme en Eiffel, très pratique).

OCL ne précise pas ce qui se produit en cas de violation d'un contrat. C'est du ressort de l'implantation.

Combinaison de contrats

context Account
inv balance : self.balance >= self.min and self.min <= 0

a la même sémantique que :

context Account
inv balanceCorrect : self.balance >= self.min
inv negativeMin : self.min <= 0

ou

context Account
inv negativeMin : self.min <= 0
inv balanceCorrect : self.balance >= self.min

Invariant de classe

Expression booléenne qui doit être vraie de toute instance de la classe dans tout état stable.

Pré et post-conditions

Stéréotypes «precondition» et «postcondition» respectivement.

context Compte::debiter(montant : Integer)
pre montantDebit : montant > 0 and 
        montant <= self.solde - self.plancher
post debitEffectue : self.solde = self.solde@pre - montant

On peut utiliser @pre sur un appel de méthode.

context Compte::getSolde() : Integer
post : result = self.solde

context Compte::crediter(montant : Integer)
pre montantCreditPositif : montant > 0
post creditEffectue : self.solde = self.getSolde()@pre + montant

Les conditionnelles

On utilise soit l'opérateur booléen implies, soit le if then else.

Ex : on change la définition de remuneration, pas de rémunération en dessous d'un certain seuil :

context Compte inv : self.solde < 100 implies
      self.remuneration = 0

Ex : on change encore, taux de rémunération variable :

context Compte
inv : if self.solde < 100 
      then self.remuneration = 0.001 * self.solde
      else self.remuneration = 0.005 * self.solde
      endif

Les variables

Pour factoriser une sous-expression localement à une expression : let ... in.

context Compte::ajouterInterets(pourcent : Real) : Real
post ajoutEffectue : 
   let facteur : Real = 1 + pourcent/100
   in solde = solde@pre * facteur
-- facteur utilisable seulement dans cette post-cond

Ajout d'attributs/opérations au modèle : def

Si l'attribut major de Person n'existe pas dans le diagramme UML :

context Person
def: major : Boolean = age >= 18

context Person
def: isMajor() : Boolean = major

-- major et isMajor() utilisables dans toute expression  

Navigation

Navigation d'une association sans nom

Navigation d'une association qualifiée

Existe encore en UML 2 ?

-- tous les comptes de l'agence
self.account
-- le compte numéro 889988
self.account['889988']

Navigation depuis/vers une classe-association

Depuis la classe-association la navigation retourne un objet :

Context Enrollment
inv : self.date.isBefore(courses.date)  

Vers la classe association :

context Student
inv : Enrollment[students]->notEmpty()
-- enrollment[students]->notEmpty()
-- enrollment->notEmpty()  

Utilisation du nom de rôle obligatoire si association récursive.

Les types

Dans une expression OCL rattachée à un diagramme UML possible d'utiliser :

Notion de paquetage :

package Package::SubPackage
context ...
... 
endpackage

Type énuméré

context Person::isMale() : Boolean
body : gender = Gender::male  

Data Type

Existe encore en UML2 ? Sorte de classe statique

Date::now  

Types de base OCL

Aussi types Collection, Bag, Set, Sequence.

Collections OCL

Les différentes collections

La syntaxe des littéraux est en gros la même pour tous :

Les collections d'UML 1.4 étaient forcément plates, UML et OCL 2.0 autorisent les collections de collection.

Quelques opérations

Bien sûr toutes sans effet de bord...

Type Collection
Type Set
Type OrderedSet

etc, etc

Accès aux propriétés d'une collection

Attention Notation fléchée -> et non pointée.

context Person
def : accountNumber() : Integer = self.account->size()  
context Person
inv hasAnAccount : self.age < 18 implies account->isEmpty()
context Person :
inv : account->notEmpty() implies agency->notEmpty()

Navigation et collections

Accès à l'extrémité d'une association : expression résultante de type variable suivant la cardinalité et l'ordonnancement.

Dans le contexte de A :

self.b : B
self.b : Set(B)
self.b : OrderedSet(B)

Ex : dans le contexte de Person.

Ex : Vol et escales dans Aéroport

petit diagramme de classe
pour Aeroport avec escales
Cas particulier

Cardinalité 0 ou 1 traitée comme une collection pour tester l'existence d'une référence.

context Account
inv : bankBook->notEmpty() implies owner.age <= 28
-- inv : bankBook->asSet()->notEmpty() implies owner.age <= 28
Enchainements

Prendre garde au typage en cas de navigations enchaînées. Dans le contexte de A :

self.b.c : Set(C)
self.b.c : Bag(C)
self.b.c : Sequence(C)
self.b.c : Bag(C)
self.b.c : Sequence(C)
context Account
-- l'ens des clients de l'agence contient le propriétaire du 
-- compte
inv : self.bankAddress.client->includes(owner)
-- le compte appartient à un des clients de l'agence
inv : self.bankAddress.client.account->includes(self)

Un exemple avec une variable :

context Person inv :
 let totalBalance : Integer = account->collect(balance)->sum() in
 age > 18 implies totalBalance > 100 

Le même avec une définition :

context Person 
def : totalBalance : Integer = account.balance->sum() 

context Person
inv : age > 18 implies totalBalance > 100 

Itérateurs sur collections

Itérateur iterate
collection->iterate(element : Type1;
                    result : Type2 = 
                  | 

  • element : variable d'itération sur la collection source ;
  • result : accumulateur avec initialisation ;
  • expr : évaluée à chaque étape, affectée à result.
context Collection(T)::sum() : T 
post: result = self->iterate( elem : T; 
                              acc : T = 0 | acc + elem )

Type du résultat syntaxiquement optionnel.

context Collection::count(object : T) : Integer
post: result = self->iterate( elem; 
                        acc : Integer = 0 |
                        if elem = object 
                        then acc + 1 
                        else acc endif)  
Autres itérateurs
  • pas d'accumulateur ;
  • variable d'itération et son type optionnels
    opIter(element : Type | )
    opIter(element | )
    opIter()
    
  • la première alternative me semble la plus claire
  • imbrication des espaces de nommage, recherche des noms dans les autres espaces si échec.
Itérateur exists

Plus limité que le quantificateur JML : limité à une itération sur collection.

Retourne vrai si au moins un élément de la collection source satisfait le corps (expr bool).

source->exists(it | body) =
source->iterate(it; result : Boolean = false 
              | result or body)  
-- le conseiller d'un client gère au moins un
-- des comptes de ce client
context Person inv :
self.account->exists(c : Account | 
    c.administrator->includes(self.counsellor)) 

Imbrication des espaces de nommage :

context Agency::hasClientIncomeGreater(itsIncome : Real) : Boolean
body: self.client->exists(income >= itsIncome)
  • Si Agency a aussi un attribut ref ?
  • Bien celui de Person qui est pris (espace de nommage le plus interne)...
  • ... mais pas facile à lire !
context Agency::hasClientIncomeGreater(itsIncome : Real) : Boolean
body: self.client->exists(client : Client | client.income >= itsIncome)  
Itérateur forall

Retourne vrai si tous les éléments de la collection source satisfont le corps (expr bool).

source->forall(it | body) = 
   source->iterate(it; 
                   result : Boolean = ???
                 | ??? )
-- un gestionnaire ne gère pas ses propres comptes
context Person inv :
self.managedAccount->forAll 
    (c : Account | c.owner <> self)
context Person inv :
self.managedAccount->forAll(owner <> self)

Il existe aussi un mécanisme permettant d'écrire sans lourdeur une double itération :

context Person inv :
self.contract->forAll(c1,c2 : Contract | 
  c1 <> c2 implies c1.id <> c2.id) 
Itérateurs select et reject

Retournent le sous-ensemble de la collection qui satisfait/ne satisfait pas le corps (expr bool).

-- Pour \texttt{Set(T)}
source->select(iterator | body) =
  source->iterate(iterator; result : Set(T) = Set{} |
         if body then result->including(iterator)
         else result
         endif)  
-- obligation faite aux agences d'accepter des
-- clients à faible revenu
context Agency
inv : self.client->select(Person p | p.income < 100)->notEmpty()

ou

context Agency
inv : self.client->select(income < 100)->notEmpty()

Idem pour reject.

Itérateur one

Retourne vrai si exactement un élément de la collection satisfait le corps (expr bool).

source->one(iterator | body) =
   source->select(iterator | body)->size() = 1  
Itérateur any

Retourne n'importe quel élément de la collection (en pratique le premier) qui satisfait le corps (expr bool), null sinon.

source->any(iterator | body) =
   source->select(iterator | body)->asSequence()->first()
Itérateur sortedBy

Pour les collections dont le type possède une relation d'ordre : retourne une collection ordonnée selon les body croissants.

context Agency::richestClient() : Client
body : client.income->sortedBy(income)->last()
Itérateur collect

Retourne une collection plate contenant l'ensemble des body correspondant à chaque élément de la collection source.

Dans le contexte de Agency, collecter l'ensemble des ages des clients :

self.client->collect(p : Person | p.age) 

Attention à ne pas lire «collecter l'ensemble des Person telles que...». Dans l'exemple ci-dessus on produit un Bag d'entiers.

Navigation et collect
  • Accès à une extrémité d'association : produit une collection.
  • Deux navigations enchaînées : cache un collect
    self.nav1.nav2 
    self.nav1->collect(nav2)
    
  • Raccourci syntaxique : source->collect(X) remplacé par source.X.
self.client.age

Pas toujours possible d'utiliser le raccourci :

context Commande
inv : montantHT = lignes.collect(nbArt * article.prix)  
Itérateur isUnique

Retourne vrai si chaque évaluation du corps pour les éléments de la collection source produit un résultat différent, vrai sinon.

context Agency
inv accountNbUnique : self.account->isUnique(accountNumber)

Types OCL avancés

Types particuliers : OclInvalid, OclVoid, OclAny

OclInvalid

Type conforme à tous les autres qui ne contient que la valeur invalid (ex : résultat d'une division par 0).

OclVoid

Type conforme à tous les autres qui ne contient que la valeur null (pas de référence).

  • Tout appel d'opération appliqué à null produit invalid.
  • null ou invalid = valeur indéfinie
  • en général, une expression dont une partie s'évalue à indéfini est elle-même indéfinie.
  • cas particuliers des booléens : true or indéfini = true
  • Pas d'opérations mal gardées en OCL
  • expressions sémantiquement équivalentes :
    x <> 0 and 1/x > 0.1
    1/x > 0.1 and x <> 0 
    
  • mais attention à la génération de code !
    if (1/x > 0.1 && x <> 0) ...  
    

OclAny

Super-type de tous les types d'OCL, propose des opérations qui s'appliquent à tout objet.

Typage
  • oclIsTypeOf(t : OclType) : Boolean : vrai si le type de self et t sont exactement les mêmes.
  • oclIsKindOf(t : OclType) : Boolean : vrai si t est le type de self ou un sous-type (cf instanceOf de Java).
  • oclAsType() : permet le transtypage vers un sous-type.
  • allInstances() : S'applique à une classe et non un objet. Retourne l'ensemble de toutes les instances d'une classe et de ses sous-types.
    allInstances() : Set(T)
    

allInstances() est fortement déconseillé :

  • Risque de complexification inutile des invariants ;
  • Ne marche que pour les types qui ont un nombre fini d'instances (ex : types utilisateur avec création explicite) ;
  • Pas évident à implanter, notamment en Java.
Context Person
inv : self.parents->size()=2
inv : Person.allInstances->forall(p : Person | 
                                p.parents->size()=2 )
Test des valeurs null et indéfini
  • oclIsInvalid()
  • oclIsUndefined()
  • etc
oclIsNew()

Utilisé dans une post-condition seulement. Vaut vrai si l'instance est créée pendant l'opération.

oclIsNew() : Boolean
post: self@pre.oclIsUndefined()
  • Très expressif au niveau spécification
  • Tout aussi difficile à implanter ?
context Agency::createAccount(p : Person) : Account
pre : client->includes(p)
post : result.oclIsNew() and 
client = client@pre->including(p) and
p.account = p.account@pre->including(result)

Priorité des opérateurs

Du plus au moins prioritaire :

  • @pre
  • . et ->
  • not et moins unaire
  • * et /
  • + et -
  • if-then-else-endif
  • <=, >, etc
  • =, <>
  • and, or et xor
  • implies

Outils liés à OCL

Les modeleurs

Certains modeleurs permettent la saisie de contraintes et expressions OCL, et vérifient leur syntaxe.

  • pas Objecteering
  • OCLE: Object Constraint Language Environment
  • ArgoUML
  • d'autres, voir par ex la page dédiée de Klasse Objecten

Les vérificateurs

Par ex des outils qui permettent la construction de diagrammes d'objets et vérifient qu'ils sont conformes aux contraintes OCL.

Les générateurs de code

Ils générent (par ex en Java) un squelette d'application à partir du modèle tel que les contraintes OCL associées au modèle seront surveillées à l'exécution et les expressions (type initialisation, règle de dérivation) seront respectées.

  • OCLE : ne sait pas gérer les valeurs @pre
  • Octopus de Klasse Objecten : OCL Tool for Precise Uml Specifications. Ne génére pas de code pour les post-conditions. N'est pas un modeleur, mais accepte certains modèles au format XMI (issus de Objecteering, Poseidon)
  • Toolkit de Dresde : n'est pas un modeleur mais coopère avec ArgoUML. Apparemment le plus complet.

NB : et Objecteering ? Permet d'annoter des classes/ méthodes avec des pré/post-conditions/invariants... écrits en Java !


Mirabelle Nebut
Last modified: Fri Feb 24 10:23:29 CET 2006