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:

  1. La correspondance de Types

  2. Les Classes

  3. Les Attributs (mono et multivalués)

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

Tableau 1. Les types primitifs d’UML
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 .

Tableau 2. Exemple de table de correspondance
UML Java MySQL TypeScript

Integer

java.lang.Integer

BIGINT

number

Boolean

java.lang.Boolean

BOOLEAN

boolean

UnlimitedNatural

java.lang.Integer

TINYINT

number

String

java.lang.String

VARCHAR

string

Real

java.lang.Double

REAL

number

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.

Tableau 3. Un autre exemple d’une table de correspondance
UML Java

Integer

int

Boolean

boolean

UnlimitedNatural

int

String

java.lang.StringBuilder

Real

double

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

Tableau 4. Une classe UML devient une classe Java
Modèle de conception Modèle d’implémentation Code source
svg
svg
public class HTMLPage  {
    // (...)
}
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.

Tableau 5. Une classe UML devient une paire
Modèle de conception Modèle d’implémentation
Diagram
Diagram

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.

Tableau 6. Le pattern Generation Gap
Modèle de conception Modèle d’implémentation
Diagram
Diagram

Le code source Java correspondant est le suivant :

Listing 1. Source Code
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.

Tableau 7. Une classe UML devient une paire
Modèle de conception Code Java
Diagram
public interface Common {
	Common copy();
	Common deepCopy();
	boolean equals(Common);
    Map<String,Object> values();
}

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
Figure 1. La classe HTMLPage

