Correspondance entre modèles de conception UML et le code: aspects structurels
1. Introduction
Traduire un modèle de conception UML en code source d’un langage à objets, tels que Java, Kotlin, C# ou C++, est une tâche complexe.
Tout d’abord, UML comporte plusieurs diagrammes (21, dans la version 2.5), utilisant des notations graphiques différentes, chacune représentant différents aspects du modèle.
Deuxièmement, la correspondance entre les concepts UML (classes, associations, signaux, états, opérations, etc.) et les concepts des langages à objets (classes, champs et méthodes) n’est pas triviale. Certains concepts ont exactement le même nom, par exemple, UML, Java et Ruby ont des Classes, mais signification n’est pas exactement la même pour chaque langage.
Enfin, UML manque de sémantique : il n’y a pas de règle universelle pour transposer la conception au code. Cela signifie que les concepteurs peuvent interpréter différemment le même modèle de conception.
Pour résoudre ces problèmes, le développeur a besoin d’une « stratégie d’implémentation » pour guider la traduction.
2. Stratégie d’implémentation
Une stratégie d’implémentation consiste en un ensemble de règles qui spécifient comment traduire les modèles de conception en code source. Elle se compose de plusieurs parties correspondant à différents concepts UML : composants, classes, attributs, opérations, diagrammes d’état, etc. Elle contient également une table de correspondance entre les types UML et les types du langage cible (code).
Parfois, la stratégie est accompagnée d’un profil UML spécifique (ensemble d’étiquettes et de stéréotypes), de templates de génération, de configurations, etc.
2.1. Creation d’une stratégie d’implémentation
Une stratégie d’implémentation peut contenir des règles pour les différents concepts UML (Classes, États, Activités). Comme UML est un langage très riche, contenant plus d’une centaine de concepts différents, les stratégies se restreignent souvent à un sous-ensemble contenant au moins:
-
La correspondance de Types
-
Les Classes
-
Les Attributs (mono et multivalués)
-
Les Associations (uni et bi-directionnelles)
Dans les sections suivantes, nous allons présenter différentes stratégies pour ces concepts.
2.2. Correspondance de Types
2.2.1. Les types primitifs UML
La version basique d’UML ne contient que 5 types primitifs, ce qui est assez restraint en comparaison avec des langages de programmation (à part TypeScript).
Ce choix permet à UML d’être étendu plus facilement.
En effet, il est possible d’étendre les types primitifs UML grâce au "profils",
qui peuvent contenir de nouveaux types de données (ou datatypes),
par exemple : Date, Monnaie, Numéro de téléphone, etc.
| Type | Valeurs |
|---|---|
Integer |
-1, 0, 1, 2, … |
Boolean |
true, false |
UnlimitedNatural |
0, 1, * |
String |
"to be or not to be" |
Real |
1.5, 3.14, … |
2.2.2. Table de correspondance
Une table de correspondance permet de maintenir la cohérence à l’intérieur d’un projet. Lorsque différents développeurs doivent traduire un type UML en Java, par exemple, la table de correspondance assure qu’ils utiliseront .
| UML | Java | MySQL | TypeScript |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dans cette table de correspondance, le développeur a choisi de traduire les types primitifs UML en classes-enveloppe (wrapper) Java.
Quel est l’intérêt d’avoir de types neutres, si certains projets n’ont qu’un seul langage cible ?
Il est vrai que si l’on utilise un modèle de conception pour ne générer que du code dans un même langage, l’intérêt des types neutres est limité.
Cependant, de plus en plus de projets sont développés en plusieurs langages : un pour la partie client (frontend), un autre pour la partie serveur (backend), une troisième pour la persistance, etc. Dans ce cas, les développeurs ont besoin de connaître la correspondance entre les types des différents langages.
Lorsque les développeurs ont besoin de plus précision sur les types, UML propose différents mécanismes pour affiner le domaine des valeurs de ces types : les stéréotypes, les étiquettes, des invariants OCL, etc.
| UML | Java |
|---|---|
|
|
|
|
|
|
|
|
|
|
Dans cette nouvelle table de correspondance, le développeur préfère utiliser les types de base Java.
2.3. Implémentation de classes
Il existe au moins trois approches pour traduire une classe UML en Java :
- Correspondance simple
-
pour chaque classe UML, créer une classe Java.
- Classe-Interface
-
pour chaque classe UML, créer une paire (classe, interface).
- Pattern Generation Gap
-
pour chaque classe UML, créer un triplet (interface, classe abstraite, classe concrète).
2.3.1. Correspondance simple
| Modèle de conception | Modèle d’implémentation | Code source |
|---|---|---|
|
| Oui, il est possible d’utiliser UML pour représenter des modèles d’implémentation. Le diagramme de classes UML devient alors une représentation graphie de Java (ou de C#, Kotlin, Ruby, etc.). |
La correspondance simple est suffisante dans la plupart des cas. Toutefois, elle présente au moins une limitation : il n’est pas possible de traduire l’héritage multiple, possible en UML, mais inexistante en Java.
2.3.2. Une classe UML, une paire (Classe, Interface)
Dans cette deuxième approche, chaque classe UML est traduite en une classe et une interface Java.
| Modèle de conception | Modèle d’implémentation |
|---|---|
L’avantage ici, c’est qu’il est possible de simuler l’héritage multiple grâce aux interfaces Java, puisqu’en java une classe peut réaliser (implémenter) plusieurs interfaces.
Cette approche peut s’avérer utile lorsque la classe UML doit être utilisée dans différents contextes : Data Transfert Objects (DTO), Remote Procedure Call (RPC), Persistance, Tests, etc.
2.3.3. Pattern Generative Gap
Une troisième approche de traduction de classes UML consiste à utiliser le pattern Generation Gap, où chaque classe UML est traduite en un triplet : une Interface, une classe Abstraite et une Classe Concrète.
Ce pattern est utilise dans le contexte de la génération automatique de code, lorsque l’on souhaite pouvoir modifier le code généré et en même temps, éviter que les modifications soient écrasées par les générations subséquentes.
| Modèle de conception | Modèle d’implémentation |
|---|---|
Le code source Java correspondant est le suivant :
public interface HTMLPage {
// (...)
}
public abstract class BasicHTMLPage implements HTMLPage {
// (...)
}
public class UserHTMLPage extends BasicHTMLPage {
// (...)
}
L’utilisation de Generation Gap est utile dans un contexte de génération automatique de code :
la sous-classe UserHTMLPage n’est générée qu’une seule fois et par conséquent,
le code inséré manuellement par le développeur n’est jamais écrasé.
L’inconvénient principal de cette approche est la prolifération des classes/interfaces, ce qui peut rendre difficile la navigation dans le code source.
2.3.4. Approche alternative: utilisation d’une interface commune
Les approches précédentes peuvent être enrichies par une approche alternative : l’utilisation d’une interface (ou classe abstraite) commune à toutes les classes système, ou seulement aux classes qui représentent des concepts du métier.
| Modèle de conception | Code Java |
|---|---|
|
L’utilisation d’une interface commune a plusieurs avantages, comme le fait que le développeur peur attendre que chaque classe implémente de façon cohérente les mêmes méthodes.
L’interface commune peut éventuellement être étendue avec d’autres méthodes, permettant par exemple la réflexion ;
Objet get(String fieldName)
void set(String fieldName, Object value)
void call(String methodName)
2.4. Implémentation d’attributs
2.4.1. Préambule : les attributs UML
Les attributs UML sont des propriétés structurelles typées, qui spécifient la structure de toutes les instances d’une classe (ou classificateur) donnée.
D’une certaine manière, les attributs UML correspondent aux champs de Java, aux membres de C++ ou aux propriétés TypeScript.
La différence essentiel c’est que le type des attributs UML est restreint aux types (primitifs, de données, énumérations, etc). Cela parce qu’en UML il est possible de créer des associations entre classes, ce qui n’est pas possible dans les autres langages à objet.
La classe HTMLPage contient 6 attributs :
-
titleest un attribut public (+) accessible en lecture seule -
sizeest un attribut public, dérivé (ou calculé) -
versionest attribut optionnel (multiplicité[0..1]) de visibilité paquet (~) -
contentsest attribut protégé (#) -
visibilityest un attribut privé (-), dont la valeur initiale esttrue -
tagsest un attribut privé, multivalué (multiplicité[1..5]), c’est à dire, une instance deHTMLPagepeut avoir entre 1 et 5 étiquettes (tags)
Les attributs peuvent être mono ou multivalués.
Prenons comme exemple la classe Person, qui contient trois attributs.
Interprétation des attributs :
| Attribut | Description | Exemples |
|---|---|---|
|
Attribut obligatoire monovalué |
1; 2; 99. |
|
Attribut optionnel monovalué |
null; "john"; "paul". |
|
Attribut multivalué |
{}, {"john", "paul"}, {"ringo", "george"} |
Tout comme la plupart des langages à objets, les attributs UML peuvent avoir des visibilités :
| Symbole | Visibilité |
|---|---|
|
Public |
|
Protected |
|
Package |
|
Private |
Contrairement aux propriétés structurelles des autres langages à objets, les attributs peuvent avoir des propriétés, qui peuvent donner des informations additionnelles sur chaque attribut.
| Propriété | Description |
|---|---|
|
Attribut en lecture seule |
|
Les valeurs de l’attributs sont uniques parmi les instances de la classe |
|
L’attribut redéfinie l’attribut hérité |
En UML les attributs peuvent avoir des contraintes, qui sont décrites grâce au langage OCL
OCL est un langage très riche, qui permet la description formelle d’invariants, qui peuvent ajouter plus de précision aux modèles de conception UML.
Enfin, les attributs dits dérivés peuvent être calculés à partir d’un autre attribut, ou d’un autre élément d’UML. Les attributs dérivés sont spécifiés en OCL: une expression booléenne qui spécifie la relation entre les deux attributs.
| Comme vous avez pu constater, les attributs UML sont bien plus complexes que les champs de Java. Cette différence rend la traduction d’attributs UML en Java non triviale. |
2.4.2. Implémentation d’attributs monovalués
Par la suite, nous allons décrire trois approches différentes d’implémentation d’attributes : L’approche naïve, avec des accesseurs et avec les classes enveloppe.
Approche naïve
L’approche naïve consiste à considérer qu’il existe une équivalence directe entre les concepts UML et ceux de Java :
| UML | Java |
|---|---|
Attribut |
Champ |
Visibilité
|
Visibilité
|
Attribut |
Champ |
Si l’on applique cette approche à la classe HTMLPage,
nous aurons la classe Java suivante~:
| Modèle de conception | Code source |
|---|---|
|
Les limitations de cette approche sont évidentes : elle ne permet pas la prise en compte ni des attributs dérivés (atribut size),
ni des contraintes (attribut first names).
Approche par accesseurs
L’approche par accesseurs permet d’éviter les limitations de l’approche naïve. Cette approche suit la logique suivante :
-
Créer un champ privé pour chaque attribut non dérivé.
-
Créer une méthode getter pour chaque attribut, en respectant la visibilité.
-
Créer une méthode setter pour chaque attribut non dérivé et non accessible en lecture seule.
Première étape : créer un champ privé pour chaque attribut non dérivé
La classe HTMLPage contient quatre attributs non-dérivés.
| Modèle de conception | Code source Java |
|---|---|
|
Étape 2 : créer un accesseur pour chaque attribut, en respectant la visibilité
La classe HTMLPage contient 5 attributs monovalués,
nous devons donc créer 5 accesseurs.
| Modèle de conception | Code source Java |
|---|---|
|
L’implémentation des accesseurs pour les attributs privés n’est pas obligatoire.
Étape 3 : créer un mutateur pour chaque attribut qui n’est pas en lecture seule, ni dérivé
La classe HTMLPage contient seulement 3 attributs modifiables,
nous devons donc créer 3 mutateurs.
| Modèle de conception | Code source Java |
|---|---|
|
L’implémentation des mutateurs d’attributs privés est conseillée : elle permettra la vérification des éventuelles contraintes sur l’attribut.
L’approche par accesseurs possède plusieurs avantages sur l’approche naïve :
-
Tous les champs sont privés, l’encapsulation de la classe est maintenue.
-
La visibilité des attributs est assurée par la visibilité des méthodes d’accès.
-
L’approche permet l’implémentation des attributs dérivés et en lecture seule.
Approche par Classe-Enveloppe
L’approche par Classe-Enveloppe évite elle-aussi les limitations de l’approche naïve. La logique d’implémentation est la suivante :
-
Créer une classe enveloppe[1] pour les types d’attributs.
-
Pour chaque attribut monovalué :
-
Créer un champ privé pour chaque attribut.
-
Créer une méthode d’accès pour chaque attribut, en respectant la visibilité.
-
Étape 1 : créer une classe-enveloppe pour chaque type d’attribut
Les attributs de la classe HTMLPage ont trois types différents : String, Integer et Boolean.
Nous allons dont implémenter 3 classes enveloppe.
Ces trois classes ont une structure similaire :
public class BooleanWrapper {
private boolean value;
public BooleanWrapper();
public BooleanWrapper(boolean b) {
value = b;
}
public void set(boolean newValue) {
value = newValue;
}
public boolean get() {
return value;
}
}
public class IntegerWrapper {
private int value;
public IntegerWrapper();
public IntegerWrapper(int i) {
value = i;
}
public void set(int newValue) {
value = newValue;
}
public int get() {
return value;
}
}
public class StringWrapper {
private String value;
public StringWrapper();
public StringWrapper(String i) {
value = i;
}
public void set(String newValue) {
value = newValue;
}
public String get() {
return value;
}
}
Étape 1bis : créer une classe générique pour tous les types d’attribut
Une alternative à la création de plusieurs classes-enveloppe, c’est d’utiliser les classes paramétrées de Java :
public class Attribute<T> {
private T value;
public Attribute();
public Attribute(T t) {
value = t;
}
public void set(T newValue) {
value = newValue;
}
public T get() {
return value;
}
}
L’avantage principal de l’utilisation d’une classe paramétrée est de pouvoir traiter tous les attributs de façon homogène, puisqu’ils ont tous une super-classe commune définissant leur interface.
Étape 2 : créer une classe enveloppe pour les attributs en lecture seule
La classe HTMLPage n’a qu’un seul attribut en lecture seule,
title, dont le type est String.
Il faut donc implémenter une classe enveloppe non-modifiable pour le type String :
public class ReadOnlyStringWrapper {
private final String value;
public ReadOnlyStringWrapper(String s) {
value = s;
}
public void set(String newValue) {
throw new UnsupportedOperationException();
}
public String get() {
return value;
}
}
Une alternative possible à l’utilisation d’une exception dans la méthode set() est tout simplement de ne pas déclarer cette méthode.
Étape 3 : créer une classe enveloppe pour les attributs dérivés
La classe HTMLPage n’a qu’un seul attribut en lecture dérivé,
size, fe type est Integer, dont la valeur correspond à la taille de l’attribut contents.
public class SizeAttribute {
private final StringWrapper contents;
public SizeAttribute(StringWrapper attr) {
contents = attr;
}
public void set(int newValue) {
throw new UnsupportedOperationException();
}
public int get() {
return contents.get().size();
}
}
Tout comme pour la classe précédente,
une alternative d’implémentation consiste à ne pas déclarer la méthode set().
Étape 4 : créer un champ privé pour chaque attribut
La classe HTMLPage contient 5 attributs monovalués.
Il faut donc déclarer et initialiser 5 champs Java.
public class HTMLPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper();
private final IntegerWrapper version = new IntegerWrapper();
private final StringWrapper contents = new StringWrapper();
private final BooleanWrapper visibility = new BooleanWrapper(true);
private final SizeAttribute size = new SizeAttribute(this.contents);
}
Étape 5 : créer un accesseur pour chaque attribut
public class HTMLPage {
public ReadOnlyStringWrapper title() {
return title;
}
public SizeAttribute size() {
return size;
}
IntegerWrapper version() {
return version;
}
protected StringWrapper contents() {
return contents;
}
private BooleanWrapper visibility() {
return visibility;}
}
Le diagramme de classes (niveau implémentation) suivant résume le résultat de l’approche :
L’approche par classe-enveloppe possède elle-aussi aussi plusieurs avantages :
-
Tous les champs sont privés, l’encapsulation de la classe est maintenue.
-
La visibilité des attributs est assurée par la visibilité des méthodes d’accès.
-
Les enveloppes peuvent implémenter des attributs en lecture seule et des attributs dérivés, mais une classe spécifique peut être nécessaire.
-
Les classes-enveloppe sont extensibles: d’autres méthodes/comportements peuvent être ajoutées à la classe, par exemple
reset(),isSet(), etc.
L’inconvénient principal de l’approche est la multiplication des classes et surtout des objets.
2.4.3. Retour sur les attributs UML
Contrairement aux propriétés structurelles des langages de programmation, les attributs UML peuvent avoir des multiplicités et des propriétés.
Par exemple, considérez la classe Patient,
qui contient quatre attributs multivalués :
Les attributs de cette classe ont quatre propriétés différentes, qui indiquent la nature de leurs contenus.
| Propriété | Description |
|---|---|
|
Les valeurs de l’attribut sont ordonnées |
|
Les valeurs de l’attribut sont uniques (c’est un ensemble) |
|
Les valeurs de l’attribut ne sont pas uniques (c’est un multi-ensemble) |
|
Les valeurs de l’attribut sont ordonnées et ne sont pas uniques |
La classe Ill Patient ci-dessous, utilise deux propriétés supplémentaires sur les attributs: union
et subsets <p>.
| Propriété | Description |
|---|---|
|
L’attribut est une union dérivée de ses sous-ensembles. |
|
L’attribut est un sous-ensemble de l’attribut nommée |
Les propriétés décrites précédemment pour les attributs monovalués
2.4.4. Implémentation d’attributs multivalués
De façon similaire à l’implémentation d’attributs monovalués, l’implémentation d’attributs multivalués peut suivre au moins trois approches :
-
Approche naïve
-
Approche par accesseurs
-
Approche par Classe-Enveloppe
Approche naïve
La logique de cette approche est simples :
-
Chaque attribut UML correspond à un champ Java.
-
La visibilité des champs sera la même que celle des attributs
-
Le type des champs se base sur le Java Collections Framework, JCF
Lorqu’on applique cette logique à la classe Patient,
on obtient le code Java suivant :
| Modèle de conception | Code source Java |
|---|---|
|
Rappel des interfaces de la JFC :
| Interface | Description |
|---|---|
|
Ensemble d’éléments de type |
|
Multi-ensemble non-ordonné de type |
|
Multi-ensemble ordonné de type |
Cette approche présente plusieurs problèmes. Pour commencer, n’importe quelle autre classe peut affecter une nouvelle collection à un des champs et remplacer complètement le contenu déjà existant. En d’autres termes, l’encapsulation peut être violée.
Ensuite, elle ne peut pas gérer les multiplicités.
Par exemple, si un attribut a pour multiplicité maximale de 5 ([0..5]),
dans le code elle devient infinie ([0..] ou tout simplement []).
Enfin, la classe Patient le peut pas contrôler l’accès au contenu de ses champs. Si l’attribut spécifie une contrainte sur le contenu de l’attribut, cette contrainte sera perdue dans le code.
Approche par accesseurs
L’utilisation d’accesseurs permet de contrôler l’accès aux champs et par conséquent, résoudre ces problèmes. La logique d’implémentation est la suivante :
-
Créer un champ privé (s’il n’est dérivé)
-
Créer les méthodes d’accès à ces champs, en respectant la visibilité. Les accesseurs doivent permettre d’ajouter et supprimer des éléments, ainsi que de parcourir le contenu.
Étape 1 : Créer un champ privé pour chaque attribut
La classe Patient possède 4 attributs. Il faut donc déclarer et initialiser 4 champs privés.
| Modèle de conception | Code source Java |
|---|---|
|
Étape 2 : Créer des accesseurs pour chaque attribut
Les 4 attributs sont modifiables, alors il faut déclarer une méthode d’ajout d’un élément pour chaque attribut.
| Modèle de conception | Code source Java |
|---|---|
|
Nous allons également déclarer une méthode d’enlèvement d’un élément pour chaque attribut.
| Modèle de conception | Code source Java |
|---|---|
|
Enfin, pour parcourir les éléments, nous allons déclarer une méthode donnant accès à un itérateur sur les éléments des champs.
| Modèle de conception | Code source Java |
|---|---|
|
Il est évidemment possible d’utiliser d’autres méthodes pour parcourir les éléments, utilisant par exemple, un itérateur interne.
L’approche par accesseurs a plusieurs avantages par rapport à l’approche naïve .
-
Tous les champs sont privés, préservant l’encapsulation
-
La visibilité est assurée par les méthodes d’accès
-
Les accesseurs peuvent implémenter les contrôles des multiplicités maximales (vérifier le nombre d’éléments avant l’ajout)
Elle a aussi plusieurs inconvénients.
-
L’interface pour manipuler les champs multivalués est limitée : seulement 3 méthodes contre 25 pour l’interface Java
List -
Prolifération des méthodes : il faut au moins 3 méthodes par attribut multivalué
Approche par Classe-Enveloppe
L’approche par classe enveloppe peut résoudre les principaux inconvénients de l’approche par accesseurs. La logique de l’approche est la suivante :
-
Créer une classe enveloppe pour les types des attributs multivalués.
-
Pour chaque attribut multivalué :
-
Créer un champ privé correspondant à l’attribut.
-
Créer une méthode d’accès pour l’attribut, en respectant la visibilité.
-
Étape 1 : Créer une classe enveloppe pour les types des attributs multivalués
Ici, deux options sont possibles : (i) créer une classe-enveloppe pour chaque type d’attribut ou (ii) créer une classe-enveloppe paramétrée. Nous avons choisi la deuxième option.
| Modèle de conception | Code source Java |
|---|---|
|
Étape 2 : Créer un champ privé correspondant à chaque attribut multivalué
Pour initialiser les champs, nous utilisons les classes de la JCF.
Par exemple, pour assurer que l’attribut pathologies ne contient pas de doublons,
nous utilisons la classe HashSet.
| Modèle de conception | Code source Java |
|---|---|
|
Étape 3 : Créer une méthode d’accès pour chaque attribut multivalué, en respectant la visibilité
| Modèle de conception | Code source Java |
|---|---|
|
-
Tous les champs sont privés, préservant l’encapsulation.
-
La visibilité des attributs est assurée par les accesseurs.
-
Les enveloppes peuvent implémenter des attributs en lecture seule et des attributs dérivés, mais parfois une classe spécifique est nécessaire.
-
Les enveloppes peuvent implémenter des contrôles de multiplicité maximale.
-
Cette approche est extensible : d’autres méthodes/comportements peuvent être implémentés, par exemple l’interface Java
List.
-
Prolifération des classes et des objets.
2.5. Implémentation d’associations
2.5.1. Préambule : les associations UML
En UML, une association entre deux (ou plusieurs) classes représente un lien stable entre deux (ou plusieurs) objets, instances de ces classes.
Un lien est une instance d’une association, de même qu’un objet est une instance d’une classe.
Par lien stable, on entend un lien qui a une durée supérieure à l’exécution d’une opération : un lien existe pendant tout le cycle de vie d’un objet, même si ce lien peut changer pour relier un autre objet.
| Certains liens entre objets sont temporaires : par exemple, un objet passé en paramètre d’une méthode. Ces liens sont des dépendances entre les classes, non pas des associations. |
La Figure 9 montre un exemple d’une association entre les classes
HTMLFolder et HTMLPage.
La description de l’association est la suivante :
-
Une association binaire est représentée par une ligne continue. Lorsqu’une association relie plusieurs classes (ternaire, quaternaire, etc.), elle est représentée par un losange et des lignes continues entre les losanges et les classes qui participent à l’association.
-
L’association possède un nom, généralement un verbe. Ici, l’association s’appelle
Owns -
Parfois, le nom contient une flèche (▸), qui indique le sens de lecture : "An HTML folder owns HTML pages".
-
Une association possède des rôles, autant de rôles que de classes participantes. Un rôle a un nom et une multiplicité. Ici, l’association possède deux rôles,
containeretcontents.
| Les rôles sont des propriétés structurelles, comme les attributs. Dans une association, les classes possèdent les rôles opposés. Les rôles ont des noms et des multiplicités, tout comme les attributs. |
Dans l’exemple, la classe UMLFolder possède le rôle contents et
la classe UMLPage possède le rôle container.
La multiplicité de contents est [*] et celle de container est [1].
Certaines associations représentent des relations hiérarchiques ou des relations partie-tout. Ce sont des aggregations*, des associations binaires entre deux classes, qui jouent des rôles asymétriques. Les aggregations sont représentées par un losange (creux ou plein) placé du côté de la classe conteneur (le parent ou le tout).
| Symbole | Description |
|---|---|
Association |
|
Aggregation partagée |
|
Aggregation composite |
Comme les aggregations représentent des hiérarchies, elles forment obligatoirement un graphe acyclique. Par exemple, le diagramme suivant n’est pas valide :
Une association peut avoir une direction. Elle est unidirectionnelle ou bidirectionnelles.
Enfin, les rôles des associations peuvent avoir des modificateurs.
readOnly et uniqueunion et subsets| Modificateur | Description |
|---|---|
id |
La propriété fait partie de l’identifiant de la classe qui possède la propriété. |
readOnly |
La propriété est en lecture seule (isReadOnly = true). |
ordered |
La propriété est ordonnée (isOrdered = true). |
unique |
La propriété multivaluée n’a pas de valeurs dupliquées (isUnique = true). |
nonunique |
La propriété multivaleur peut avoir des valeurs en double (isUnique = false). |
sequence (ou seq) |
La propriété est un sac ordonné (isUnique = false et isOrdered = true). |
union |
La propriété est une union dérivée de ses sous-ensembles. |
redefines property-name |
La propriété redéfinit une propriété héritée nommée property-name. |
subsets nom-de-la-propriété |
La propriété est un sous-ensemble de la propriété nommée nom-de-la-propriété. |
property-constraint |
Contrainte qui s’applique à la propriété |
Comme les associations et les rôles n’existent pas dans les langages à objet, leur implémentation est complexe. Dans les sections suivantes, nous présentons des différentes stratégies de mise en oeuvre d’associations uni et bidirectionnelles.
2.5.2. Implémentation d’associations unidirectionnelles
Nous allons commencer par l’implémentation d’associations unidirectionnelles, qui est similaire à celle des attributs.
Approche par accesseurs
-
Les rôles monovalués ([0..1] ou [1]) sont implémentés comme des attributs.
-
Les rôles multivalués (càd, dont la valeur maximale est supérieure à 1 : [0..2], [*], etc.) : utilisation de l’interface
Collection:-
Applique le patron de conception Decorator.
-
-
La visibilité est assurée par les visibilités des accesseurs.
Si on applique cette stratégie à la classe Card du diagramme ci-dessus,
les résultat est assez simple :
public class Card {
private Account account;
public Account getAccount() {
return account;
}
public void setAccount(Account anAccount) {
this.account = anAccount;
}
}
Lorsqu’il s’agir de rôles multivalués, le résultat est également simple.
public class HTMLFolder {
private Collection<HTMLPage> pages =
new PageCollection(new HashSet<HTMLPage>());
public Collection<HTMLPage> getPages() {
return pages;
}
}
Ici, l’utilisation directe des classes Java qui implémentent l’interface Collection (ArrayList, LinkedList) n’est pas souhaitable ,
car il ne serait pas possible de vérifier certaines contraintes,
comme la multiplicité maximale.
La classe PageCollection est un décorateur de l’interface Collection,
qui ajoute un comportement additionnel,
va vérification de la multiplicité maximale.
Plus précisément, les méthodes add() et addAll() vérifient que l’ajout d’éléments ne dépassera pas la multiplicité maximale.
Pour simplifier son implémentation,
nous utilisons une classe abstraite, AbstractCollectionDecorator,
dont le seul comportement et de déléguer toutes ses méthodes à l’objet décoré.
La classe AbstractCollectionDecorator
public abstract class AbstractCollectionDecorator<T> implements Collection<T> {
private final Collection<T> decorated;
public AbstractCollectionDecorator(Collection<T> list) {
this.decorated = list;
}
public int size() {
return decorated.size();
}
public boolean isEmpty() {
return decorated.isEmpty();
}
public boolean contains(Object o) {
return decorated.contains(o);
}
public Iterator<T> iterator() {
return decorated.iterator();
}
public Object[] toArray() {
return decorated.toArray();
}
public <T> T[] toArray(T[] a) {
return decorated.toArray(a);
}
public boolean add(T htmlPage) {
return decorated.add(htmlPage);
}
public boolean remove(Object o) {
return decorated.remove(o);
}
public boolean containsAll(Collection<?> c) {
return decorated.containsAll(c);
}
public boolean addAll(Collection<? extends T> c) {
return decorated.addAll(c);
}
public boolean addAll(int index, Collection<? extends T> c) {
return decorated.addAll(index, c);
}
public boolean removeAll(Collection<?> c) {
return decorated.removeAll(c);
}
public boolean retainAll(Collection<?> c) {
return decorated.retainAll(c);
}
public void clear() {
decorated.clear();
}
public T get(int index) {
return get(index);
}
public T set(int index, T element) {
return set(index, element);
}
public boolean remove(int index) {
return decorated.remove(index);
}
public int lastIndexOf(Object o) {
return lastIndexOf(o);
}
}
PageCollectionpublic class PageCollection extends AbstractCollectionDecorator<HTMLPages> {
private int max;
public PageCollection(Collection<HTMLPages> list, int max) {
super(list);
this.max = max;
}
public boolean addAll(Collection<? extends T> c) {
if (this.size() >= this.max) throw new UnsupportedOperationException();
return super.decorated.addAll(c);
}
public boolean addAll(int index, Collection<? extends T> c) {
if (this.size() + c.size() => this.max) throw new UnsupportedOperationException();
return super.decorated.addAll(index, c);
}
}
Cette stratégie s’appuie sur les implémentation de l’interface Set, de la JFC ,
HashSet, TreeSet, etc., pour implémenter le modificateur unique.
Voici un résumé des avantages et inconvénients de cette stratégie :
-
Le décorateur permet l’ajout de nouveaux comportements, comme la vérification de certaines contraintes, comme la borne maximale et des contraintes spécifiques.
-
L’implémentation est relativement simple.
-
L’implémentation des rôles mono et multivalués est différentes : l’interface pour les utiliser n’est pas uniforme.
| De façon analogue aux attributs, les associations unidirectionnelles peuvent aussi s’implémenter grâce aux classes-enveloppe. |
2.5.3. Implémentation d’associations bidirectionnelles
L’implémentation d’associations bidirectionnelles est bien plus complexe que celle des associations unidirectionnelles. Ceci, car le concept de lien bidirectionnel n’est pas présent dans les langages de programmation.
Les associations bidirectionnelles imposent une contrainte additionnelle,
appelée intégrité référentielle.
Pour l’expliquer, prenons comme exemple l’association présentée dans la Figure 16, les instance des classes A et B peuvent être liées.
Lorsqu’un objet a1, instance de A,
est lié à un objet b1, instance de B,
cela implique que b1 est aussi lié à l’objet a1.
Intégrité référentielle
Intégrité référentielle ou "poignée de mains" (en anglais handshaking) est donc une règle entre des propriétés structurelles d’une ou plusieurs classes (une classe peut avoir une association bidirectionnelle avec elle même).
Folder et FileLa Figure 17 présente une association bidirectionnelle entre les classes
Folder et File.
Pour respecter l’intégrité référentielle,
l’implémentation de cette association doit s’assurer que :
-
Un fichier doit appartenir au dossier qui le contient.
-
Ajouter un fichier à un dossier doit produire exactement le même effet qu’affecter le dossier d’un fichier.
Par exemple, si l’on execute un dele code suivant :
var project = new Folder();
var archive = new Folder();
var readme = new File();
var doc = new File();
project.getFiles().add(readme);
project.getFiles().add(doc);
Code alternatif
Le code suivant doit produire le même résultat que le précédent.
var project = new Folder();
var archive = new Folder();
var readme = new File();
var doc = new File();
readme.setFolder(project);
doc.setFolder(project);
Le résultat de l’exécution est un ensemble d’objets dont la structure est présentée sous la forme d’un diagramme d’objets présenté par la Figure 18.
| Bien qu’il soit possible de dessiner des liens bidirectionnels en UML, nous avons préféré dessiner des liens unidirectionnels pour mieux illustrer le problème. |
Folder et FileLe résultat respecte l’intégrité référentielle.
Maintenant, si l’on souhaite déplacer le ficher doc vers le dossier archive,
en exécutant le code suivant :
doc.setFolder(archive)
Code alternatif
Le code suivant doit produire le même résultat que le précédent.
archive.getFiles().add(doc)
Le nouveau modèle d’objets doit être le suivant :
Pour assurer l’intégrité référentielle,
l’implémentation de la méthode setFolder() (ou add()) doit assurer que les liens entre
project et doc soient supprimés et que les liens entre archive et doc soient créés.`
Approche naïve
Une solution très simple serait de se fier aux classes clientes et
imposer qu’elles assurent l’intégrité référentielle.
L’implémentation de la classe Folder est très simple :
public class File {
private Folder folder;
public void setFolder(Folder other) {
folder = other;
}
public Folder getFolder() {
return folder;
}
}
Dans cette logique, chaque appel de la méthode setFolder() devra être suivi par un appel de add(),
comme suit :
Folder folder = new Folder();
File file = new File();
// (...)
if (file.getFolder() != null) {
// If the file already belongs to a folder, remove the file from it.
file.getFolder().getFiles().remove(file);
}
file.setFolder(folder);
folder.getFiles().add(file);
Si l’implémentation est simple et que cette règle sera facilement respectée par son développeur, elle sera probablement négligée lorsque de nouveaux développeurs arriveront et que le code sera maintenu.
-
Toutes les classes clientes doivent respecter cette règle.
-
C’est très difficile à assurer pendant toute la durée de vie du code source.
Approche par accesseurs
La difficulté d’implémenter l’intégrité référentielle est la suivante :
Comme l’appel de
file.setFolder(folder)doit déclencher l’appel defolder.getFiles().add(file)et que l’appel defolder.getFiles().add(file)doit appeler la méthodefile.setFolder(folder), nous avons une boucle.
Pour casser cette boucle, nous avons deux choix :
-
Laisser le client s’occuper de l’intégrité, comme nous avons vu précédemment
-
Utiliser des méthodes auxiliaires
La solution par méthodes auxiliaires consiste à ajouter deux nouvelles méthodes :
-
Une méthode appelée
basicSet(Folder)à la classeFile. -
Une méthode appelée
basicAdd(File)à la classeFileCollection.
L’objectif de ces deux méthodes est d’implémenter le comportement de base.
La méthode basicSet(Folder) ne fait qu’affecter le rôle folder et
la méthode basicAdd(File) ne fait qu’ajouter file à la collection.
Bien qu’il soit possible d’assurer l’intégrité grâce aux accesseurs, cette stratégie pose des problèmes elle aussi. En résumé :
-
Les méthodes auxiliaires,
basicSet()etbasicAdd()ne sont pas dans l’interfaceList. -
Il est difficile de généraliser la solution, car on doit connaître la signature de la méthode permettant d’accéder au role opposé et cette signature est unique pour chaque rôle.