Plan de ce cours :
Références pour ce cours :
OCL = Object Contraint Language.
Exemple de diagramme de classe imprécis:
Décoré avec OCL :
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.
Par la suite on parlera d'expressions au sens large.
-- toute personne a un nom context p : Person inv : p.name <> '' context Person inv : name <> '' context Person inv : self.name <> ''
Notation pointée. Dans le contexte de Flight :
Plus de détails sur la navigation d'un diagramme ensuite.
Notation quatre points ::
context TypeName::AttributeName : TypeAttribute
context Typename::operationName(param1 : Type1, ... ) context Typename::operationName(param1 : Type1, ... ) : ReturnType
context Typename::operationName : Type
Exemples :
context Flight::capacity : Integer context Flight::availableSeats() : Integer context Flight::ajouterPassager(name : String) context Flight::passengers : Set(Person)
Les contrats de base, mais aussi des facilités proches d'un langage de programmation (plus que de spécification).
-- un joli commentaire qui va jusqu'en fin de ligne
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
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
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.
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
Expression booléenne qui doit être vraie de toute instance de la classe dans tout état stable.
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
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
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
Existe encore en UML 2 ?
-- tous les comptes de l'agence self.account -- le compte numéro 889988 self.account['889988']
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.
Dans une expression OCL rattachée à un diagramme UML possible d'utiliser :
Notion de paquetage :
package Package::SubPackage context ... ... endpackage
context Person::isMale() : Boolean body : gender = Gender::male
Existe encore en UML2 ? Sorte de classe statique
Date::now
Aussi types Collection, Bag, Set, Sequence.
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.
Bien sûr toutes sans effet de bord...
etc, etc
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()
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
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
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
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