Beans JavaFX
Javass – étape 8

1 Introduction

Le but de cette étape est d'écrire des classes permettant de stocker certaines informations au sujet d'une partie de Jass, afin de les afficher dans l'interface graphique d'un joueur. Les informations en question sont : la main du joueur, le pli courant, et les scores courants.

Bien entendu, des classes représentant chacune de ces informations ont déjà été écrites lors des étapes précédentes. Toutefois, ces classes ne sont pas directement utilisables dans l'interface graphique, car elles ne sont pas ce qu'on appelle observables (voir plus bas).

Les classes à écrire dans le cadre de cette étape se basent sur celles déjà écrites, mais sont observables et donc utilisables dans le cadre de l'interface graphique qui sera mise en œuvre prochainement.

Cette étape utilise des concepts qui n'auront pas encore été vus au cours lors de sa publication. Avant de continuer, il est donc fortement recommandé de lire les deux sections ci-dessous des notes de cours sur les patrons de conception (partie II) et les interfaces graphiques JavaFX :

  1. la §2, dédiée au patron Observer, des notes de cours sur les patrons de conception,
  2. la §4, dédiée aux propriétés et liens, des notes de cours sur les interfaces graphiques JavaFX.

1.1 Interface graphique Javass

L'interface graphique de Javass montre à un joueur toutes les informations dont il a besoin au sujet de la partie en cours. Elle est séparée en trois sections, comme l'illustre la figure ci-dessous, sur laquelle ces sections ont été teintées pour faciliter leur identification.

javass-gui-parts.png

Figure 1 : Les trois sections de l'interface graphique de Javass

De haut en bas, les trois sections montrent :

  1. les scores actuels du tour et de la partie (en rouge),
  2. le pli courant et l'atout (en vert),
  3. la main du joueur (en bleu).

On remarquera que le valet de pique est mis en évidence par une bordure rouge dans la seconde section, ce qui signifie qu'il s'agit de la carte la plus forte du pli actuel.

D'autre part, on constatera que le quatrième emplacement (depuis la gauche) dans la main du joueur est vide. Cela signifie que la carte qui s'y trouvait initialement a été jouée lors d'un pli précédent. En d'autres termes, lorsqu'une carte est jouée, elle disparaît de la main, et l'emplacement qu'elle occupait reste libre.

1.2 Mise à jour de l'interface

Un problème récurrent lorsqu'on réalise une interface graphique est de savoir comment la garder à jour. C'est-à-dire comment faire pour que, au fur et à mesure que les données internes du programme qui sont représentées à l'écran — le modèle — changent, l'interface soit mise à jour afin de toujours représenter la réalité ?

La solution choisie par beaucoup de bibliothèques graphiques — dont JavaFX, utilisée ici — consiste à demander à ce que le modèle soit observable (au sens du patron Observer), et à faire ensuite en sorte que l'interface graphique observe le modèle. De la sorte, dès que le modèle change, l'interface graphique en est informée et peut se mettre à jour.

Par exemple, la section supérieure de l'interface graphique de Javass affiche les scores actuels. Cet affichage doit bien entendu être mis à jour lorsque les scores changent. Pour cela, le composant graphique qui affiche les scores les observe, et se met à jour chaque fois qu'ils changent.

Bien qu'assez élégante, cette solution présente un problème dans le cadre de ce projet : ni la classe représentant les scores, ni aucune autre classe écrite jusqu'à présent d'ailleurs, n'est observable. Il est donc nécessaire de définir des classes observables représentant les différents éléments visibles dans l'interface graphique, ce que nous ferons dans cette étape en écrivant ce que JavaFX nomme des beans.

1.3 Beans JavaFX

En Java, le terme bean1 est utilisé pour désigner des objets dotés d'attributs observables et respectant un certain nombre de conventions. Les détails ne sont pas importants pour cette étape, il faut juste se souvenir que nous appellerons bean JavaFX un objet doté d'attributs observables par des composants graphiques JavaFX. Dans la terminologie JavaFX, ces attributs observables sont nommés propriétés (properties).

