TP 2 — Observable, observateurs et stratégie
Une roulette de casino : faire réagir des joueurs aux tirages avec Observable-Observateurs, puis varier les paris avec Stratégie — corrigé pas à pas depuis le fork du dépôt.
Objectifs et mise en place
Deux patrons à mettre en œuvre dans ce TP :
- Observable – Observateurs ;
- Stratégie.
L'objectif est de comprendre et retenir le principe de ces patrons et de savoir les appliquer de manière autonome.
Fork — créez une bifurcation du dépôt : une copie sur un dépôt distant qui vous appartient, sur laquelle vous pouvez modifier sans altérer l'original.
Clone — depuis IntelliJ : menu projet > Get from Version Control…, collez l'URL du dépôt, Clone — le projet s'ouvre directement dans l'IDE (Maven est déjà configuré).
Complétez — le dépôt fournit l'organisation en paquetages et une première base des
classes : fr.univlille.iut.r304.roulette avec Roulette, Annonceur, Joueur,
ObservateurDeRoulette et Main.
Le patron Observable-Observateurs, pour rappel
Un ou plusieurs observateurs s'inscrivent auprès d'un observable et sont avertis des événements
qui se produisent sur lui : la méthode update() des observateurs est déclenchée par
notifyObservers(). Le cours 2 détaille le patron dans son intégralité.
Une roulette de casino
On simule une table de roulette sans s'occuper des mises ni des gains :
- 1 observable : une roulette qui sort un numéro au hasard entre 0 et 49 à chaque tour ;
- 2 observateurs :
- un annonceur qui affiche le numéro sorti sous la forme
Annonceur : <numéro>; - un joueur nommé Hal qui parie toujours sur le 42 et affiche
Gagné !ouZut, perdu !.
- un annonceur qui affiche le numéro sorti sous la forme
---La roulette s'arrête---
Annonceur : 17
Hal : Zut, perdu !
---La roulette s'arrête---
Annonceur : 42
Hal : Gagné !
...Deux réactions dans n'importe quel ordre
Une exécution où Hal réagirait avant que l'annonceur dise le numéro est tout aussi valable : les acteurs réagissent au numéro gagnant, l'ordre de parcours de la liste d'observateurs n'est pas imposé.
Pour appliquer le patron :
- dans
Roulette: encapsuler une liste d'observateurs et les notifier après chaque lancer (avec la méthode qui change l'état de l'observable) ; - dans l'interface
ObservateurDeRoulette: la signature de méthode adéquate ; - dans les classes des observateurs : implémenter l'interface et écrire le code du scénario ;
- dans
Main: un scénario de 10 lancers — il ne devrait y avoir qu'une instruction dans la boucle !
Le squelette du dépôt donne déjà l'état de l'observable (numero, lancer(),
getNumero()). Il reste le trio du patron :
- l'interface
ObservateurDeRoulette— une seule méthode suffit ; quelle signature ? L'observateur aura besoin de connaître le numéro sorti : le plus simple est de lui passer la roulette elle-même —void reagir(Roulette r). Il ira piocherr.getNumero()s'il en a besoin ; - l'observable — une
List<ObservateurDeRoulette>,add/removepour l'abonnement, et la notification à la fin delancer()(l'événement, c'est le tirage) ; - les observateurs —
Annonceuraffiche,Joueurcompare et affiche ;Maincâble le tout et lance 10 tirages.
public interface ObservateurDeRoulette {
void reagir(Roulette r);
}public class Roulette {
private static final Random GEN_ALEA = new Random();
private final List<ObservateurDeRoulette> observateurs = new ArrayList<>();
private int numero = 0;
public void add(ObservateurDeRoulette obs) {
observateurs.add(obs);
}
public void remove(ObservateurDeRoulette obs) {
observateurs.remove(obs);
}
public void lancer() {
this.numero = GEN_ALEA.nextInt(50); // 1. changement d'état
System.out.println("---La roulette s'arrête---");
notifyObservers(); // 2. notification
}
private void notifyObservers() {
for (ObservateurDeRoulette obs : observateurs) {
obs.reagir(this); // 3. réaction de chacun
}
}
public int getNumero() {
return this.numero;
}
}Les trois temps du patron se lisent dans lancer() : modification d'état → notification →
réactions. notifyObservers() est private : c'est un mécanisme interne de l'observable,
personne dehors ne devrait pouvoir déclencher les observateurs à la main.
public class Annonceur implements ObservateurDeRoulette {
@Override
public void reagir(Roulette r) {
System.out.println("Annonceur : " + r.getNumero());
}
}Dans un premier temps, Joueur porte simplement son numéro fétiche — la version qui rendra
l'ajout de Djaouad gratuit :
public class Joueur implements ObservateurDeRoulette {
private final String nom;
private final int numeroFetiche;
public Joueur(String nom, int numeroFetiche) {
this.nom = nom;
this.numeroFetiche = numeroFetiche;
}
@Override
public void reagir(Roulette r) {
if (r.getNumero() == numeroFetiche) {
System.out.println(nom + " : Gagné !");
} else {
System.out.println(nom + " : Zut, perdu !");
}
}
}public class Main {
public static void main(String[] args) {
Roulette r = new Roulette();
r.add(new Annonceur());
r.add(new Joueur("Hal", 42));
for (int tour = 0; tour < 10; tour++) {
r.lancer(); // la seule instruction de la boucle
}
}
}Ajouter Djaouad (parie sur le 7) : une ligne dans le main, zéro nouvelle classe —
r.add(new Joueur("Djaouad", 7));. C'est exactement la mesure du découplage : la roulette
n'a pas bougé.
Nouveaux joueurs, nouvelles stratégies
On ajoute d'autres joueurs : tous réagissent comme Hal (gagné/perdu), mais chacun a sa propre stratégie pour choisir son numéro :
- Alice parie sur le 0 au 1ᵉʳ tour, le 1 au 2ᵉ tour, et ainsi de suite ;
- Moussa alterne entre deux numéros : 20 et 10.
C'est le patron Stratégie : revoyez la conception, montrez l'UML pour validation, puis implémentez.
Le refactor se fait en douceur : ce qui est variable chez un joueur, c'est le numéro qu'il choisit. On l'extrait derrière une interface :
StrategiePari { int choisirNumero(); } ← l'interfaceJoueur ne garde plus un int numeroFetiche mais un StrategiePari strategie ; Hal et
Djaouad deviennent deux instances de la même stratégie fixe. Ajouter Alice ou Moussa = écrire
une classe de stratégie de plus, sans toucher ni à Joueur ni à Roulette.
Question de conception à se poser : que doit faire la stratégie entre deux tours ? Alice avance d'un cran à chaque pari, Moussa alterne — leur état évolue quand on leur demande un numéro. Gardez ça en tête pour la Martingale.
public interface StrategiePari {
int choisirNumero();
}
public class ParieFixe implements StrategiePari {
private final int numero;
public ParieFixe(int numero) { this.numero = numero; }
@Override
public int choisirNumero() { return numero; }
}
public class ParieSequence implements StrategiePari {
private int suivant = 0;
@Override
public int choisirNumero() { return suivant++; }
}
public class ParieAlterne implements StrategiePari {
private final int[] numeros = {20, 10};
private int tour = 0;
@Override
public int choisirNumero() {
int pari = numeros[tour % 2];
tour++;
return pari;
}
}Et Joueur refactoré :
public class Joueur implements ObservateurDeRoulette {
private final String nom;
private final StrategiePari strategie;
public Joueur(String nom, StrategiePari strategie) {
this.nom = nom;
this.strategie = strategie;
}
@Override
public void reagir(Roulette r) {
int pari = strategie.choisirNumero();
if (pari == r.getNumero()) {
System.out.println(nom + " : Gagné !");
} else {
System.out.println(nom + " : Zut, perdu !");
}
}
}Câblage dans le main :
r.add(new Joueur("Hal", new ParieFixe(42)));
r.add(new Joueur("Djaouad", new ParieFixe(7)));
r.add(new Joueur("Alice", new ParieSequence()));
r.add(new Joueur("Moussa", new ParieAlterne()));Martingale
Nouvelle stratégie : parier sur le même numéro que le dernier qui a été tiré (au 1ᵉʳ tour, on pariera sur 0). Proposez une conception, faites-la valider, implémentez-la.
public class Martingale implements StrategiePari {
private int dernierTire = 0; // au 1er tour, on parie sur 0
@Override
public int choisirNumero() {
return dernierTire;
}
public void numeroTire(int numero) {
this.dernierTire = numero;
}
}Le piège : quand la stratégie apprend-elle le numéro sorti ? Si le joueur lisait le
numéro au moment de réagir (r.getNumero()) pour le rejouer au tour suivant, il faudrait
mémoriser « le précédent »… Le plus sûr est de faire de l'apprentissage une étape explicite :
l'interface évolue en
public interface StrategiePari {
int choisirNumero();
void numeroTire(int numero); // le tour est terminé, voici le résultat
}et Joueur.reagir enchaîne dans le bon ordre :
@Override
public void reagir(Roulette r) {
int pari = strategie.choisirNumero(); // 1. je parie (avec la mémoire des tours passés)
if (pari == r.getNumero()) {
System.out.println(nom + " : Gagné !");
} else {
System.out.println(nom + " : Zut, perdu !");
}
strategie.numeroTire(r.getNumero()); // 2. j'apprends le résultat, pour le tour suivant
}Les stratégies qui n'ont rien à apprendre laissent numeroTire vide (corps vide, ou méthode
default dans l'interface). Si numeroTire était appelé avant choisirNumero, la
Martingale parierait sur le numéro qui vient de sortir et gagnerait tous les tours — l'ordre
des deux appels est bien une décision de conception.
Y a plus de saisons !
On simule cette fois le comportement d'animaux (et d'une enfant, et d'une vacancière) qui réagissent à la météo, celle-ci changeant aléatoirement :
| Météo | Soleil | Pluie | Nuage |
|---|---|---|---|
| Enfant | joue dehors | joue dans la boue | observe les nuages |
| Chat | se prélasse | regarde à l'intérieur | se balade dans le jardin |
| Vacancière | bronze | sort son parapluie | joue au molkky |
| Oiseau | chante | se réfugie dans l'arbre | mange dans l'herbe |
---Météo : Pluie---
Un enfant joue dans la boue
Un chat regarde à la fenêtre
Une vacancière sort son parapluie
Un oiseau se réfugie dans l'arbre
...À quel patron recourir ? Proposez l'UML, faites-le valider, implémentez.
Le patron est le même que la roulette, transposé : l'observable est la météo (état =
temps), les observateurs sont les personnages. C'est un bon exercice de généralisation :
vous venez d'écrire deux fois le même squelette, une interface Observateur générique
reparamétrée serait la suite naturelle (mais restez simple si le sujet ne le demande pas).
public enum Meteo { SOLEIL, PLUIE, NUAGE }
public interface ObservateurMeteo {
void reagir(Meteo temps);
}
public class Ciel {
private static final Random GEN_ALEA = new Random();
private final List<ObservateurMeteo> observateurs = new ArrayList<>();
private Meteo temps = Meteo.SOLEIL;
public void add(ObservateurMeteo obs) { observateurs.add(obs); }
public void changer() {
temps = Meteo.values()[GEN_ALEA.nextInt(3)];
System.out.println("---Météo : " + temps + "---");
for (ObservateurMeteo obs : observateurs) {
obs.reagir(temps);
}
}
}Chaque personnage implémente reagir avec sa table de comportements — la table du sujet,
transposée :
public class Chat implements ObservateurMeteo {
@Override
public void reagir(Meteo temps) {
switch (temps) {
case SOLEIL -> System.out.println("Un chat se prélasse");
case PLUIE -> System.out.println("Un chat regarde à l'intérieur");
case NUAGE -> System.out.println("Un chat se balade dans le jardin");
}
}
}(idem pour Enfant, Vacanciere, Oiseau). Le main câble ciel + quatre observateurs et
fait changer la météo plusieurs fois.
Bonus — « s'il y a un chat qui se balade dans le jardin, l'oiseau se réfugie dans
l'arbre » : le chat doit à son tour être observable — il notifie ses propres observateurs
après avoir réagi (par ex. avec son comportement du moment), et Oiseau s'abonne au chat
en plus de s'abonner au ciel. Les observateurs peuvent donc observer d'autres observateurs :
le patron se compose.
Dernière mise à jour le