Préfixé par ✔️, des "checkpoints" pour vous aider à vérifier que vous avez tout bon.
La correction sera automatique, prenez donc soin de respecter les indications les plus précises.
Ici nous allons créer un simple jeu ou le joueur devra deviner le nombre choisi par l’ordinateur’.
L’objectif de cet exercice est de prendre en main le concept de découplage (ou couplage faible / lâche) en manipulant des interfaces ainsi que leurs implémentations.
-
Git
-
Java 21
-
Maven 3.9.x
-
(Optionnel, mais fortement recommandé) IntelliJ edition community 2024
-
Sur la page du template https://github.com/lernejo/maven-starter-template, cliquer sur "Use this template"
-
⚠️ Renseigner comme nom de dépôt : decoupling_java_training -
Marquer le futur dépôt comme private
-
Une fois le dépôt créé, installer l’app Korekto, ou mettre à jour sa configuration afin qu’elle ait accès à ce nouveau dépôt
-
Cloner le dépôt en utilisant l'url SSH
-
La branche par défaut est la branche main, c’est sur celle-ci que nous allons travailler
Ce qu’on appelle couramment Logger est un objet qui a la responsabilité de produire le journal applicatif.
Ce journal, qu’il soit dans la console ou dans un fichier permet de comprendre ce que fait le programme au travers de messages, qu’ils soient :
- critiques (ex : le serveur distant n’est plus joignable)
- informatifs (ex : tel utilisateur a fait telle opération)
- de "debug" (ex : la requête au serveur distant a pris 39ms)
-
Pour commencer, créer un package spécifique :
fr.lernejo.logger
.
ℹ️
|
Un package est une succession de répertoires depuis le répertoire "racine" des sources (conventionnellement src/main/java dans un projet Maven).
Tous les fichiers Java dans un package donné comportent comme première ligne la déclaration du package dans lequel ils sont : package fr.lernejo.logger;
// Suite du fichier (class, enum ou interface) |
-
Dans ce package, créer une interface
Logger
avec une seule méthode abstraite :
void log(String message);
-
Créer ensuite une classe
ConsoleLogger
implémentantLogger
et affichant lemessage
passé en paramètre dans la console en utilisantSystem.out
.
ℹ️
|
Implémenter une interface revient à la déclarer dans la classe. Pour un objet public class Dog implements Pet {
// contenu de la classe (attributs, méthodes)
} |
-
Créer enfin une classe
LoggerFactory
ayant une méthode publique et statiquegetLogger(String name)
retournant un objet de typeLogger
(c’est-à-dire implémentant l’interfaceLogger
).
Dans un premier temps, le paramètre name
ne servira à rien.
-
Indexer et commiter les fichiers nouvellement créés
Le jeu ici sera de deviner un nombre que l’ordinateur aura choisi.
Le joueur aura un retour après chaque tentative : plus grand ou plus petit.
-
Dans un package
fr.lernejo.guessgame
créer l’interfacePlayer
.
Cette dernière aura les méthodes suivantes :
long askNextGuess();
/**
* Called by {@link Simulation} to inform that the previous guess was lower or greater that the number to find.
*/
void respond(boolean lowerOrGreater);
-
Créer une première implémentation qui permettra l’interfaçage avec un utilisateur humain (IHM, pour Interface Homme Machine)
HumanPlayer
.
Cette classe utilisera :-
D’une part une instance de
Logger
donnée parLoggerFactory
avec l’argument"player"
-
D’autre part la classe
java.util.Scanner
de Java permettant de récupérer les entrées de l’utilisateur dans la console
-
-
Créer une classe
Simulation
telle que:
public class Simulation {
private final Logger logger = LoggerFactory.getLogger("simulation");
private final ??? player; //TODO add variable type
private ??? numberToGuess; //TODO add variable type
public Simulation(Player player) {
//TODO implement me
}
public void initialize(long numberToGuess) {
//TODO implement me
}
/**
* @return true if the player have guessed the right number
*/
private boolean nextRound() {
//TODO implement me
return false;
}
public void loopUntilPlayerSucceed() {
//TODO implement me
}
}
-
Le constructeur permettra de renseigner les champs
private
qui seront utilisés à chaque tour de jeu.
La méthodenextRound
devra :-
Demander un nombre au joueur
-
Vérifier s’il est égal, plus grand ou plus petit
-
S’il est égal, retourner
true
-
Sinon, donner l’indice (plus grand ou plus petit) au joueur et retourner
false
-
L’implémentation de
Player
afficher vialogger
les informations permettant au joueur humain de faire sa prochaine hypothèse
-
-
La méthode
loopUntilPlayerSucceed
devra utiliser une boucle afin d’appelernextRound
jusqu’à ce que la partie soit finie. -
Quand la partie est finie, afficher un message adéquat (ex:
you won !
,c’est gagné
, etc.) -
Créer enfin une classe
Launcher
avec une méthode statiquemain
qui-
Créera une nouvelle instance de
Simulation
avec un joueurHumanPlayer
-
Initialisera cette instance avec un nombre aléatoire, généré par la classe
java.security.SecureRandom
-
SecureRandom random = new SecureRandom();
// long randomNumber = random.nextLong(); // génère un nombre entre Long.MIN_VALUE et Long.MAX_VALUE
long randomNumber = random.nextInt(100); // génère un nombre entre 0 (inclus) et 100 (exclus)
-
Lancera une partie en appelant la méthode
loopUntilPlayerSucceed
-
Indexer et commiter les fichiers nouvellement créés
Le but de cet exercice est de créer une seconde implémentation de Player
: ComputerPlayer
.
Cette nouvelle classe aura la même fonction que HumanPlayer
, mais sans demander à l’utilisateur quoi que ce soit.
L’algorithme de recherche par dichotomie pouvant ne pas converger du premier coup, nous allons ajouter une sécurité.
-
Modifier dans la classe
Simulation
la méthodeloopUntilPlayerSucceed
afin que celle-ci prenne en paramètre un nombre qui sera le maximum d’itérations de la boucle.
Cette même méthode devra également afficher à la fin de la partie le temps que celle-ci a pris au formatmm:ss.SSS
et si oui ou non le joueur a trouvé la solution avant la limite d’itération.
Récupérer un timestamp se fait avec le code System.currentTimeMillis()
.
La valeur retournée correspond au nombre de millisecondes entre le 1er Janvier 1970 et le moment où la fonction est appelée.
-
Modifier la classe
Launcher
afin que celle-ci gère 3 cas par rapport aux paramètres passés en ligne de commande (String[] args
):-
Si le premier argument vaut
-interactive
, alors utiliser la précédente façon de lancer le programme avec unHumanPlayer
avec une limite d’itérations valantLong.MAX_VALUE
-
Si le premier argument vaut
-auto
et le second argument est numérique, alors-
Créer une nouvelle instance de
Simulation
avec un joueurComputerPlayer
-
Initialiser cette instance avec le nombre donné comme second argument
-
Lancer une partie en appelant la méthode
loopUntilPlayerSucceed
et avec comme limite d’itération 1000
-
-
Sinon afficher les deux "façons" de lancer le programme décrites ci-dessus afin de guider l’utilisateur
-
-
Enfin, implémenter les méthodes de la classe
ComputerPlayer
afin que la recherche de l’age du capitaine converge vers la solution. -
Indexer et commiter les fichiers nouvellement créés
À ce stade, des messages de logs provenant des classes Launcher
, Simulation
, HumanPlayer
et ComputerPlayer
se mélangent dans la console sans moyen de les distinguer.
-
Créer dans le package
fr.lernejo.logger
une nouvelle classeContextualLogger
implémentantLogger
, qui prendra le nom d’une classe, ainsi qu’un autreLogger
en paramètres de constructeur.
Le but de ceLogger
sera d’enrichir le message avec la date courante et le nom de la classe appelante.
Il est nécessaire pour cela d’utiliser la classe java.time.format.DateTimeFormatter
avec un pattern tel que "yyyy-MM-dd HH:mm:ss.SSS"
.
La méthode log
de cette implémentation devra elle-même appeler la méthode log
de l’objet Logger
passé par construction.
public void log(String message) {
delegateLogger.log(LocalDateTime.now().format(formatter) + " " + callerClass + " " + message);
}
-
Modifier la classe
LoggerFactory
pour qu’elle produise une instance deLogger
qui produira des messages enrichis dans la Console. -
Lancer le programme et vérifier que les messages apparaissent bien datés et avec la classe d’origine
En procédant ainsi on compose les objets Logger
sans modifier leur comportement interne.
Il est alors plus simple de remplacer, ConsoleLogger
par un objet de type FileLogger
qui ajouterai les messages dans un fichier tout en gardant le même enrichissement de message.
-
Écrire la classe
FileLogger
en utilisant le code ci-dessous :
import static java.nio.file.StandardOpenOption.APPEND;
import static java.nio.file.StandardOpenOption.CREATE;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileLogger implements Logger {
private final Path path;
public FileLogger(String pathAsString) {
path = Paths.get(pathAsString).toAbsolutePath();
}
public void log(String message) {
try {
Files.write(path, (message + "\n").getBytes(), APPEND, CREATE);
} catch (IOException e) {
throw new RuntimeException("Cannot write log message to file [" + path + "]", e);
}
}
}
-
Modifier le code de
LoggerFactory
afin que les messages soient produits dans un fichier sur le disque:target/captain.log
-
Lancer le programme et vérifier que les messages apparaissent bien datés et avec la classe d’origine dans le fichier spécifié dans la classe
LoggerFactory
-
Indexer et commiter les fichiers nouvellement créés
Ajouter les messages dans un fichier est pratique pour comprendre ce qui s’est passé a posteriori, cependant ce n’est pas pratique pour le développement.
Nous allons donc combiner les deux loggers précédents en un seul.
-
Créer une nouvelle classe
CompositeLogger
implémentantLogger
Cette classe aura un constructeur prenant deuxLogger
en paramètres.
La méthodelog
appellera successivementlog
sur les deuxLogger
renseignés par construction. -
Modifier la classe
LoggerFactory
pour qu’elle renvoie un seulLogger
écrivant les messages à la fois dans la Console et dans un fichier. -
Indexer et commiter les fichiers nouvellement créés
Afin d’y voir plus clair dans le diagnostic d’un comportement au travers d’un fichier de log, il peut être utile de filtrer certains messages afin de ne garder que ceux qui ont de l’intérêt.
Nous allons donc filtrer les messages provenant des classes implémentant Player
pour le FileLogger
.
-
Créer une classe
FilteredLogger
implémentantLogger
qui aura un constructeur avec deux paramètres :
public FilteredLogger(Logger delegate, Predicate<String> condition) {
//TODO assign arguments to fields
}
-
Implémenter la méthode log en testant si la condition valide le message donné en paramètre.
Si la condition est vérifiée, appeler leLogger
delegate avec le même paramètre.
L’interface java.util.function.Predicate
modélise une condition sur un objet dont le type est spécifié entre chevron (ici String
).
Il est possible de l’implémenter de deux façons :
- avec une classe implémentant l’interface Predicate
- avec une lambda, ex: Predicate<String> condition = message → !message.contains("player");
.
Tous les messages qui ne contiennent pas le mot "player"
valident cette condition.
-
Modifier la classe
LoggerFactory
pour qu’elle produise unLogger
qui affichera tous les messages dans la console et n’affichera que les messages de la classeSimulation
dans un fichier.
Les messages doivent tous être horodatés et indiquer de quelle classe ils proviennent.