Par exemple, l'un des beans à réaliser pour cette étape est celui des scores. Une des propriétés de ce bean contient le score de l'équipe 1. Cette propriété est observée par le composant graphique textuel JavaFX qui affiche les scores dans la première section de l'interface présentée plus haut.

En plus du bean des scores, deux autres beans existent dans ce projet, et ils correspondent aux deux autres sections de l'interface graphique :

  1. le bean du pli courant, qui possède p.ex. une propriété donnant l'atout actuel,
  2. le bean de la main du joueur, qui possède p.ex. une propriété donnant les 9 cartes (éventuellement vides) de la main actuelle du joueur.

Les différentes propriétés de ces beans sont destinées à être observées par les composants de l'interface graphique. Par exemple, le composant graphique qui affiche l'atout au centre de l'écran observe la propriété correspondante du bean du pli courant, et met à jour son image en fonction.

Bien entendu, pour que tout cela fonctionne, il faut que ces propriétés soient modifiées au fur et à mesure du déroulement de la partie. Cette tâche sera confiée à une classe écrite à l'étape 10, qui tiendra à jour les propriétés en fonction des appels faits aux méthodes de l'objet de type Player correspondant au joueur humain. Par exemple, l'appel à la méthode updateScore provoquera la mise à jour des propriétés du bean des scores.

2 Mise en œuvre Java

Les classes à écrire pour cette étape étant toutes liées à l'interface graphique, il est conseillé de les placer dans un nouveau paquetage nommé ch.epfl.javass.gui dédié à celle-ci (GUI étant l'acronyme de Graphical User Interface, c-à-d interface utilisateur graphique).

2.1 Classe ScoreBean

La classe ScoreBean du paquetage ch.epfl.javass.gui, publique et finale, est un bean JavaFX contenant (principalement) les scores et doté des propriétés suivantes :

  1. turnPoints, qui contient les points du tour,
  2. gamePoints, qui contient les points de la partie,
  3. totalPoints, qui contient le total des points,
  4. winningTeam, qui contient l'identité de l'équipe ayant gagné la partie.

Les trois premières de ces propriétés existent à deux exemplaires, puisqu'il y a deux équipes.

A chaque propriété correspond deux méthodes, une qui permet d'obtenir la propriété en question, l'autre qui permet de modifier la valeur qu'elle contient. Notez qu'en général, les beans JavaFX offrent également une troisième méthode pour chaque propriété, un accesseur (getter) permettant d'obtenir la valeur contenue dans la propriété. Comme nous n'en aurons pas besoin dans ce projet, nous ne les incluons pas.

Pour la première « double propriété » ci-dessus, turnPoints, les deux méthodes en question pourraient ressembler à ceci :

ReadOnlyIntegerProperty turnPointsProperty(TeamId team) { … }
void setTurnPoints(TeamId team, int newTurnPoints) { … }

En appelant la première méthode avec TEAM_1, on obtient la propriété contenant les points du tour de l'équipe 1. Celle-ci est retournée avec le type ReadOnlyIntegerProperty qui signale à l'appelant que cette propriété est accessible en lecture seule. Pour modifier les points du tour de l'équipe 1, il faut donc appeler la seconde méthode avec TEAM_1 en premier argument, et les nouveaux points en second argument.

Les deux autres « doubles propriétés » (gamePoints et totalPoints) sont similaires. La dernière propriété, winningTeam, est quant à elle plus simple car elle contient simplement une valeur de type TeamId, qui est nulle tant que la partie n'est pas terminée. Ses deux méthodes à elle ont donc la signature suivante :

ReadOnlyObjectProperty<TeamId> winningTeamProperty() { … }
void setWinningTeam(TeamId winningTeam) { … }

2.1.1 Conseils de programmation

Les différentes composantes des scores étant entières, les propriétés les contenant doivent idéalement être des propriétés contenant un entier, comme illustré plus haut. Trois classes les représentent dans JavaFX :

  • ReadOnlyIntegerProperty, une classe abstraite représentant le concept de propriété entière accessible en lecture seule (donc non modifiable),
  • IntegerProperty, une classe abstraite représentant le concept de propriété entière accessible en lecture/écriture,
  • SimpleIntegerProperty, une classe concrète représentant une version simple (mais tout à fait adéquate ici) d'une propriété entière accessible en lecture/écriture.

