Exercices UML et Java

Durée2h15

Contexte

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 étend CaseAbstraite.
  • 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 dans ICase, vous verrez que :
    • la méthode getValeur() doit retourner la constante ICase.VIDE dans le cas d’une case vide.
    • la méthode estMinee() devra toujours retourner false (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 dans CaseVide. Le code fourni (1) dévoile le contenu de la case et (2) appelle la méthode propager() 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’appeler clic() 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).
  • Implémentez les méthodes getValeur() et estMinee(). Testez en exécutant la méthode main de Testeur.
  • Implémentez la méthode propager() et dé-commentez le code de la méthode clic(). Pour tester, dé-commentez la ligne correspondante de TerrainMine puis exécutez la méthode main de Demineur. À 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 constante ICase.MINEE.
    • la méthode estMinee() devra toujours retourner true (puisque la case est minée).
    • la méthode clic() ne fait qu’appeler la méthode devoiler().
    • la méthode propager() ne fait rien.
  • Rajoutez au projet une classe CaseMinee héritant de CaseAbstraite.

  • 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() et estMinee(). Testez en exécutant la méthode main de Testeur.

  • Implémentez les méthodes clic() et propager(). Pour tester, dé-commentez la ligne correspondante de TerrainMine puis exécutez la méthode main de Demineur. À 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 de CaseVide.
  • Complétez l’état en rajoutant une variable d’instance valeur (de type int) 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 à initialiser valeur.

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éthode main de Demineur.
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 de TestSort du package s05.comparable puis exécutez-là.

Nous aimerions à présent trier des List<Heure>.

  • Modifiez la classe Heure afin qu’elle implémente Comparable<Heure>. Ne réinventez pas la roue et sachez exploiter les méthodes qui figurent déjà dans la classe Heure.
  • Complétez la méthode main de TestSort afin de trier la liste listeHeures avant de l’afficher.

La classe Collections (avec un s à la fin !) contient des méthodes de classe utiles à la gestion de collections.

Correction