Exercices UML et Java
Durée2h15Contexte
Le travail de cette séance sera en deux parties distinctes : une partie conception UML puis une partie programmation Java.
Diagrammes UML
UNDER CONSTRUCTION…
Petite architecture logicielle en Java
Contexte
Cet exercice vous permettra de consolider vos connaissances en programmation objet Java en réalisant une variante du jeu Démineur.
Une partie de démineur commence par l’affichage d’une grille. Le contenu des cases est initialement masqué. L’enjeu est de déterminer les cases qui contiennent une mine (sans cliquer dessus sans quoi on perd la partie). Pour dévoiler le contenu d’une case il faut cliquer dessus. Le premier coup s’effectue au hasard puisque rien ne permet de savoir initialement si une case contient une mine ou non. Mais si la case ne contient pas de mine alors :
- Si au moins une des 8 cases adjacentes contient une mine, alors le contenu de la case est dévoilé et ce contenu est le nombre de cases adjacentes minées.
- Sinon (aucune mine à proximité), la case est dévoilée (elle est vide) et on simule un clic sur les 8 cases adjacentes. Il y a alors propagation : toutes les cases vides se dévoilent de proche en proche, mais aussi les cases numérotées directement voisines des cases vides dévoilées. Si on a pu déterminer la position d’une mine on peut la marquer via un clic-droit. Un petit drapeau rouge symbolise alors l’emplacement de la mine et on ne peut plus cliquer dessus par mégarde. La partie s’achève lorsqu’on clique sur une case minée (on perd) ou lorsqu’on a dévoilé toutes les cases ne contenant pas de mines (on gagne).

Diagramme de classes
La classe Demineur
modélise l’interface graphique du jeu. Vous
n’avez pas à la modifier. Le programme a également besoin d’une
modélisation du terrain miné. Les méthodes utiles à l’interface
graphique pour interagir avec le terrain et ses cases sont listées
dans les interfaces ITerrainMine
et ICase
. Votre travail consiste
à réaliser des classes implémentant convenablement ces interfaces.