Chacune des deux dernières classes hérite — parfois indirectement — de celle qui la précède.

Cela implique entre autres qu'il est possible de représenter en interne les propriétés entières au moyen d'instances de SimpleIntegerProperty mais de les exposer à l'extérieur (via la méthode turnPointsProperty p.ex.) avec le type ReadOnlyIntegerProperty, exprimant ainsi le fait que ces propriétés ne sont pas censées être modifiées directement.

La propriété winningTeam contient quant à elle un objet (pouvant être nul) de type TeamId, elle doit être une propriété contenant un objet. Une hiérarchie similaire à celle des propriétés entières existe dans JavaFX, constituée de :

2.2 Classe TrickBean

La classe TrickBean du paquetage ch.epfl.javass.gui, publique et finale, est un bean JavaFX contenant le pli courant et doté de trois propriétés :

  1. trump, qui contient l'atout courant,
  2. trick, qui contient le pli courant,
  3. winningPlayer, qui contient le joueur menant le pli courant.

La première, trump, est une propriété objet contenant une valeur de type Color, comme on peut le supposer.

On pourrait imaginer que la seconde soit similaire, c-à-d une propriété objet contenant une valeur de type Trick. Malheureusement, une telle propriété ne serait pas très facile à utiliser dans le cadre de l'interface graphique, raison pour laquelle nous proposons une autre solution.

L'idée est d'exposer le pli comme une table associative observable associant à chaque joueur la carte qu'il a joué dans le pli courant, qui est nulle si le joueur en question n'a pas encore joué. En d'autres termes, la méthode permettant d'obtenir la propriété trick a la signature suivante :

ObservableMap<PlayerId, Card> trick() { … }

Qu'en est-il de la méthode setTrick qui permet de changer la valeur de cette table ?

Une possibilité — probablement la plus logique — serait qu'elle prenne en argument une table associative associant à chaque joueur la carte jouée durant le pli courant. Toutefois, une telle solution ne serait pas très compatible avec le reste du projet, dans lequel un pli est représenté par le type Trick.

Dès lors, nous suggérons de faire en sorte que setTrick accepte un argument de type Trick et se charge de modifier en fonction la table associative contenant le pli. En d'autres termes, la méthode setTrick a la signature suivante :

void setTrick(Trick newTrick) { … }

En plus de changer la valeur de la propriété trick, cette méthode se charge également de changer celle de la propriété winningPlayer, qui est une propriété en lecture seule pour laquelle aucune méthode de modification (setter) n'existe. Notez que winningPlayer vaut null lorsque le pli courant est vide.

2.2.1 Conseils de programmation

JavaFX fournit plusieurs classes et méthodes utiles pour créer des tables associatives observables. Lisez la documentation des entités suivantes pour comprendre comment créer la propriété contenant le pli :

  • l'interface ObservableMap, qui représente les tables associatives observables,
  • la méthode observableHashMap de FXCollections, qui permet de créer une table associative observable dont le contenu est stocké dans une instance de HashMap sous-jacente,
  • la méthode unmodifiableObservableMap de FXCollections, qui permet d'obtenir une vue non modifiable (mais observable) sur une table associative observable.

L'utilisation de vues non modifiables permet d'éviter que la table observable retournée par trick ne soit modifiable de l'extérieur.

2.3 Classe HandBean

La classe HandBean du paquetage ch.epfl.javass.gui, publique et finale, est un bean JavaFX doté de deux propriétés :

  • hand, qui contient la main du joueur,
  • playableCards, qui contient le sous-ensemble de la main du joueur qui est actuellement jouable, et qui est vide si ce n'est pas au joueur en question de jouer.

La propriété hand est une liste observable contenant toujours exactement 9 valeurs de type Card. Lorsque le joueur joue une carte de sa main, l'élément correspondant de la liste devient simplement nul.

Tout comme pour la propriété trick, nous vous conseillons de fournir, en plus d'une méthode permettant d'obtenir la propriété, une méthode de modification prenant en argument un ensemble de cartes de type CardSet. En d'autres termes, nous vous suggérons de fournir les deux méthodes suivantes pour la propriété hand :

