UML 2

De l'apprentissage à la pratique


précédentsommairesuivant

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 :

Contraintes structurelles :
  • 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. ;
Contraintes de type :
  • typage des propriétés, etc. ;
Contraintes diverses :
  • 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

Image non disponible
Figure 4.1 : UML permet d'associer une contrainte à un élément de modèle de plusieurs façons.

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.

Image non disponible
Figure 4.2 : Ce diagramme exprime que : une personne est née dans un pays, et que cette association ne peut être modifiée ; une personne a visité un certain nombre de pays, dans un ordre donné, et que le nombre de pays visités ne peut que croître ; une personne aimerait encore visiter toute une liste de pays, et que cette liste est ordonnée (probablement par ordre de préférence).

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

Image non disponible
Figure 4.3 : Diagramme de classes modélisant une banque, ses clients et leurs comptes.

La figure 4.3 montre un diagramme de classes correspondant à la problématique que nous venons de décrire.

Image non disponible
Figure 4.4 : Ajout d'une contrainte sur le diagramme de la figure 4.3.

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.

Image non disponible
Figure 4.5 : Diagramme d'objets cohérent avec le diagramme de classes de la figure 4.4.
Image non disponible
Figure 4.6 : Diagramme d'objets cohérent avec le diagramme de classes de la figure 4.4, mais représentant une situation inacceptable.

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)
Image non disponible
Figure 4.7 : Exemple d'utilisation du langage de contrainte OCL sur l'exemple bancaire.

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

Image non disponible
Figure 4.8 : Diagramme de classes modélisant une entreprise et des personnes.

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).

Image non disponible
Figure 4.9 : Définition d'une énumération en utilisant un classeur.

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

 
Sélectionnez
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

 
Sélectionnez
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

 
Sélectionnez
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 letin. 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

 
Sélectionnez
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

 
Sélectionnez
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

 
Sélectionnez
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 :

 
Sélectionnez
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>. 

Tableau 4.1 : Types et opérateurs prédéfinis dans les contraintes OCL.
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() ; …
Tableau 4.2 : Interprétation des quatre connecteurs, E1 et E2 sont deux expressions logiques.
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
Tableau 4.3 : Convention d'interprétation de la négation.
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 :

 
Sélectionnez
<nom_type_enuméré>::valeur

Par exemple, la classe Personne possède un attribut genre de type Genre. On peut donc écrire la contrainte :

 
Sélectionnez
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 :

 
Sélectionnez
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. : Image non disponible) ;

  • * ou 0..n…, le type du résultat est Set(X) (ex. : Image non disponible) ;

  • * ou 0..n…, et s'il y a en plus une contrainte {ordered}, le type du résultat est OrderedSet(X) (ex. : Image non disponible).

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

Image non disponible
Figure 4.10 : Diagramme illustrant une association qualifiée entre une classe Banque et une classe Personne.

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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
self.p1

et pour accéder à la propriété p1 de A (toujours dans le contexte de B), il faut écrire :

 
Sélectionnez
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 :

 
Sélectionnez
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.

size():Integer
  • retourne le nombre d'éléments (la cardinalité) de self.
includes(objet:T):Boolean
  • vrai si self contient l'objet objet.
excludes(objet:T):Boolean
  • vrai si self ne contient pas l'objet objet.
count(objet:T):Integer
  • retourne le nombre d'occurrences de objet dans self.
includesAll(c:Collection(T)):Boolean
  • vrai si self contient tous les éléments de la collection c.
excludesAll(c:Collection(T)):Boolean
  • vrai si self ne contient aucun élément de la collection c.
isEmpty()
  • vrai si self est vide.
notEmpty()
  • vrai si self n'est pas vide.
sum():T
  • 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.
product(c2:Collection(T2)):Set(Tuple(first:T,second:T2))
  • 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.

union(set:Set(T)):Set(T)
  • retourne l'union de self et set.
union(bag:Bag(T)):Bag(T)
  • retourne l'union de self et bag.
=(set:Set(T)):Boolean
  • vrai si self et set contiennent les mêmes éléments.
intersection(set:Set(T)):Set(T)
  • intersection entre self et set.
intersection(bag:Bag(T)):Set(T)
  • intersection entre self et bag. (12)
including(objet:T):Set(T)
  • Le résultat contient tous les éléments de self plus l'objet objet.
excluding(objet:T):Set(T)
  • Le résultat contient tous les éléments de self sans l'objet objet.
-(set:Set(T)):Set(T)
  • Le résultat contient tous les éléments de self sans ceux de set.
asOrderedSet():OrderedSet(T)
  • permet de convertir self du type Set(T) en OrderedSet(T).
asSequence():Sequence(T)
  • permet de convertir self du type Set(T) en Sequence(T).
asBag():Bag(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

  1. Une société a au moins un employé :
    context Société inv : self.employé->notEmpty()
  2. Une société possède exactement un directeur :
    context Société inv : self.directeur->size()=1
  3. 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 :

 
Sélectionnez
<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 :

 
Sélectionnez
<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 :

 
Sélectionnez
<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 :

 
Sélectionnez
<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 :

 
Sélectionnez
select( [ <élément> [ : <Type> ] | ] <expression_logique> )
reject( [ <élément> [ : <Type> ] | ] <expression_logique> )
select
  • permet de générer une sous-collection de self ne contenant que des éléments qui satisfont l'expression logique <expression_logique>.
reject
  • 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 :

  1. context Société
    inv: self.employé->select(age > 50)->notEmpty()
  2. context Société
    inv: self.employé->select(individu | individu.age > 50)->notEmpty()
  3. 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 :

 
Sélectionnez
forAll( [ <élément> [ : <Type> ] | ] <expression_logique> )
exists( [ <élément> [ : <Type> ] | ] <expression_logique> )
forAll
  • permet d'écrire une expression logique vraie si l'expression <expression_logique> est vraie pour tous les éléments de self.
exists
  • 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 :

  1. context Personne
    inv: Personne.allInstances()->forAll(p1, p2 | p1 <> p2 implies p1.nom <> p2.nom)
  2. 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 :

 
Sélectionnez
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é :

 
Sélectionnez
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 :

  1. @pre
  2. «.» et «->»
  3. not et «-» (opérateur unaire)
  4. «*» et «/»
  5. «+» et «-»(opérateur binaire)
  6. if-then-else-endif
  7. «<», «>», «<=» et «>=»
  8. «=» et «<>»
  9. and, or et xor
  10. implies

Les parenthèses, « ( » et « ) », permettent de changer cet ordre.

4-7. Exemples de contraintes

Image non disponible
Figure 4.11 : Reprise du diagramme de la figure 4.8.

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é.

 
Sélectionnez
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 €.

 
Sélectionnez
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).

 
Sélectionnez
context Personne
13. inv : parent->size()<=2

Si une personne possède deux parents, l'un est une femme et l'autre un homme.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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

précédentsommairesuivant
Non, il n'y a pas d'erreur de copier/coller : réfléchissez !

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Laurent AUDIBERT. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.