Diagramme de classes (Class diagram)

Le diagramme de cas d’usage nous permet d’y voir un peu plus clair dans le fonctionnement du programme. On peut maintenant s’intéresser aux classes qui vont nous permettre de réaliser toutes les actions.

Une vue « conceptuelle » des classes et de leurs relations :

Dans le cas du jeu de rôles, le joueur n’interagit qu’à travers une interface graphique. Celle-ci récupère les commandes du joueur via un clavier, une souris, etc. Elle affiche l’écran d’accueil, la page de création du personnage et de connexion et, lorsque le jeu est lancé, l’état du jeu. L’état du jeu lui-même devrait être une classe à part entière dont le but est uniquement de conserver l’état du jeu, c’est-à-dire les positions du joueur, des coffres et des ennemis, des points de vie du joueur et des ennemis, etc.

Cela suggère de créer une classe GUI, une classe ÉtatJeu ainsi que deux classes ClavierInput et SourisInput pour récupérer les commandes du joueur.

En UML, les classes, classes abstraites et interfaces sont représentées par des rectangles.

On peut imaginer que l’API est la même pour interroger les devices d’entrée/sortie clavier et souris. Dans ce cas, on crée une interface UserInput contenant cette API, et ClavierInput et SourisInput implémentent (on dit aussi réalisent) cette interface.

En UML, on représente l’implémentation via une flèche en pointillés dont la tête est un triangle, cf. le diagramme ci-dessous.

Évidemment, une instance de GUI doit contenir une instance de ClavierInput et/ou de SourisInput. Cette relation est une composition car cela n’aurait aucun sens que le clavier et la souris existent quand l’interface graphique du jeu est détruite.

En UML, on représente la relation de composition via une flèche en traits pleins dont la tête est un losange noir. On représente la relation d’agrégation de la même manière à ceci près que le losange est blanc.

