4. Chapitre 4 Langage de contraintes objet (Object Constraint Langage : OCL)▲
4-1. Expression des contraintes en UML▲
4-1-1. Introduction▲
Nous avons déjà vu comment exprimer certaines formes de contraintes avec UML :
- les attributs dans les classes, les différents types de relations entre classes (généralisation, association, agrégation, composition, dépendance), la cardinalité et la navigabilité des propriétés structurelles, etc. ;
- typage des propriétés, etc. ;
- les contraintes de visibilité, les méthodes et classes abstraites (contrainte abstract), etc.
Dans la pratique, toutes ces contraintes sont très utiles, mais se révèlent insuffisantes. Toutefois, UML permet de spécifier explicitement des contraintes particulières sur des éléments de modèle.
4-1-2. Écriture des contraintes▲
Une contrainte constitue une condition ou une restriction sémantique exprimée sous forme d'instruction dans un langage textuel qui peut être naturel ou formel. En général, une contrainte peut être attachée à n'importe quel élément de modèle ou liste d'éléments de modèle. Une contrainte désigne une restriction qui doit être appliquée par une implémentation correcte du système.
On représente une contrainte sous la forme d'une chaîne de texte placée entre accolades ({}). La chaîne constitue le corps écrit dans un langage de contrainte qui peut être :
- naturel ;
- dédié, comme OCL ;
- ou encore directement issu d'un langage de programmation.
Si une contrainte possède un nom, on présente celui-ci sous forme d'une chaîne suivie d'un double point (:), le tout précédant le texte de la contrainte.
4-1-3. Représentation des contraintes et contraintes prédéfinies ▲
Sur les deux diagrammes du haut, la contrainte porte sur un attribut qui doit être positif. En bas à gauche, la contrainte {frozen} précise que le nombre de roues d'un véhicule ne peut pas varier. Au milieu, la contrainte {subset} précise que le président est également un membre du comité. Enfin, en bas à droite, la contrainte {xor} (ou exclusif) précise que les employés de l'hôtel n'ont pas le droit de prendre une chambre dans ce même hôtel.
UML permet d'associer une contrainte à un, ou plusieurs, élément(s) de modèle de différentes façons (cf. figure 4.1) :
- en plaçant directement la contrainte à côté d'une propriété ou d'une opération dans un classeur ;
- en ajoutant une note associée à l'élément à contraindre ;
- en plaçant la contrainte à proximité de l'élément à contraindre, comme une extrémité d'association par exemple ;
- en plaçant la contrainte sur une flèche en pointillés joignant les deux éléments de modèle à contraindre ensemble, la direction de la flèche constituant une information pertinente au sein de la contrainte ;
- en plaçant la contrainte sur un trait en pointillés joignant les deux éléments de modèle à contraindre ensemble dans le cas où la contrainte est bijective ;
- en utilisant une note reliée, par des traits en pointillés, à chacun des éléments de modèle, subissant la contrainte commune, quand cette contrainte s'applique sur plus de deux éléments de modèle.
Nous venons de voir, au travers des exemples de la figure 4.1, quelques contraintes prédéfinies ({frozen}, {subset} et {xor}). Le diagramme de la figure 4.2 en introduit deux nouvelles : {ordered} et {addOnly}. La liste est encore longue, mais le pouvoir expressif de ces contraintes reste insuffisant comme nous le verrons dans la section 4.2.2Illustration par l'exemple . Le langage de contraintes objet OCL apporte une solution élégante à cette insuffisance.
4-2. Intérêt d'OCL▲
4-2-1. OCL - Introduction▲
4-2-1-a. QuesacOCL ?▲
C'est avec OCL (Object Constraint Language) qu'UML formalise l'expression des contraintes. Il s'agit donc d'un langage formel d'expression de contraintes bien adapté aux diagrammes d'UML, et en particulier au diagramme de classes.
OCL existe depuis la version 1.1 d'UML et est une contribution d'IBM. OCL fait partie intégrante de la norme UML depuis la version 1.3 d'UML. Dans le cadre d'UML 2.0, les spécifications du langage OCL figurent dans un document indépendant de la norme d'UML, décrivant en détail la syntaxe formelle et la façon d'utiliser ce langage.
OCL peut s'appliquer sur la plupart des diagrammes d'UML et permet de spécifier des contraintes sur l'état d'un objet ou d'un ensemble d'objets comme :
- des invariants sur des classes ;
- des préconditions et des postconditions à l'exécution d'opérations :
- les préconditions doivent être vérifiées avant l'exécution,
- les postconditions doivent être vérifiées après l'exécution ;
- des gardes sur des transitions de diagrammes d'états-transitions ou des messages de diagrammes d'interaction ;
- des ensembles d'objets destinataires pour un envoi de message ;
- des attributs dérivés, etc.
4-2-1-b. Pourquoi OCL ?▲
Nous avons dit que les contraintes pouvaient être écrites en langage naturel, alors pourquoi s'embarrasser du langage OCL ? L'intérêt du langage naturel est qu'il est simple à mettre en œuvre et compréhensible par tous. Par contre (et comme toujours), il est ambigu et imprécis, il rend difficile l'expression des contraintes complexes et ne facilite pas les références à d'autres éléments (autres que celui sur lequel porte la contrainte) du modèle.
OCL est un langage formel volontairement simple d'accès. Il possède une grammaire élémentaire (OCL peut être interprété par des outils) que nous décrirons dans les sections 4.3Typologie des contraintes OCL à 4.6Opérations sur les collections. OCL représente, en fait, un juste milieu entre le langage naturel et un langage très technique (langage mathématique, informatique…). Il permet ainsi de limiter les ambiguïtés, tout en restant accessible.
4-2-2. Illustration par l'exemple ▲
4-2-2-a. Mise en situation▲
Plaçons-nous dans le contexte d'une application bancaire. Il nous faut donc gérer :
- des comptes bancaires ;
- des clients ;
- et des banques.
De plus, on aimerait intégrer les contraintes suivantes dans notre modèle :
- un compte doit avoir un solde toujours positif ;
- un client peut posséder plusieurs comptes ;
- une personne peut être cliente de plusieurs banques ;
- un client d'une banque possède au moins un compte dans cette banque ;
- un compte appartient forcément à un client ;
- une banque gère plusieurs comptes ;
- une banque possède plusieurs clients.
4-2-2-b. Diagramme de classes▲
La figure 4.3 montre un diagramme de classes correspondant à la problématique que nous venons de décrire.
Un premier problème apparaît immédiatement : rien ne spécifie, dans ce diagramme, que le solde du client doit toujours être positif. Pour résoudre le problème, on peut simplement ajouter une note précisant cette contrainte ({solde > 0}), comme le montre la figure 4.4.
Cependant, d'autres problèmes subsistent. La figure 4.5 montre un diagramme d'objets valide vis-à-vis du diagramme de classes de la figure 4.4 et également valide vis-à-vis de la spécification du problème. Par contre, la figure 4.6 montre un diagramme d'objets valide vis-à-vis du diagramme de classes de la figure 4.4 mais ne respectant pas la spécification du problème. En effet, ce diagramme d'objets montre une personne (P1) ayant un compte dans une banque sans en être client. Ce diagramme montre également un client (P2) d'une banque n'y possédant pas de compte.
- context Compte
- inv : solde > 0
- context Compte :: débiter(somme : int)
- pre : somme > 0
- post : solde = solde@pre - somme
- context Compte
- inv : banque.clients -> includes (propriétaire)
Le langage OCL est particulièrement adapté à la spécification de ce type de contrainte. La figure 4.7 montre le diagramme de classes de notre application bancaire accompagné des contraintes OCL adaptées à la spécification du problème.
Faites bien attention au fait qu'une expression OCL décrit une contrainte à respecter et ne décrit absolument pas l'implémentation d'une méthode.
4-3. Typologie des contraintes OCL▲
4-3-1. Diagramme support des exemples illustratifs▲
Le diagramme de la figure 4.8 modélise des personnes, leurs liens de parenté (enfant/parent et mari/femme) et le poste éventuel de ces personnes dans une société. Ce diagramme nous servira de support aux différents exemples de contraintes que nous donnerons, à titre d'illustration, dans les sections qui suivent (4.3Typologie des contraintes OCL à 4.7Exemples de contraintes).
Ce diagramme introduit un nouveau type de classeur, stéréotypé « enumeration », permettant de définir une énumération. Une énumération est un type de donnée UML, possédant un nom, et utilisé pour énumérer un ensemble de littéraux correspondant à toutes les valeurs possibles que peut prendre une expression de ce type. Un type énuméré est défini par un classeur possédant le stéréotype « enumeration » comme représenté sur la figure 4.9.
4-3-2. Contexte (context)▲
Une contrainte est toujours associée à un élément de modèle. C'est cet élément qui constitue le contexte de la contrainte. Il existe deux manières pour spécifier le contexte d'une contrainte OCL :
- en écrivant la contrainte entre accolades ({}) dans une note (comme nous l'avons fait sur la figure 4.4). L'élément pointé par la note est alors le contexte de la contrainte ;
- en utilisant le mot-clef context dans un document accompagnant le diagramme (comme nous l'avons fait sur la figure 4.7).
4-3-2-a. Syntaxe▲
context <élément>
<élément> peut être une classe, une opération, etc. Pour faire référence à un élément op (comme un opération) d'un classeur C (comme une classe), ou d'un paquetage… il faut utiliser les :: comme séparateur (comme C::op).
4-3-2-b. Exemple▲
Le contexte est la classe Compte :
- context Compte
Le contexte est l'opération getSolde() de la classe Compte :
- context Compte::getSolde()
4-3-3. Invariants (inv)▲
Un invariant exprime une contrainte prédicative sur un objet, ou un groupe d'objets, qui doit être respectée en permanence.
4-3-3-a. Syntaxe▲
inv : <expression_logique>
<expression_logique> est une expression logique qui doit toujours être vraie.
4-3-3-b. Exemple▲
Le solde d'un compte doit toujours être positif.
- context Compte
- inv : solde > 0
Les femmes (au sens de l'association) des personnes doivent être des femmes (au sens du genre).
- context Personne
- inv : femme->forAll(genre=Genre::femme)
self est décrit section 4.5.1Accès aux attributs et aux opérations (self) et forAll() section 4.6.3Opération sur les éléments d'une collection.
4-3-4. Préconditions et postconditions (pre, post)▲
Une précondition (respectivement une postcondition) permet de spécifier une contrainte prédicative qui doit être vérifiée avant (respectivement après) l'appel d'une opération.
Dans l'expression de la contrainte de la postcondition, deux éléments particuliers sont utilisables :
- l'attribut result qui désigne la valeur retournée par l'opération ;
- et <nom_attribut>@pre qui désigne la valeur de l'attribut <nom_attribut> avant l'appel de l'opération.
4-3-4-a. Syntaxe▲
- Précondition : Sélectionnez
pre : <expression_logique>
- Postcondition : Sélectionnez
post : <expression_logique>
<expression_logique> est une expression logique qui doit toujours être vraie.
4-3-4-b. Exemple▲
Concernant la méthode débiter de la classe Compte, la somme à débiter doit être positive pour que l'appel de l'opération soit valide et, après l'exécution de l'opération, l'attribut solde doit avoir pour valeur la différence de sa valeur avant l'appel et de la somme passée en paramètre.
- context Compte::débiter(somme : Real)
- pre : somme > 0
- post : solde = solde@pre - somme
Le résultat de l'appel de l'opération getSolde doit être égal à l'attribut solde.
- context Compte::getSolde() : Real
- post : result = solde
Même si cela peut sembler être le cas dans ces exemples, nous n'avons pas décrit comment l'opération est réalisée, mais seulement les contraintes sur l'état avant et après son exécution.
4-3-5. Résultat d'une méthode (body)▲
Ce type de contrainte permet de définir directement le résultat d'une opération.
4-3-5-a. Syntaxe▲
body : <requête>
<requête> est une expression qui retourne un résultat dont le type doit être compatible avec le type du résultat de l'opération désignée par le contexte.
4-3-5-b. Exemple▲
Voici une autre solution au deuxième exemple de la section 4.3.4Préconditions et postconditions (pre, post) : le résultat de l'appel de l'opération getSolde doit être égal à l'attribut solde.
- context Compte::getSolde() : Real
- body : solde
4-3-6. Définition d'attributs et de méthodes (def et let…in)▲
Parfois, une sous-expression est utilisée plusieurs fois dans une expression. let permet de déclarer et de définir la valeur (i.e. initialiser) d'un attribut qui pourra être utilisé dans l'expression qui suit le in.
def est un type de contrainte qui permet de déclarer et de définir la valeur d'attributs comme la séquence let…in. def permet également de déclarer et de définir la valeur retournée par une opération interne à la contrainte.
4-3-6-a. Syntaxe de let…in▲
let <déclaration> = <requête> in <expression>
Un nouvel attribut déclaré dans <déclaration> aura la valeur retournée par l'expression <requête> dans toute l'expression <expression>.
Reportez-vous à la section 4.7Exemples de contraintes pour un exemple d'utilisation.
4-3-6-b. Syntaxe de def▲
def : <déclaration> = <requête>
<déclaration> peut correspondre à la déclaration d'un attribut ou d'une méthode. <requête> est une expression qui retourne un résultat dont le type doit être compatible avec le type de l'attribut, ou de la méthode, déclaré dans <déclaration>. Dans le cas où il s'agit d'une méthode, <requête> peut utiliser les paramètres spécifiés dans la déclaration de la méthode.
4-3-6-c. Exemple▲
Pour imposer qu'une personne majeure doit avoir de l'argent, on peut écrire indifféremment :
- context Personne
- inv : let argent=compte.solde->sum() in age>=18 implies argent>0
- context Personne
- def : argent : int = compte.solde->sum()
- context Personne
inv : age>=18 implies argent>0
sum() est décrit section 4.6.2Opérations de base sur les collections.
4-3-7. Initialisation (init) et évolution des attributs (derive)▲
Le type de contrainte init permet de préciser la valeur initiale d'un attribut ou d'une terminaison d'association.
Les diagrammes d'UML définissent parfois des attributs ou des associations dérivées. La valeur de tels éléments est toujours déterminée en fonctions d'autres éléments du diagramme. Le type de contrainte derive permet de préciser comment la valeur de ce type d'élément évolue.
Notez bien la différence entre ces deux types de contraintes. La contrainte derive impose une contrainte perpétuelle : l'élément dérivé doit toujours avoir la valeur imposée par l'expression de la contrainte derive. D'un autre côté, la contrainte init ne s'applique qu'au moment de la création d'une instance précisée par le contexte de la contrainte. Ensuite, la valeur de l'élément peut fluctuer indépendamment de la contrainte init.
4-3-7-a. Syntaxe▲
init : <requête>
derive : <requête>
4-3-7-b. Exemple▲
Quand on crée une personne, la valeur initiale de l'attribut marié est faux et la personne ne possède pas d'employeur :
- context Personne::marié : Boolean
- init : false
context Personne::employeur : Set(Société) - init : Set{}
Les collections (dont Set est une instance) sont décrites section 4.4.4Collections. Set{} correspond à un ensemble vide.
L'âge d'une personne est la différence entre la date courante et la date de naissance de la personne :
- context Personne::age : Integer
- derive : date_de_naissance - Date::current()
On suppose ici que le type Date possède une méthode de classe permettant de connaître la date courante et que l'opération moins (-) entre deux dates est bien définie et retourne un nombre d'années.
4-4. Types et opérations utilisables dans les expressions OCL▲
4-4-1. Types et opérateurs prédéfinis▲
Le langage OCL possède un certain nombre de types prédéfinis et d'opérations prédéfinies sur ces types. Ces types et ces opérations sont utilisables dans n'importe quelle contrainte et sont indépendants du modèle auquel sont rattachées ces contraintes.
Le tableau 4.1 donne un aperçu des types et opérations prédéfinis dans les contraintes OCL. Les tableaux 4.2 et 4.3 rappellent les conventions d'interprétation des opérateurs logiques.
L'opérateur logique if-then-else-endif est un peu particulier. Sa syntaxe est la suivante :
if <expression_logique_0>
then <expression_logique_1>
else <expression_logique_2>
endif
Cet opérateur s'interprète de la façon suivante : si <expression_logique_0> est vrai, alors la valeur de vérité de l'expression est celle de <expression_logique_1>, sinon, c'est celle de <expression_logique_2>.
Type | Exemples de valeurs | Opérateurs |
Boolean | true ; false | and ; or ; xor ; not ; implies ; if-then-else-endif ; … |
Integer | 1 ; −5 ; 2 ; 34 ; 26524 ; … | * ; + ; − ; / ; abs() ; … |
Real | 1,5 ; 3,14 ; … | * ; + ; − ; / ; abs() ; floor() ; … |
String | "To be or not to be …" | concat() ; size() ; substring() ; … |
E1 | E2 | P1 and P2 | P1 or P2 | P1 xor P2 | P1 implies P2 |
VRAI | VRAI | VRAI | VRAI | FAUX | VRAI |
VRAI | FAUX | FAUX | VRAI | VRAI | FAUX |
FAUX | VRAI | FAUX | VRAI | VRAI | VRAI |
FAUX | FAUX | FAUX | FAUX | FAUX | VRAI |
expression | not expression |
VRAI | FAUX |
FAUX | VRAI |
4-4-2. Types du modèle UML▲
Toute expression OCL est écrite dans le contexte d'un modèle UML donné. Bien entendu, tous les classeurs de ce modèle sont des types dans les expressions OCL attachées à ce modèle.
Dans la section 4.3.1Diagramme support des exemples illustratifs, nous avons introduit le type énuméré. Une contrainte OCL peut référencer une valeur de ce type de la manière suivante :
<nom_type_enuméré>::valeur
Par exemple, la classe Personne possède un attribut genre de type Genre. On peut donc écrire la contrainte :
context Personne
inv : genre = Genre::femme
Dans ce cas, toutes les personnes doivent être des femmes.
4-4-3. OCL est un langage typé▲
OCL est un langage typé dont les types sont organisés sous forme de hiérarchie. Cette hiérarchie détermine comment différents types peuvent être combinés. Par exemple, il est impossible de comparer un booléen (Boolean) avec un entier (Integer) ou une chaîne de caractères (String). Par contre, il est possible de comparer un entier (Integer) et un réel (Real), car le type entier est un sous-type du type réel dans la hiérarchie des types OCL. Bien entendu, la hiérarchie des types du modèle UML est donnée par la relation de généralisation entre les classeurs du modèle UML.
4-4-4. Collections▲
OCL définit également la notion d'ensemble sous le terme générique de collection (collection en anglais). Il existe plusieurs sous-types du type abstrait Collection :
Ensemble ( Set ) :
- collection non ordonnée d'éléments uniques (i.e. pas d'élément en double) ;
Ensemble ordonné ( OrderedSet ) :
- collection ordonnée d'éléments uniques ;
Sac ( Bag ) :
- collection non ordonnée d'éléments identifiables (i.e. comme un ensemble, mais pouvant comporter des doublons) ;
Séquence ( Sequence ) :
- collection ordonnée d'éléments identifiables.
Jusqu'à UML 2.0 exclu, les collections étaient toujours plates : une collection ne pouvait pas posséder des collections comme éléments. Cette restriction n'existe plus à partir d'UML 2.0.
4-5. Accès aux caractéristiques et aux objets▲
Dans une contrainte OCL associée à un objet, il est possible d'accéder aux caractéristiques (attributs, opérations et terminaison d'association) de cet objet, et donc, d'accéder de manière transitive à tous les objets (et leurs caractéristiques) avec lesquels il est en relation.
4-5-1. Accès aux attributs et aux opérations (self)▲
Pour faire référence à un attribut ou une opération de l'objet désigné par le contexte, il suffit d'utiliser le nom de cet élément. L'objet désigné par le contexte est également accessible par l'expression self. On peut donc également utiliser la notation pointée : self.<propriété>.
Une opération peut avoir des paramètres, il faut alors les préciser entre les parenthèses de l'opération.
Lorsque la multiplicité d'un attribut, de type T, n'est pas 1 (donc s'il s'agit d'un tableau), la référence à cet attribut est du type ensemble (i.e. Set(T)).
Par exemple, dans le contexte de la classe Compte, on peut utiliser les expressions suivantes :
- solde ;
- self.solde ;
- getSolde() ;
- self.getSolde() ;
- débiter(1000) ;
- self.débiter(1000).
Dans l'exemple précédent, le résultat de l'expression self.débiter(1000) est un singleton du type Real. Mais une opération peut comporter des paramètres définis en sortie ou en entrée/sortie. Dans ce cas, le résultat sera un tuple contenant tous les paramètres définis en sortie ou en entrée/sortie. Par exemple, imaginons une opération dont la déclaration serait operation(out param_out : Integer):Real possédant un paramètre défini en sortie param_out. Dans ce cas, le résultat de l'expression operation(paramètre) est un tuple de la forme (param_out : Integer, result : Real). On peut accéder aux valeurs de ce tuple de la façon suivante :
operation(paramètre).param_out
operation(paramètre).result
4-5-2. Navigation via une association▲
Pour faire référence à un objet, ou un groupe d'objets, en association avec l'objet désigné par le contexte, il suffit d'utiliser le nom de la classe associée (en minuscules) ou le nom du rôle d'association du côté de cette classe. Quand c'est possible, il est préférable d'utiliser le nom de rôle de l'association du côté de l'objet auquel on désire faire référence. C'est indispensable s'il existe plusieurs associations entre l'objet désigné par le contexte et l'objet auquel on désire accéder, ou si l'association empruntée est réflexive.
Le type du résultat dépend de la propriété structurelle empruntée pour accéder à l'objet référencé, et plus précisément de la multiplicité du côté de l'objet référencé, et du type de l'objet référencé proprement dit. Si on appelle X la classe de l'objet référencé, dans le cas d'une multiplicité de :
-
1, le type du résultat est X (ex. : ) ;
-
* ou 0..n…, le type du résultat est Set(X) (ex. : ) ;
-
* ou 0..n…, et s'il y a en plus une contrainte {ordered}, le type du résultat est OrderedSet(X) (ex. : ).
Emprunter une seule propriété structurelle peut produire un résultat du type Set (ou OrderedSet). Emprunter plusieurs propriétés structurelles peut produire un résultat du type Bag (ou Sequence).
Par exemple, dans le contexte de la classe Société (context Société) :
- directeur désigne le directeur de la société (résultat de type Personne) ;
- employé désigne l'ensemble des employés de la société (résultat de type Set(Personne)) ;
- employé.compte désigne l'ensemble des comptes de tous les employés de la société (résultat de type Bag(Compte)) ;
- employé.date_de_naissance désigne l'ensemble des dates de naissance des employés de la société (résultat de type Bag(Date)).
4-5-3. Navigation via une association qualifiée▲
Une association qualifiée (cf. section3.3.6Qualification) utilise un ou plusieurs qualificatifs pour sélectionner des instances de la classe cible de l'association. Pour emprunter une telle association, il est possible de spécifier les valeurs, ou les instances, des qualificatifs en utilisant des crochets ([]).
Plaçons-nous dans le cadre du diagramme de la figure 4.10. Dans le contexte de banque (context Banque), pour faire référence au nom des clients dont le compte porte le numéro 19503800 il faut écrire :
self.client[19503800].nom
Dans le cas où il y a plusieurs qualificatifs, il faut séparer chacune des valeurs par une virgule en respectant l'ordre des qualificatifs du diagramme UML. Il n'est pas possible de ne préciser la valeur que de certains qualificatifs en en laissant d'autres non définis. Par contre, il est possible de ne préciser aucune valeur de qualificatif :
self.client.nom
Dans ce cas, le résultat sera l'ensemble des noms de tous les clients de la banque.
Ainsi, si on ne précise pas la valeur des qualificatifs en empruntant une association qualifiée, tout se passe comme si l'association n'était pas qualifiée. Dans ce cas, faites attention à la cardinalité de la cible qui change quand l'association n'est plus qualifiée (cf. section 3.3.6Qualification).
4-5-4. Navigation vers une classe association▲
Pour naviguer vers une classe association, il faut utiliser la notation pointée classique en précisant le nom de la classe association en minuscules. Par exemple, dans le contexte de la classe Société (context Société), pour accéder au salaire de tous les employés, il faut écrire :
self.poste.salaire
Cependant, dans le cas où l'association est réflexive (c'est le cas de la classe association Mariage), il faut en plus préciser par quelle extrémité il faut emprunter l'association. Pour cela, on précise le nom de rôle de l'une des extrémités de l'association entre crochets ([]) derrière le nom de la classe association. Par exemple, dans le contexte de la classe Personne (context Personne), pour accéder à la date de mariage de toutes les femmes, il faut écrire :
self.mariage[femme].date
4-5-5. Navigation depuis une classe association▲
Il est tout à fait possible de naviguer directement depuis une classe association vers une classe participante.
Exemple :
context Poste
inv : self.employé.age > 21
Par définition même d'une classe association, naviguer depuis une classe association vers une classe participante produit toujours comme résultat un objet unique. Par exemple, l'expression self.employé.age de l'exemple précédant produit bien un singleton.
4-5-6. Accéder à une caractéristique redéfinie (oclAsType())▲
Quand une caractéristique définie dans une classe parente est redéfinie dans une sous-classe associée, la caractéristique de la classe parente reste accessible dans la sous-classe en utilisant l'expression oclAsType().
Supposons une classe B héritant d'une classe A et une propriété p1 définie dans les deux classes. Dans le contexte de la classe B (context B), pour accéder à la propriété p1 de B, on écrit simplement :
self.p1
et pour accéder à la propriété p1 de A (toujours dans le contexte de B), il faut écrire :
self.oclAsType(A).p1
4-5-7. Opérations prédéfinies sur tous les objets▲
L'opération oclAsType, que nous venons de décrire (section 4.5.6Accéder à une caractéristique redéfinie (oclAsType())), est une opération prédéfinie dans le langage OCL qui peut être appliquée à tout objet. Le langage OCL en propose plusieurs :
- oclIsTypeOf (t : OclType) : Boolean
- oclIsKindOf (t : OclType) : Boolean
- oclInState (s : OclState) : Boolean
- oclIsNew () : Boolean
- oclAsType (t : OclType) : instance of OclType
4-5-7-a. Opération oclIsTypeOf▲
oclIsTypeOf retourne vrai si le type de l'objet au titre duquel cette opération est invoquée est exactement le même que le type t passé en paramètre.
Par exemple, dans le contexte de Société, l'expression directeur.oclIsTypeOf(Personne) est vraie tandis que l'expression self.oclIsTypeOf(Personne) est fausse.
4-5-7-b. Opération oclIsKindOf▲
oclIsKindOf permet de déterminer si le type t passé en paramètre correspond exactement au type ou à un type parent du type de l'objet au titre duquel cette opération est invoquée.
Par exemple, supposons une classe B héritant d'une classe A :
- dans le contexte de B, l'expression self.oclIsKindOf(B) est vraie ;
- toujours dans le contexte de B, l'expression self.oclIsKindOf(A) est vraie ;
- mais dans le contexte de A, l'expression self.oclIsKindOf(B) est fausse.
4-5-7-c. Opération oclIsNew▲
L'opération oclIsNew doit être utilisée dans une postcondition. Elle est vraie quand l'objet au titre duquel elle est invoquée est créé pendant l'opération (i.e. l'objet n'existait pas au moment des préconditions).
4-5-7-d. Opération oclInState▲
Cette opération est utilisée dans un diagramme d'états-transitions (cf. section 5Chapitre 5 Diagramme d'états-transitions (State machine diagram)). Elle est vraie si l'objet décrit par le diagramme d'états-transitions est dans l'état s passé en paramètre. Les valeurs possibles du paramètre s sont les noms des états du diagramme d'états-transitions. On peut faire référence à un état imbriqué en utilisant des «::» (par exemple, pour faire référence à un état B imbriqué dans un état A, on écrit : A::B).
4-5-8. Opération sur les classes ▲
Toutes les opérations que nous avons décrites jusqu'ici s'appliquaient sur des instances de classe. Cependant, OCL permet également d'accéder à des caractéristiques de classe (celles qui sont soulignées dans un diagramme de classes). Pour cela, on utilise le nom qualifié de la classe suivi d'un point puis du nom de la propriété ou de l'opération : <nom_qualifié>.<propriété>.
Le langage OCL dispose également d'une opération prédéfinie sur les classes, les interfaces et les énumérations (allInstances) qui retourne l'ensemble (Set) de toutes les instances du type au titre duquel elle est invoquée, au moment où l'expression est évaluée. Par exemple, pour désigner l'ensemble des instances de la classe personne (type set(Personne)) on écrit :
Personne.allInstances()
4-6. Opérations sur les collections▲
4-6-1. Introduction : « . », « -> », « :: » et self▲
Comme nous l'avons vu dans la section précédente (4.5Accès aux caractéristiques et aux objets), pour accéder aux caractéristiques (attributs, terminaisons d'associations, opérations) d'un objet, OCL utilise la notation pointée : <objet>.<propriété>. Cependant, de nombreuses expressions ne produisent pas comme résultat un objet, mais une collection. Le langage OCL propose plusieurs opérations de base sur les collections. Pour accéder ce type d'opération, il faut, utiliser non pas un point, mais une flèche : <collection>-><opération>. Enfin, rappelons que pour désigner un élément dans un élément englobant on utilise les «::» (cf. Section 4.3.2Contexte (context) et 4.5.7Opérations prédéfinies sur tous les objets par exemple). En résumé :
- permet de désigner un élément (comme une opération) dans un élément englobant (comme un classeur ou un paquetage) ;
- permet d'accéder à une caractéristique (attributs, terminaisons d'associations, opérations) d'un objet ;
- permet d'accéder à une caractéristique d'une collection.
Nous avons dit dans la section 4.5.1Accès aux attributs et aux opérations (self) que l'objet désigné par le contexte est également accessible par l'expression self. self n'est pas uniquement utilisé pour désigner le contexte d'une contrainte dans une expression, mais également pour désigner le contexte d'une sous-expression dans le texte (en langage naturel). Ainsi, lorsque l'on utilise self pour une opération <opération>, c'est pour désigner l'objet (comme une collection par exemple) sur lequel porte l'opération. Cet objet peut être le résultat d'une opération intermédiaire comme l'évaluation de l'expression <expression> précédant l'opération <opération> dans l'expression complète : <expression>.<opération>.
4-6-2. Opérations de base sur les collections▲
Nous ne décrirons pas toutes les opérations sur les collections et ses sous-types (ensemble…) dans cette section. Référez-vous à la documentation officielle [19] pour plus d'exhaustivité.
4-6-2-a. Opérations de base sur les collections▲
Nous décrivons ici quelques opérations de base sur les collections que propose le langage OCL.
- retourne le nombre d'éléments (la cardinalité) de self.
- vrai si self contient l'objet objet.
- vrai si self ne contient pas l'objet objet.
- retourne le nombre d'occurrences de objet dans self.
- vrai si self contient tous les éléments de la collection c.
- vrai si self ne contient aucun élément de la collection c.
- vrai si self est vide.
- vrai si self n'est pas vide.
- retourne la somme des éléments de self. Les éléments de self doivent supporter l'opérateur somme (+) et le type du résultat dépend du type des éléments.
- le résultat est la collection de Tuples correspondant au produit cartésien de self (de type Collection(T)) par c2.
4-6-2-b. Opérations de base sur les ensembles (Set)▲
Nous décrivons ici quelques opérations de base sur les ensembles (type Set) que propose le langage OCL.
- retourne l'union de self et set.
- retourne l'union de self et bag.
- vrai si self et set contiennent les mêmes éléments.
- intersection entre self et set.
- intersection entre self et bag. (12)
- Le résultat contient tous les éléments de self plus l'objet objet.
- Le résultat contient tous les éléments de self sans l'objet objet.
- Le résultat contient tous les éléments de self sans ceux de set.
- permet de convertir self du type Set(T) en OrderedSet(T).
- permet de convertir self du type Set(T) en Sequence(T).
- permet de convertir self du type Set(T) en Bag(T).
Les sacs (type Bag) disposent d'opérations analogues.
4-6-2-c. Exemples▲
- Une société a au moins un employé :
context Société inv : self.employé->notEmpty() - Une société possède exactement un directeur :
context Société inv : self.directeur->size()=1 - Le directeur est également un employé :
context Société inv : self.employé->includes(self.directeur)
4-6-3. Opération sur les éléments d'une collection▲
4-6-3-a. Syntaxe générale▲
La syntaxe d'une opération portant sur les éléments d'une collection est la suivante :
<collection> -> <opération>( <expression> )
Dans tous les cas, l'expression <expression> est évaluée pour chacun des éléments de la collection <collection>. L'expression <expression> porte sur les caractéristiques des éléments en les citant directement par leur nom. Le résultat dépend de l'opération <opération>.
Parfois, dans l'expression <expression>, il est préférable de faire référence aux caractéristiques de l'élément courant en utilisant la notation pointée : <élément>.<propriété>. Pour cela, on doit utiliser la syntaxe suivante :
<collection> -> <opération>( <élément> | <expression> )
<élément> joue alors un rôle d'itérateur et sert de référence à l'élément courant dans l'expression <expression>.
Il est également possible, afin d'être plus explicite, de préciser le type de cet élément :
<collection> -> <opération>( <élément> : <Type> | <expression> )
La syntaxe générale d'une opération portant sur les éléments d'une collection est donc la suivante :
<collection> -> <opération>( [ <élément> [ : <Type> ] | ] <expression> )
4-6-3-b. Opération select et reject▲
Ces deux opérations permettent de générer une sous-collection en filtrant les éléments de la collection self. Leur syntaxe est la suivante :
select( [ <élément> [ : <Type> ] | ] <expression_logique> )
reject( [ <élément> [ : <Type> ] | ] <expression_logique> )
- permet de générer une sous-collection de self ne contenant que des éléments qui satisfont l'expression logique <expression_logique>.
- permet de générer une sous-collection contenant tous les éléments de self excepté ceux qui satisfont l'expression logique <expression_logique>.
Par exemple, pour écrire une contrainte imposant que toute société doit posséder, parmi ses employés, au moins une personne de plus de 50 ans, on peut écrire indifféremment :
- context Société
inv: self.employé->select(age > 50)->notEmpty() - context Société
inv: self.employé->select(individu | individu.age > 50)->notEmpty() - context Société
inv: self.employé->select(individu : Personne | individu.age > 50)->notEmpty()
4-6-3-c. Opération forAll et exists▲
Ces deux opérations permettent de représenter le quantificateur universel (∀) et le quantificateur existentiel (∃). Le résultat de ces opérations est donc du type Boolean. Leur syntaxe est la suivante :
forAll( [ <élément> [ : <Type> ] | ] <expression_logique> )
exists( [ <élément> [ : <Type> ] | ] <expression_logique> )
- permet d'écrire une expression logique vraie si l'expression <expression_logique> est vraie pour tous les éléments de self.
- permet d'écrire une expression logique vraie si l'expression <expression_logique> est vraie pour au moins un élément de self.
Par exemple, pour écrire une contrainte imposant que toute société doit posséder, parmi ses employés, au moins une personne de plus de 50 ans, on peut écrire :
- context Société
- inv: self.employé->exists(age > 50)
L'opération forAll possède une variante étendue possédant plus d'un itérateur. Dans ce cas, chacun des itérateurs parcourra l'ensemble de la collection. Concrètement, une opération forAll comportant deux itérateurs est équivalente à une opération forAll n'en comportant qu'un, mais réalisée sur le produit cartésien de self par lui-même.
Par exemple, imposer qu'il n'existe pas deux instances de la classe Personne pour lesquelles l'attribut nom a la même valeur, c'est-à-dire pour imposer que deux personnes différentes ont un nom différent, on peut écrire indifféremment :
- context Personne
inv: Personne.allInstances()->forAll(p1, p2 | p1 <> p2 implies p1.nom <> p2.nom) - context Personne
inv: (Personne.allInstances().product(Personne.allInstances()))
->forAll(tuple | tuple.first <> tuple.second implies tuple.first.nom <> tuple.second.nom)
4-6-3-d. Opération collect▲
Cette opération permet de construire une nouvelle collection en utilisant la collection self. La nouvelle collection construite possède le même nombre d'éléments que la collection self, mais le type de ces éléments est généralement différent. La syntaxe de l'opérateur collect est la suivante :
collect( [ <élément> [ : <Type> ] | ] <expression> )
Pour chaque élément de la collection self, l'opérateur collect évalue l'expression <expression> sur cet élément et ajoute le résultat dans la collection générée.
Par exemple, pour définir la collection des dates de naissance des employés d'une société, il faut écrire, dans le contexte de la classe Société :
self.employé->collect(date_de_naissance)
Puisque, toujours dans le contexte de la classe Société, l'expression self.employé->collect(date_de_naissance)->size() = self.employé->size() est toujours vraie, il faut en conclure que le résultat d'une opération collect sur une collection du type Set n'est pas du type Set, mais du type Bag. En effet, dans le cadre de notre exemple, il y aura certainement des doublons dans les dates de naissance.
4-6-4. Règles de précédence des opérateurs ▲
Ordre de précédence pour les opérateurs par ordre de priorité décroissante :
- @pre
- «.» et «->»
- not et «-» (opérateur unaire)
- «*» et «/»
- «+» et «-»(opérateur binaire)
- if-then-else-endif
- «<», «>», «<=» et «>=»
- «=» et «<>»
- and, or et xor
- implies
Les parenthèses, « ( » et « ) », permettent de changer cet ordre.
4-7. Exemples de contraintes▲
Dans cette section, nous allons illustrer par quelques exemples l'utilisation du langage OCL. Nous restons toujours sur le diagramme de classes de la figure 4.8 représenté à nouveau sur la figure 4.11 pour des raisons de proximité.
Dans une société, le directeur est un employé, n'est pas un chômeur et doit avoir plus de 40 ans. De plus, une société possède exactement un directeur et au moins un employé.
context Société
inv :
self.directeur->size()=1 and
not(self.directeur.chômeur) and
self.directeur.age > 40 and
self.employé->includes(self.directeur)
Une personne considérée comme au chômage ne doit pas avoir des revenus supérieurs à 100 €.
context Personne
inv :
let revenus : Real = self.poste.salaire->sum() in
if chômeur then
revenus < 100
else
revenus >= 100
endif
Une personne possède au plus deux parents (référencés).
context Personne
13. inv : parent->size()<=2
Si une personne possède deux parents, l'un est une femme et l'autre un homme.
context Personne
inv :
parent->size()=2 implies
( parent->exists(genre=Genre::homme) and
parent->exists(genre=Genre::femme) )
Tous les enfants d'une personne ont bien cette personne comme parent et inversement.
context Personne
inv :
enfant->notEmpty() implies
enfant->forAll( p : Personne | p.parents->includes(self))
context Personne
inv :
parent->notEmpty() implies
parent->forAll ( p : Personne | p.enfant->includes (self))
Pour être marié, il faut avoir une femme ou un mari.
context Personne::marié
derive : self.femme->notEmpty() or self.mari->notEmpty()
Pour être marié, il faut avoir plus de 18 ans. Un homme est marié avec exactement une femme et une femme avec exactement un homme.
context Personne
inv :
self.marié implies
self.genre=Genre::homme implies (
self.femme->size()=1 and
self.femme.genre=Genre::femme)
and self.genre=Genre::femme implies (
self.mari->size()=1 and
self.mari.genre=Genre::homme)
and self.age >=18