Implémentation de la conception pilotée par le domaine

Pendant la construction du logiciel, le passage du modèle de conception au code source est essentielle. Dans ce chapitre, nous allons utiliser les patrons de conception, ainsi que des principes de conception introduits pendant les cours pour proposer l’implémentation des modèles de conception du domaine.

Un modèle de conception du domaine représente des objets du monde réel dans un domaine donné, en opposition aux objets dits techniques, comme Window, NetworkSocket, MouseEvent, etc.

La figure Figure 1 est un modèle de conception du domaine pour le jeu de plateau Risk.

Commençons par la classe Game, qui est l’objet racine du modèle. Il est relié à la classe Player, puisqu’un joueur joue à une partie (a player plays a game).

Notez le triangle pour le sens de lecture de l’association. Notez aussi qu’une association possède au moins deux rôles : un à chaque extrémité. Chaque rôle a un nom et une multiplicité.

Selon la multiplicité du rôle de l’association entre ces deux classes, il peut y avoir entre deux et six ([2..6]) joueurs qui jouent à une partie. L’association entre Player et Territory précise qu’un joueur peut occuper entre 0 et 42 territoires.

L’association entre Continent et Territory représente la composition des territoires des différents continents, soit 4 (Amérique du sud et Océanie), 6 (Afrique), 7 (Europe), 9 (Amérique du nord) ou 12 (Asie).

Modèle de conception du jeu Risk
Figure 1. Modèle de conception du jeu Risk

1. Correspondance de types