Pour que l’interface graphique GUI puisse afficher l’état du jeu, elle doit demander à ÉtatJeu de lui communiquer son état. Il existe donc une relation (on parle d'association) entre ces deux classes. ÉtatJeu n’est pas vraiment un « composant » de l’interface graphique, il ne serait donc pas logique de créer une relation de composition ou d’agrégation entre ces deux classes.

En UML, si une relation n’est ni de l’agrégation ni de la composition, on parle simplement d'association si la relation est bi-directionnelle ou bien de dépendance si elle est uniquement mono-directionnelle.

Ici, si on considère qu’ÉtatJeu peut envoyer des signaux à GUI pour l’obliger à rafraichir la fenêtre de jeu, on utilisera une association, sinon seule GUI enverra des messages à ÉtatJeu et on aura une dépendance. On va supposer ici que c’est la première solution qui est retenue pour notre jeu.

En UML, une association est représentée par une arête en traits pleins. Les dépendances sont représentées par des flèches en pointillés dont la tête est un >.

Enfin, pour toutes ces relations (association, dépendance, composition, agrégation), on peut indiquer à côté de l’arête/la flèche le nombre d’instances impliquées (lorsqu’il ne s’agit que d’une seule instance, on n’indique pas le nombre). Par exemple, GUI peut contenir uniquement un clavier, uniquement une souris ou les deux, on indiquera donc une cardinalité « 1..2 ». On peut préciser que l’on peut avoir un nombre arbitraire d’instances en utilisant la cardinalité *. Cela nous amène donc au diagramme de classe suivant :

diagramme de classe

On peut maintenant détailler le contenu de la classe ÉtatJeu. ÉtatJeu contient l’état du Joueur, des Coffres ainsi que des Orcs, Gobelins et Elfs. Ici, les relations sont de composition. Mais, pour écrire un code évolutif (principes SOLID de la séance suivante), on ne doit pas stocker directement dans ÉtatJeu une liste d’Orcs, une liste de Gobelins et une liste d’Elfs car rajouter un nouveau type d’ennemi impliquerait de modifier ÉtatJeu. On crée donc une interface ou une classe abstraite Ennemi qui représentera n’importe quel type d’ennemi et ÉtatJeu contiendra une liste d’Ennemi. On peut imaginer que certaines propriétés (position, points de vie, etc.) sont communs à tous les types d’ennemis. Donc, ici, une classe abstraite paraît plus appropriée. C’est ce que reflète le diagramme ci-dessous.

Pour savoir s’il est proche d’un Ennemi, le Joueur peut demander (via une méthode) à l’Ennemi sa position, et réciproquement. Puisque l’interaction est bidirectionnelle, on a ici une association (une composition ou une agrégation n’aurait aucun sens). L’intérêt de préciser ce type de lien réside dans le fait qu’on peut anticiper qu’un changement d’API dans l’une de ces classes peut nécessiter des changements dans l’autre. Il y a également une relation entre le Joueur et les Coffres : le Joueur pourra demander à voir le contenu d’un Coffre, mais le Coffre ne demandera jamais rien au Joueur. Dans ce cas, la relation est asymétrique et il s’agit donc d’une dépendance.

diagramme de classe

Notez que ce sont surtout l’héritage, la composition et l’agrégation qui sont importantes, puisque ces relations auront un impact très concret sur vos classes. Les associations et les dépendances ont surtout un intérêt pour voir l’impact de modifications dans les API (signatures des méthodes) quand vous ferez de la maintenance/mise à jour de votre application.

L’architecture ci-dessus semble permettre de réaliser toutes les actions répertoriées dans le diagramme de cas d’usage. Par exemple, l’action « se connecter » revient à ce que le joueur interagisse avec GUI pour sélectionner, dans un menu, l’item de connexion. GUI demande alors à ÉtatJeu de s’initialiser. Ce dernier crée les Ennemis et les Coffres et initialise l’instance de Joueur. GUI peut alors afficher l’état du jeu. On peut de manière similaire déterminer les processus impliqués par les autres actions.

Résumé des entités et relations d'un diagramme de classes

Voici une résumé de la notation graphique des diagrammes de classes. Tout d’abord, les entités les plus utilisées (classes, classes abstraites, interfaces) que l’on peut trouver :

entités du diagramme de classe

Les relations les plus utilisées :

relations du diagramme de classe

Exercice 1 : C'est la classe   

Écrivez le diagramme de classes du système de contrôle de l’USS Orville.

Niveaux de détails des classes

On peut grossièrement scinder en 3 le niveau de détails des classes que l’on affiche :

Le diagramme de classes montré plus haut est clairement de niveau conceptuel. Si l’on considère les deux autres niveaux, il faut décrire plus précisément le contenu des classes, classes abstraites et interfaces.

Prenons comme exemple la classe Joueur. Un joueur a un état (position en x,y, points de vie, quantité de munitions, etc.). Il est pourvu de méthodes pour se déplacer, changer de direction, prendre le contenu d’un coffre, attaquer un ennemi. Dans le niveau « spécification », on pourrait donc avoir une classe similaire à celle ci-dessous :

classe dans un diagramme de classe

On voit que la « boite » correspondant à une classe est scindée verticalement en trois blocs : on a tout d’abord le nom de la classe, puis la liste des attributs et, enfin, les méthodes. Pour les attributs, il est d’usage d’indiquer leur type (mais ce n’est pas obligatoire). En général, on n’indique pas le type de retour des méthodes ni leurs paramètres (mais on peut le faire si on veut). On n’indique pas non plus les constructeurs (ni le destructeur si l’on est en C++). Notons que sur la gauche des attributs et méthodes, on indique leur visibilité :

signe Signification
+ attribut/méthode « public »
- attribut/méthode « private »
# attribut/méthode « protected »
~ attribut/méthode « package-private »

Les méthodes et classes abstraites sont en principe écrites en italique. Les méthodes statiques sont représentées en les soulignant.

Les méthodes statiques abstraites ne sont pas représentées… Ah ah, évidemment puisque ce type de méthode n’existe pas !

Le niveau « implementation » est identique à celui de « spécification », excepté que l’on donne plus de détails : il s’agit de préciser la totalité des attributs et des méthodes.

Un exemple de diagramme de classe plus détaillé

Le diagramme ci-dessous représente une application de gestion bancaire. La Banque est constituée de Conseillers et de Comptes clients. En ce qui concerne les Conseillers, il y a une agrégation car, si la Banque ferme, les Conseillers peuvent se faire embaucher dans une autre Banque. En revanche, en cas de fermeture de la banque, les comptes clients sont détruits. Les clients sont des Personnes qui peuvent être clients dans une ou plusieurs Banques. Ici, la relation entre Banque et Client n’est pas une relation de composition ou d’agrégation mais plutôt une relation « une Banque possède des Clients » ou « un Client a une Banque ». On peut indiquer la signification de l’association sur l’arête (ici, « a une »), avec un triangle qui spécifie la direction de cette relation (ici, c’est le Client qui a une Banque). C’est le Client qui « possède » un Compte, etc. Un compte peut être un compte client classique ou bien un compte d’épargne comme un Livret A. Les règles de dépôt et de retrait sont différentes (si vous déposez de l’argent le 3 du mois sur un Livret A, il sera crédité au 15 du mois par exemple). D’où l’idée d’hériter d’une classe abstraite Compte. Cette classe contient la liste des operations bancaires du compte (OpérationCompte). Cela nous amène naturellement au diagramme ci-dessous :

diagramme de classe pour une banque

Exercice 2 : Spécification, spécification...   

Réécrivez le diagramme de classe de l’application du contrôle de l’USS Orville au niveau de détails « spécification ».

 
© C.G. 2007 - 2025