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).
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.).
|
|
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 :
Any2.1. Exercice : Identifiants simples
-
Implémentez la classe
Anyen Java. -
Utilisez un champ de type
longpour représenter l’identifiant unique. -
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 :
|
-
Modifiez la classe
Anyde l’exercice précédent pour lui ajouter un constructeur contenant un seul paramètre, son identifiant. -
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 :
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.
|
-
Modifiez la classe
Factoryde 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 :
-
Avec des accesseurs.
-
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.
|
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.
|
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
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.
Game et PlayerLes 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 :
-
L’ajout d’un joueur à une autre partie.
-
L’affection d’une partie à un joueur.
|
Ajout d’un joueur à une autre partie
|
|
Affectation d’une partie à un joueur
|
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.
Player et SecretMission
|
|