La classe HTMLPage contient 6 attributs :

  • title est un attribut public (+) accessible en lecture seule

  • size est un attribut public, dérivé (ou calculé)

  • version est attribut optionnel (multiplicité [0..1]) de visibilité paquet (~)

  • contents est attribut protégé (#)

  • visibility est un attribut privé (-), dont la valeur initiale est true

  • tags est un attribut privé, multivalué (multiplicité [1..5]), c’est à dire, une instance de HTMLPage peut 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.

person
Figure 2. La classe Person

Interprétation des attributs :

Attribut Description Exemples

code : Integer [1]

Attribut obligatoire monovalué

1; 2; 99.

last name : String [0..1]

Attribut optionnel monovalué

null; "john"; "paul".

first names : String [*]

Attribut multivalué

{}, {"john", "paul"}, {"ringo", "george"}

Tout comme la plupart des langages à objets, les attributs UML peuvent avoir des visibilités :

Tableau 8. Visibilité en UML
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.

person properties
Figure 3. La classe Person
Tableau 9. Propriétés des attributs UML
Propriété Description

readOnly

Attribut en lecture seule

id

Les valeurs de l’attributs sont uniques parmi les instances de la classe

redefines <p>

L’attribut redéfinie l’attribut hérité <p> (le renommage d’attributs est possible)

En UML les attributs peuvent avoir des contraintes, qui sont décrites grâce au langage OCL

person constraints
Figure 4. La classe Person

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.

image$person age
Figure 5. La classe Person

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 :

Tableau 10. Concepts
UML Java

Attribut

Champ

Visibilité
  • +`

  • #

  • ~

  • -

Visibilité
  • public

  • protected

  • ""

  • private

Attribut readOnly

Champ final

Si l’on applique cette approche à la classe HTMLPage, nous aurons la classe Java suivante~:

Tableau 11. Approche naïve d’implémentation d’attributs
Modèle de conception Code source
svg
public class HTMLPage {
	public final String title;
	Integer version;
	protected String contents;
	private Boolean visibility = new Boolean(true);
}

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 :

Pour chaque attribut monovalué :
  1. Créer un champ privé pour chaque attribut non dérivé.

  2. Créer une méthode getter pour chaque attribut, en respectant la visibilité.

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

Tableau 12. Implémentation des champs
Modèle de conception Code source Java
Diagram
public class HTMLPage {
	private final String title;
	private Integer version;
	private String contents;
	private Boolean visibility = new Boolean(true);
}

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

Tableau 13. Implémentation des accesseurs
Modèle de conception Code source Java
Diagram
public class HTMLPage {
    public String getTitle() {
        return title;
    }
    public Integer getSize() {
        return contents.size();
    }
    Integer getVersion() {
        return version;
    }
    protected String getContents() {
        return contents;
    }
    private Boolean getVisibility() {
        return visibility;
    }
}

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.

Tableau 14. Implémentation des mutateurs
Modèle de conception Code source Java
Diagram
public class HTMLPage {
    void setVersion(Integer aVersion) {
        version = aVersion;
    }
    protected void setContents(String str) {
        contents = str;
    }
    private void setVisibility(Boolean bool) {
        visibility = bool;
    }
}

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 :

  1. Tous les champs sont privés, l’encapsulation de la classe est maintenue.

  2. La visibilité des attributs est assurée par la visibilité des méthodes d’accès.

  3. 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 :

  1. Créer une classe enveloppe[1] pour les types d’attributs.

  2. Pour chaque attribut monovalué :

    1. Créer un champ privé pour chaque attribut.

    2. 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 :

Listing 2. La classe BooleanWrapper
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;
    }
}
Listing 3. La classe IntegerWrapper
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;
    }
}
Listing 4. La classe StringWrapper
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 :

Listing 5. La classe paramétrée Attribute
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 :

Listing 6. Classe enveloppe non-modifiable
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.

Listing 7. Classe enveloppe pour l’attribut size
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.

Listing 8. Déclaration et initialisation des champs
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

Listing 9. Déclaration des accesseurs
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 :

Modèle d’implémentation de la classe HTMLPage (aperçu)
Figure 6. Modèle d’implémentation de la classe HTMLPage (aperçu)

L’approche par classe-enveloppe possède elle-aussi aussi plusieurs avantages :

  1. Tous les champs sont privés, l’encapsulation de la classe est maintenue.

  2. La visibilité des attributs est assurée par la visibilité des méthodes d’accès.

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

  4. 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 :

Patient
Figure 7. La classe Patient

Les attributs de cette classe ont quatre propriétés différentes, qui indiquent la nature de leurs contenus.

Propriété Description

ordered

Les valeurs de l’attribut sont ordonnées

unique

Les valeurs de l’attribut sont uniques (c’est un ensemble)

non-unique

Les valeurs de l’attribut ne sont pas uniques (c’est un multi-ensemble)

sequence ou seq

Les valeurs de l’attribut sont ordonnées et ne sont pas uniques

La classe Ill Patient
Figure 8. La classe Ill Patient

La classe Ill Patient ci-dessous, utilise deux propriétés supplémentaires sur les attributs: union et subsets <p>.

Propriété Description

union

L’attribut est une union dérivée de ses sous-ensembles.

subsets nom-de-la-propriété

L’attribut est un sous-ensemble de l’attribut nommée nom-de-la-propriété.

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 :

  1. Approche naïve

  2. Approche par accesseurs

  3. 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
Diagram
public class Patient {
    public final Set<String> pathologies = new HashSet<String>;
    public final Collection<String>  exams = new ArrayList<String>();
    public final List<Double> temperatures = new ArrayList<Double>;
    public final Collection<String> notes = new ArrayList<String>();
}

Rappel des interfaces de la JFC :

Interface Description

Set<T>

Ensemble d’éléments de type T. Ne contient pas de doublons

Collection<T>

Multi-ensemble non-ordonné de type T

List<T>

Multi-ensemble ordonné de type T

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 :

Pour chaque attribut multivalué :
  1. Créer un champ privé (s’il n’est dérivé)

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

Tableau 15. Création des champs privés correspondant aux attributs multivalués
Modèle de conception Code source Java
Diagram
public class Patient {
    private final Set<String> pathologies   = new HashSet<String>();
    private final Collection<String>  exams = new ArrayList<String>();
    private final List<Double> temperatures = new ArrayList<Double>();
    private final Collection<String> notes  = new ArrayList<String>();
}

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

Tableau 16. Déclaration des méthodes d’ajout d’éléments
Modèle de conception Code source Java
Diagram
public class Patient {
    public boolean addPathologie(String str) {
        return this.pathologies.add(str);
    }
    public boolean addExam(String str) {
        return this.exams.add(str);
    }
    public boolean addTemperature(Double d) {
        return this.temperatures.add(d);
    }
    public boolean addNote(String str) {
        if (notes.size == 5) return false;
        return this.notes.add(str);
    }
}

Nous allons également déclarer une méthode d’enlèvement d’un élément pour chaque attribut.

Tableau 17. Déclaration des méthodes d’enlèvement d’éléments
Modèle de conception Code source Java
Diagram
public class Patient {
    public boolean removePathologie(String str) {
        return this.pathologies.remove(str);
    }
    public boolean removeExam(String str) {
        return this.exams.remove(str);
    }
    public boolean removeTemperature(Double d) {
        return this.temperatures.remove(d);
    }
    public boolean removeNote(String str) {
        return this.notes.remove(str);
    }
}

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.

Tableau 18. Déclaration des méthodes de parcours d’éléments
Modèle de conception Code source Java
Diagram
public class Patient {
	public Iterator<String> iterator() {
		return this.pathologies.iterator();
	}
	public Iterator<String> iterator() {
		return this.exams.iterator();
	}
	public Iterator<Double> iterator() {
		return this.temperatures.iterator();
	}
	public Iterator<String> iterator() {
		return this.notes.iterator();
	}
}

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 .

Avantages
  1. Tous les champs sont privés, préservant l’encapsulation

  2. La visibilité est assurée par les méthodes d’accès

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

Inconvénients
  1. L’interface pour manipuler les champs multivalués est limitée : seulement 3 méthodes contre 25 pour l’interface Java List

  2. 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 :

Étapes
  1. Créer une classe enveloppe pour les types des attributs multivalués.

  2. Pour chaque attribut multivalué :

    1. Créer un champ privé correspondant à l’attribut.

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

Tableau 19. Classe-Enveloppe paramétrée
Modèle de conception Code source Java
Diagram
public class MultivaluedAttribute<T> {
    private final List<T> values;

    public MultivaluedAttribute(List<T> l) {
        this.valued = l;
    }

    public boolean add(T t) {
        return this.values.add(t);
    }

    public boolean remove(T t) {
        return this.values.remove(t);
    }

    public Iterator<T> iterator() {
        return values.iterator();
    }
}

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

Tableau 20. Déclaration et initialisation des champs
Modèle de conception Code source Java
Diagram
public class Patient {
    private final MultivaluedAttribute<String> pathologies =
        new MultivaluedAttribute<String>(new HashSet<String>());

    private final MultivaluedAttribute<String>  exams =
        new MultivaluedAttribute<String>(new ArrayList<String>());

    private final MultivaluedAttribute<Double> temperatures =
        new MultivaluedAttribute<Double>(new ArrayList<Double>());

    private final MultivaluedAttribute<String> notes =
        new MultivaluedAttribute<String>(new ArrayList<String>());
}

Étape 3 : Créer une méthode d’accès pour chaque attribut multivalué, en respectant la visibilité

Tableau 21. Déclaration des accesseurs
Modèle de conception Code source Java
Diagram
public class Patient {
    public MultivaluedAttribute<String> pathologies() {
        return this.pathologies;
    }
    public MultivaluedAttribute<String> exams() {
        return this.examns;
    }
    public MultivaluedAttribute<Double> temperature() {
        return this.temperatures;
    }
    public MultivaluedAttribute<String> notes() {
        return this.notes;
    }
}
Avantages
  1. Tous les champs sont privés, préservant l’encapsulation.

  2. La visibilité des attributs est assurée par les accesseurs.

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

  4. Les enveloppes peuvent implémenter des contrôles de multiplicité maximale.

  5. Cette approche est extensible : d’autres méthodes/comportements peuvent être implémentés, par exemple l’interface Java List.

Inconvénients
  1. 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.
L’association Owns
Figure 9. L’association Owns

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, container et contents.

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

Tableau 22. Associations et aggregations
Symbole Description
Diagram

Association

Diagram

Aggregation partagée

Diagram

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 :

Les aggregations doivent former un graphe acyclique
Figure 10. Les aggregations doivent former un graphe acyclique

Une association peut avoir une direction. Elle est unidirectionnelle ou bidirectionnelles.

Association bidirectionnelle
Figure 11. Association bidirectionnelle

Enfin, les rôles des associations peuvent avoir des modificateurs.

Les modificateurs `readOnly` et `unique`
Figure 12. Les modificateurs readOnly et unique
Les modificateurs `union` et `subsets`
Figure 13. Les modificateurs union et subsets
Tableau 23. Modificateurs de propriétés (attributs ou roles)
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
Logique
  • 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.

Clients, comptes et cartes
Figure 14. Clients, comptes et cartes

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;
    }
}
Rôle multivalué
Figure 15. Rôle multivalué

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);
    }

}
Listing 10. La classe PageCollection
public  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 :

Avantages
  • 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.

Inconvénient
  • 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.

Exemple d’association bidirectionnelle
Figure 16. Exemple d’association bidirectionnelle

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

L’association bidirectionnelle entre `Folder` et `File`
Figure 17. L’association bidirectionnelle entre Folder et File

La 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.
Modèle d’objets `Folder` et `File`
Figure 18. Modèle d’objets Folder et File

Le 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 :

Modèle d’objets après le déplacement d’un fichier
Figure 19. Modèle d’objets après le déplacement d’un fichier

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 :

Listing 11. Solution alternative: laisser les classes clientes assurer l’intégrité référentielle
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.

Inconvénients
  • 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 de folder.getFiles().add(file) et que l’appel de folder.getFiles().add(file) doit appeler la méthode file.setFolder(folder), nous avons une boucle.

Diagram

Pour casser cette boucle, nous avons deux choix :

  1. Laisser le client s’occuper de l’intégrité, comme nous avons vu précédemment

  2. 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 classe File.

  • Une méthode appelée basicAdd(File) à la classe FileCollection.

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.

Ajout d’un fichier sans boucle
Figure 20. Ajout d’un fichier sans boucle
Affectation d’un dossier sans boucle
Figure 21. Affectation d’un dossier sans boucle

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

Inconvénient
  • Les méthodes auxiliaires, basicSet() et basicAdd() ne sont pas dans l’interface List.

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


1. Une Classe-Enveloppe ou Wrapper est une classe mutable, qui contrôle l’accès à une valeur ou à un autre objet.