ObservableList<Card> hand() { … }
void setHand(CardSet newHand) { … }

L'idée étant que lorsque setHand est appelée avec un ensemble contenant 9 cartes, la totalité de la main est redéfinie, alors que si elle est appelée avec un ensemble plus petit, les cartes qui ne font pas partie de l'ensemble donné sont mises à null. (Voir l'exemple de la §2.4.)

Pour la propriété playableCards, nous vous proposons d'adopter une solution similaire et de fournir les deux méthodes suivantes :

ObservableSet<Card> playableCards() { … }
void setPlayableCards(CardSet newPlayableCards) { … }

2.3.1 Conseils de programmation

JavaFX fournit des méthodes et classes permettant de représenter les listes et ensembles observables, qui sont très similaires à celles liées aux tables associatives observables. Nous nous contentons donc de vous donner ici les liens vers leur documentation :

2.4 Tests

Les classes de cette étape ne sont pas très faciles à tester. Il est toutefois conseillé de vérifier que les différentes propriétés sont bien observables, c-à-d que les auditeurs qu'on leur attache sont bien informés des changements.

L'extrait de programme ci-dessous illustre comment faire cela pour la propriété hand de la classe HandBean.

HandBean hb = new HandBean();
ListChangeListener<Card> listener = e -> System.out.println(e);
hb.hand().addListener(listener);

CardSet h = CardSet.EMPTY
  .add(Card.of(Color.SPADE, Rank.SIX))
  .add(Card.of(Color.SPADE, Rank.NINE))
  .add(Card.of(Color.SPADE, Rank.JACK))
  .add(Card.of(Color.HEART, Rank.SEVEN))
  .add(Card.of(Color.HEART, Rank.ACE))
  .add(Card.of(Color.DIAMOND, Rank.KING))
  .add(Card.of(Color.DIAMOND, Rank.ACE))
  .add(Card.of(Color.CLUB, Rank.TEN))
  .add(Card.of(Color.CLUB, Rank.QUEEN));
hb.setHand(h);
while (! h.isEmpty()) {
  h = h.remove(h.get(0));
  hb.setHand(h);
}

En l'exécutant, les lignes ci-dessous devraient s'afficher à l'écran :

 1: { [null] replaced by [♠6] at 0 }
 2: { [null] replaced by [♠9] at 1 }
 3: { [null] replaced by [♠J] at 2 }
 4: { [null] replaced by [♡7] at 3 }
 5: { [null] replaced by [♡A] at 4 }
 6: { [null] replaced by [♢K] at 5 }
 7: { [null] replaced by [♢A] at 6 }
 8: { [null] replaced by [♣10] at 7 }
 9: { [null] replaced by [♣Q] at 8 }
10: { [♠6] replaced by [null] at 0 }
11: { [♠9] replaced by [null] at 1 }
12: { [♠J] replaced by [null] at 2 }
13: { [♡7] replaced by [null] at 3 }
14: { [♡A] replaced by [null] at 4 }
15: { [♢K] replaced by [null] at 5 }
16: { [♢A] replaced by [null] at 6 }
17: { [♣10] replaced by [null] at 7 }
18: { [♣Q] replaced by [null] at 8 }

Notez que les 9 premières lignes sont dues au premier appel à setHand figurant avant la boucle while, car on lui passe un ensemble de taille 9, ce qui implique que la totalité des éléments de la main sont redéfinis.

3 Résumé

Pour cette étape, vous devez :

  • écrire les classes HandBean, ScoreBean et TrickBean (ou équivalent) en fonction des indications données plus haut,
  • tester votre code,
  • documenter la totalité des entités publiques que vous avez définies.

Aucun rendu n'est à faire pour cette étape avant le rendu final. N'oubliez pas de faire régulièrement des copies de sauvegarde de votre travail en suivant nos indications à ce sujet.

Notes de bas de page

1

Le mot bean fait référence aux grains de café (coffee beans). Java lui-même ayant reçu le nom d'une île d'Indonésie productrice de café, beaucoup de technologies liées à ce langage ont été nommées en référence à cette boisson.