Implémenter l’interface ICase
Décompressez l’archive fournie ici sur votre disque
local puis ouvrez le dossier obtenu dans VS Code à partir du menu
File>>Add Folder to Workspace...
.
CaseAbstraite
Le jeu comporte trois types de cases :
- des cases vides (classe
CaseVide
), qui n’ont pas de mine dans les cases voisines. - des cases numérotées (classe
CaseNumerotee
), qui sont des cases vides qui portent un numéro correspondant au nombre (strictement positif) de mines dans les cases voisines. - des cases minées (classe
CaseMinee
).
Toute case :
- a des coordonnées (un index de colonne et un index de ligne).
- mémorise si elle est marquée à l’aide d’un drapeau (ou non).
- mémorise si son contenu est dévoilé ou encore caché.
- connaît le terrain dans lequel elle figure (car elle doit accéder aux cases voisines pour propager le dévoilement des cases).
Les trois types de cases ont donc des attributs communs. Le code de la
déclaration de ces variables d’instances et de leurs accesseurs est
factorisé au sein de la classe abstraite CaseAbstraite
. Cette classe
implémente l’interface ICase
. Mais étant abstraite la classe
CaseAbstraite
n’a pas l’obligation de fournir une implémentation de
toutes les méthodes de ICase
(car il est impossible de créer une
instance de CaseAbstraite
). Cependant, toute classe concrète qui
étend CaseAbstraite
doit fournir une implémentation des méthodes de
ICase
absentes de CaseAbtraite
(i.e. getVal()
, estMinee()
, clic()
et
propager()
). Les classes CaseVide
, CaseMinee
et CaseNumerotee
vont étendre CaseAbstraite
en fournissant une implémentation des
méthodes manquantes.
CaseVide
- Déclarez que la classe
CaseVide
étendCaseAbstraite
. - Ajoutez un constructeur
CaseVide(ITerrainMine terrain, int colonne, int ligne)
qui fait appel au constructeur de sa super-classe afin d’initialiser les variables d’instances héritées. En consultant les spécifications indiquées dansICase
, vous verrez que :- la méthode
getValeur()
doit retourner la constanteICase.VIDE
dans le cas d’une case vide. - la méthode
estMinee()
devra toujours retournerfalse
(la case est vide, donc non minée). - la méthode
clic()
est appelée lorsque l’utilisateur clique sur la case. Cette méthode est déjà implémentée dansCaseVide
. Le code fourni (1) dévoile le contenu de la case et (2) appelle la méthodepropager()
sur chacune des cases voisines. - la méthode
propager()
est appelée lorsqu’une case voisine vient d’être dévoilée. Le comportement à implémenter pour une case vide est d’appelerclic()
mais uniquement si le contenu de la case est encore caché (vérifier si la case est encore cachée permet d’éviter à la propagation de boucler indéfiniment).
- la méthode
- Implémentez les méthodes
getValeur()
etestMinee()
. Testez en exécutant la méthodemain
deTesteur
. - Implémentez la méthode
propager()
et dé-commentez le code de la méthodeclic()
. Pour tester, dé-commentez la ligne correspondante deTerrainMine
puis exécutez la méthodemain
deDemineur
. À ce stade, la grille est remplie de cases vides et un clic sur n’importe quelle case doit dévoiler toute la grille.
CaseMinee
-
Dans le cas d’une case minée :
- l’état n’a pas à être complété : le fait d’être de type
CaseMinee
implique implicitement que la case est minée. - la méthode
getValeur()
doit retourner la constanteICase.MINEE
. - la méthode
estMinee()
devra toujours retournertrue
(puisque la case est minée). - la méthode
clic()
ne fait qu’appeler la méthodedevoiler()
. - la méthode
propager()
ne fait rien.
- l’état n’a pas à être complété : le fait d’être de type
-
Rajoutez au projet une classe
CaseMinee
héritant deCaseAbstraite
. -
Rajoutez un constructeur prenant les mêmes paramètres que le constructeur de
CaseAbstraite
et qui fait appel au constructeur de sa super-classe afin d’initialiser les variables héritées. -
Implémentez les méthodes
getValeur()
etestMinee()
. Testez en exécutant la méthodemain
deTesteur
. -
Implémentez les méthodes
clic()
etpropager()
. Pour tester, dé-commentez la ligne correspondante deTerrainMine
puis exécutez la méthodemain
deDemineur
. À ce stade, un clic sur une case vide dévoile toutes les cases qui ne sont pas minées. Les cases qui demeurent cachées sont minées.
CaseNumerotee
Une case numérotée est avant tout une case vide (non minée), mais une case vide qui a des cases voisines minées. Lorsque son contenu est dévoilé, l’interface graphique indique le nombre de cases voisines minées. Nous pourrions vouloir recalculer chaque fois que nécessaire le nombre de cases voisines minées. Cependant, ce nombre ne change pas tout au long de la partie et pour éviter ces re-calculs inutiles nous allons mémoriser l’information au sein de la case numérotée.
- Rajoutez au projet une classe
CaseNumerotee
héritant deCaseVide
. - Complétez l’état en rajoutant une variable d’instance
valeur
(de typeint
) qui mémorisera le nombre de cases voisines minées. - Rajoutez un constructeur ayant un paramètre de plus (comparé au constructeur de
CaseAbstraite
) et qui servira à initialiservaleur
.
En cas de clic sur une case numérotée seul le contenu de la case est dévoilé (appel à devoiler()
) : contrairement à CaseVide
il n’y a pas propagation.
- Redéfinissez les méthodes qui doivent l’être.
Pour tester, dé-commentez la ligne correspondante de
TerrainMine
puis exécutez la méthodemain
deDemineur
.
Correction
Pour aller plus loin
Interface Comparable
Vous avez probablement déjà exploré diverses méthodes pour trier un ensemble (ou une collection). En connaissant un algorithme de tri, comme le tri par fusion (https://fr.wikipedia.org/wiki/Tri_fusion), vous pourriez souhaiter l’implémenter dans une méthode public void sort(List<Integer> list)
pour trier une liste d’entiers. Plus tard, vous pourriez avoir besoin d’implémenter une méthode public void sort(List<String> list)
pour trier des listes de chaînes de caractères. Les implémentations seraient très similaires, car l’algorithme reste le même. Il serait donc plus efficace de créer une version générique unique qui fonctionnerait indépendamment du type des éléments.
Pour tous types d’éléments ? Pas tout à fait. Pour trier nous avons besoin de pouvoir comparer les éléments entre eux. Nous serions capables d’écrire une méthode de tri générique, capable de trier une List<T>
pour peu que les éléments de type T
soient comparables entre eux, et donc que le type T
dispose d’une méthode capable de déterminer si un élément de type T
est inférieur ou supérieur à un autre.
Les interfaces sont utilisées pour imposer l’existence de la méthode requise. Dans notre exemple, la méthode de classe Collections.sort(List<T> list)
ne peut être utilisée que si T
implémente l’interface Comparable<T>
. C’est ce qu’indique la condition <T extends Comparable<? super T>>
dans la javadoc de la classe Collections
.
static <T extends Comparable<? super T>> void sort(List<T> list)
"Sorts the specified list into ascending order, according to the natural ordering of its elements."
L’interface Comparable<T>
ne comporte qu’une méthode, compareTo
dont voici un extrait des spécifications indiquées dans la javadoc :
int compareTo(T o)
Parameters: o - the object to be compared.
Returns: a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.
Les classes « enveloppes » (Integer
, Double
, Character
, …)
et String
implémentent cette interface. Il est donc possible
d’utiliser Collections.sort(List<T> list)
sur des listes d’éléments
de ces types.
Exercice
- Dans l’archive de l’exercice précédent, quelques classes vous sont fournies dans le package
s05.comparable
. - Sous VS Code, consultez la méthode
main
deTestSort
du packages05.comparable
puis exécutez-là.
Nous aimerions à présent trier des List<Heure>
.
- Modifiez la classe
Heure
afin qu’elle implémenteComparable<Heure>
. Ne réinventez pas la roue et sachez exploiter les méthodes qui figurent déjà dans la classeHeure
. - Complétez la méthode
main
deTestSort
afin de trier la listelisteHeures
avant de l’afficher.
La classe Collections
(avec un s
à la fin !) contient des méthodes de classe utiles à la gestion de collections.
- Complétez la méthode
main
deTestSort
en exploitant les méthodes deCollections
(javadoc disponible sous https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Collections.html) afin de :- Mélanger (shuffle) la liste
listeHeures
puis l’afficher. - Afficher la valeur minimale de
listeHeures
. - Afficher la valeur maximale de
listeHeures
. - Trier la liste
listeHeures
dans l’ordre décroissant puis l’afficher.
- Mélanger (shuffle) la liste