Le langage UML dispose d’un ensemble restreint de types primitifs : String, Integer, Real, Boolean et UnlimitedNatural. Pour implémenter un modèle de conception UML dans un langage de programmation, nous devons établir une correspondance entre les types UML et ceux du langage cible (Java, Kotlin, C#, Python, etc.).

  1. Tout d’abord, complétez le tableau suivant pour proposer une correspondance entre les types UML et ceux de Java, pour les attributs monovalués.

Tableau 1. Types d’attributs monovalués
UML Java

String

Integer

Real

Boolean

UnlimitedNatural

  1. Maintenant, complétez le tableau suivant, pour proposer une table de correspondance similaire, mais pour des attributs à valeurs multiples, ou multivalués. Utilisez les classes et les interfaces fournies par le Java Collection Framework (JCF).

Tableau 2. Types d’attributs multivalués
UML Java

AnyType [*] {unique, ordered}

AnyType [*] {unique, unordered}

AnyType [*] {nonunique, ordered}

AnyType [*] {nonunique, unordered}

2. Identifiants uniques

Un des principes de la programmation à objets est l’unicité des objets : chaque objet possède un identifiant unique qui le distingue des autres objets. En Java, comme dans d’autres langages à objets, c’est l’adresse mémoire d’un objet qui lui sert d’identifiant unique. Ainsi, si deux objets ont la même adresse mémoire, ils sont identiques.

Cette approche fonctionne correctement, pourvu que l’objet reste dans le même espace mémoire et dans le même contexte d’exécution. Si l’objet est enregistré sur une base de données ou envoyé, par le réseau, sur un autre espace mémoire, son adresse mémoire change et son identifiant n’est plus le même.

Contrairement aux objets techniques, dont le cycle de vie est lié à celui de l’exécution du logiciel, les objets du domaine doivent continuer à exister et rester cohérents d’une exécution à l’autre. Ils sont sujets à être stockés dans des bases de données et aussi envoyés à travers le réseau pour être utilisés par un autre logiciel, dans un espace mémoire différent.

En conséquence, il est naturel d’ajouter un identifiant unique aux objets du domaine. Différentes approches sont possibles : un nombre entier, une chaine de caractères, un identifiant unique universel, UUID [1], etc.

C’est ce que nous allons faire ici. Nous allons implémenter une classe nommée Any, qui servira de super classe commune à toutes les classes ayant besoin d’un identifiant. Voici la représentation de cette classe en UML :

La classe `Any`
Figure 2. La classe Any

2.1. Exercice : Identifiants simples

  1. Implémentez la classe Any en Java.

  2. Utilisez un champ de type long pour représenter l’identifiant unique.

  3. Implémentez un accesseur pour le champ id.

Un Accesseur ou Getter est une méthode qui renvoie la valeur d’un champ et ne fait rien de plus.

2.2. Exercice : Utilisation d’une méthode fabrique

La solution précédente est très simple et efficace. Cependant, elle peut poser problème dans certaines situations, où l’on souhaite contrôler la valeur de l’identifiant.

Par exemple, supposez que vous souhaitez restaurer un objet stocké dans un fichier. Cet objet possède déjà un identifiant (par exemple, l’entier 42), comment créer une instance de Any et lui affecter un identifiant ?

On peut évidemment créer un deuxième constructeur et passer l’identifiant comme argument, mais dans ce cas, comment assurer l’unicité des identifiants ?

La solution est d’utiliser un patron de conception appelé « Méthode Fabrique ». Le diagramme de classes suivant décrit cette solution :

Diagram
  • Notez le lien unidirectionnel pointillé entre Factory et Any. Il représente une dépendance en UML. Ici, une dépendance de création/instantiation.

  • Notez aussi que l’opération Any() est marquée avec «create». Cela veut dire qu’il s’agit d’un constructeur.

  1. Modifiez la classe Any de l’exercice précédent pour lui ajouter un constructeur contenant un seul paramètre, son identifiant.

  2. Implémentez la classe Factory.

2.3. Exercice : Utilisation d’un Singleton

La solution précédente pose aussi problème, car la classe Factory peut avoir plusieurs instances et par conséquent, plusieurs valeurs de nextId. Nous devons empêcher que la classe Factory soit instanciée plusieurs fois.

Pour cela, nous allons utiliser le patron de conception « Singleton », pour assurer que la classe Factory n’aura qu’une seule instance. Le diagramme de classes suivant décrit cette solution :

Diagram
Le symbole - devant une propriété (attribut ou opération) indique que la visibilité est privée. Les symboles +, ~ et # représentent les visibilités publique, paquet et protégée, respectivement.
  1. Modifiez la classe Factory de l’exercice précédent pour en faire un Singleton.

3. Implémentation d’Attributs Monovalués

Dans cet exercice, nous allons utiliser deux approches différentes pour implémenter des attributs :

  1. Avec des accesseurs.

  2. Avec des classes-enveloppes.

3.1. Approche simple: utilisation d’accesseurs

Pour implémenter la classe Continent et ses attributs, utilisez la stratégie d’implémentation Getter/Setter introduite pendant les cours.

  1. Tout d’abord, déclarez la classe Java et ses champs. N’oubliez pas d’en faire une sous-classe de la classe Any.

  2. Déclarez ensuite les accesseurs et les modifieurs (getters et setters) des champs. N’oubliez pas de respecter la visibilité des attributs.

  3. Finalement, décidez comment traiter les erreurs, par exemple les arguments dont la valeur est null.

Continent: Diagramme de classes d’implémentation
Figure 3. Continent: Diagramme de classes d’implémentation

3.2. Avec des classes mutables

Maintenant, nous allons utiliser la stratégie d’implémentation "Classes-Enveloppes" introduite pendant les cours pour implémenter la classe Continent. L’idée ici est d’utiliser des classes mutables pour implémenter les attributs.

  1. Commencez par implémenter une classe-enveloppe pour le type String

  2. Cette classe doit être capable de :

    1. Stocker la valeur par défaut d’un attribut

    2. Dire si la valeur a été affectée (méthode booléenne isSet())

    3. Revenir à la valeur initiale (méthode reset())

  3. Ensuite, ajoutez à la classe Continent un champ dont le type est StringWrapper, et un accesseur pour ce champ.

Continent: Diagramme de classes d’implémentation
Figure 4. Continent: Diagramme de classes d’implémentation

3.3. Comparaison

Analysez les deux solutions. A votre avis :

  1. Laquelle demande le plus de lignes de code ?

  2. Laquelle utilise le plus de mémoire ?

  3. Laquelle est la plus simple à utiliser ?

4. Implémentation d’Attributs Multivalués

Les attributs multivalués sont des attributs dont la multiplicité maximale est supérieure à 1. Le nombre maximal d’éléments peut être non contraint (multiplicité [*]) ou contraint (multiplicité de [n], où n est un nombre naturel supérieur à 1).

La classe Player possède deux attributs, name et nickname. Le premier est monovalué (multiplicité 1) et le deuxième, multivalué. Par la suite, vous allez implémenter ces deux attributs.

4.1. Exercice: utilisez le patron Décorateur pour implémenter une liste

Tout d’abord, appliquez le patron "Décorateur" pour implémenter une liste de taille maximale limitée. Comme le décorateur doit implémenter toutes les méthodes de l’interface List, vous aurez besoin de consulter la spécification de cette interface.

4.2. Exercice: implémentation de la classe Player

Maintenant, implémentez la classe Player et ses deux attributs. Utilisez des accesseurs pour ces deux attributs.

Solution
Listing 1. La classe Player
package fr.unantes.sce.domain;

import java.util.ArrayList;
import java.util.List;

public class Player {

    private String name;
    private final List<String> nicknames = new SizeLimitedList<>(new ArrayList<>(), 3);

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<String> nicknames() {
        return nicknames;
    }
}

5. Implémentation d’Association Bidirectionnelles

Les associations bidirectionnelles sont des associations navigables dans les deux sens. Par exemple, la figure ci-dessous montre une association bidirectionnelle entre les classes Player et Game. Cette association est navigable à partir des instances de Player vers une instance de Game, mais aussi à partir des instances de Game vers des instances de Player.

Une association bidirectionnelle entre les classes `Game` et `Player`
Figure 5. Une association bidirectionnelle entre les classes Game et Player

Les associations bidirectionnelles imposent une contrainte, appelée intégrité référentielle : si une instance g de Game est liée à une instance p de Player, alors cette instance p est aussi liée à l’instance g.

5.1. Implémentation de l’association entre Game et Player

L’implémentation de cette association est plutôt complexe, car elle impose de maintenir la cohérence des deux côtés de l’association, ce qui implique l’envoi de plusieurs messages entre les différents objets.

Pour comprendre l’interaction entre les objets, vous allez utiliser des diagrammes de communication, pour illustrer les deux cas suivants :

  1. L’ajout d’un joueur à une autre partie.

  2. L’affection d’une partie à un joueur.

Ajout d’un joueur à une autre partie
  1. Commencez par illustrer l’interaction lorsqu’une classe client demande à une instance de Game, beta, d’ajouter à la liste de joueurs une instance de Player, alice, qui participe déjà à une autre partie, alpha.

  2. Les messages sont numérotés pour indiquer l’ordre d’envoi.

  3. Utilisez des flèches pour indiquer le sens de l’envoi des messages.

  4. Utilisez des croix pour indiquer la destruction d’un lien ou d’une instance, et des couleurs différentes pour indiquer les nouveaux liens ou instances créés.

    svg
Affectation d’une partie à un joueur
  1. Maintenant, illustrez l’interaction lorsqu’une classe client demande à une instance de Player d’affecter une nouvelle instance de Game.

    svg

5.2. Implémentation de l’association entre SecretMission et Player

Contrairement à l’association entre Player et Game, les deux rôles de l’association entre SecretMission et Player sont mono-valuées. La conséquence est que les liens entre les objets sont des références Java, qui peuvent être nulles : il faudra tester si le lien est nul avant de l’utiliser.

L’association entre `Player` et `SecretMission`
Figure 6. L’association entre Player et SecretMission
  1. Utilisez le diagramme de communication ci-dessous pour illustrer l’affectation d’un joueur à une mission.

    Affectation d’un joueur à une mission
    Figure 7. Affectation d’un joueur à une mission
  1. Utilisez le diagramme de communication ci-dessous pour illustrer l’affectation d’une mission à un joueur.

    Affectation d’une mission à un joueur
    Figure 8. Affectation d’une mission à un joueur

1. Universally Unique IDentifier est en informatique un système permettant à des systèmes distribués d’identifier de façon unique une information sans coordination centrale importante