Exercices Patron Visitor

Simple Time Protocol

Dans tout l'exercice, le switch sur les classes introduit en Java 17 est interdit sauf mention explicite du contraire.

Le Simple Time Protocol (STP) est un protocole qui fournit une liste de commandes pour manipuler des chronomètres:

La compagnie EvilCorp (pour qui vous travaillez) fournit une librairie qui parse les commandes du Simple Time Protocol (STP). La librairie qui peut être récupérée ici définit une interface STPCommand commune à toutes les commandes et offre une classe pour représenter chacune des quatre commandes. La classe STPParser offre une static factory method permettant de convertir une chaîne de caractères en l'objet STPCommand si c'est possible:

public static Optional<STPCommand> parse(String cmd);

La librairie a été mal conçue. Le but serait que cette librairie soit utilisable dans plusieurs applications qui utilisent le protocole Simple Time Protocol (STP) pour des usages différents.

Pour comprendre le problème, écrivez une application Triviale qui utilise la librairie pour parser les commandes lues au clavier et qui affiche "Pas compris" lorsqu'elle lit une ligne non reconnue par le parser de la librairie, "Au revoir" lorsqu'elle rencontre la commande hello et "non implémenté" pour toutes les autres commandes.

Quel principe SOLID n'est pas implémenté par la librairie stp ?

Comment pourrait-on résoudre le problème en utilisant le polymorphisme ? Est-ce que c'est envisageable dans cette situation ?

Quel patron permet d'ajouter des nouvelles opérations aux classes implémentant l'interface STPCommand sans modifier ces classes ?

Modifiez la librairie fournie par EvilCorp pour la rendre utilisable avec ce patron et récrivez la petite application Triviale demandée précédemment.

Modifiez la classe Application.java, qui implémente une gestion de chronomètres en local, pour utiliser cette nouvelle librairie.

Faites un version moderne de la libaire STP dans un nouveau package com.evilcorp.stphipster qui utilise toutes les fonctionnalités de Java 17. Les classes des commandes seront des record et l'interface STPCommand sera sealed (votre interface devait déjà être sealed même si vous n'utilisez pas les switchs). Réécrivez la classe Application dans une classe ApplicationHipster en utilisant les switchs de Java 17.

A faire chez vous: Très content de votre travail, votre patron vous demande d'étendre l'application pour qu'elle:

Il est fort probable que vous ayez à écrire de nombreuses extensions de ce type, donc votre patron vous demande une architecture logicielle facilement extensible.

Modifiez votre application pour permettre l'ajout de ces fonctionnalités et ajoutez les deux fonctionnalités demandées. Vous continuerez à utiliser votre classe Aplication qui utilise des visiteurs et pas la classe de la question précédente.

Une calculette !

Le but de cet exercice est de modifier un code existant permettant d'effectuer le calcul d'une expression en notation polonaise inverse dans le but de fermer la hiérarchie d'expressions tout en laissant la possibilité à un utilisateur d'écrire ses propres traitements.

Une expression arithmétique sera représenté par objet de l'interface Expr qui est téléchargeable ici. Cette interface scellée autorise des records Value représentant des valeurs entières et des records BinOp qui permettent de représenter des opérations binaires quelconques.

Le code suivant permet d'évaluer une expression en polonaise inverse:

var iterator = Pattern.compile(" ").splitAsStream("+ * 4 + 1 1 + 2 3").iterator();
var expr = Expr.parse(iterator);
System.out.println(expr);
System.out.println(expr.eval());

Dessinez l'arbre d'expressions créé par le code ci-dessus puis dessinez sur l'arbre l'évaluation de l'expression (c'est-à-dire un appel à la méthode eval()).

Modifiez le code pour introduire le visiteur ExprVisitor, comme déclaré ci-dessous:

public interface ExprVisitor {
    public int visitValue(Value value);
    public int visitBinOp(BinOp binOp);
}

Ecrivez une classe EvalExprVisitor qui effectue l'évaluation en utilisant le patron visitor plutôt qu'en utilisant la méthode eval. Une fois que l'EvalExprVisitor fonctionne, supprimez la méthode eval de l'interface Expr (et des sous classes).

On souhaite faire la même transformation mais avec la méthode toString à la place de la méthode eval. Comme toString retourne une String et pas un int comme eval, il faut faire évoluer l'interface ExprVisitor.

Après avoir modifié votre interface ExprVisitor, écrivez une classe ToStringVisitor qui effectue le même travail que toString puis retirez les méthodes toString une fois que le visiteur marche.

Le toStringVisitor que vous avez écrit (comme les méthodes toString qu'il y avait précédemment) n'est pas très efficace car il y a beaucoup de créations de String intermédiaires; il serait préférable d'utiliser un seul StringBuilder pour tout le calcul.

Modifiez le ToStringVisitor pour n'utiliser qu'un seul StringBuilder.

Note: cela nécessite peut-être de modifier l'interface ExprVisitor, ou pas, il y a plusieurs solutions.

Le visiteur avec des lambdas

Revenons sur ce qu'est un visiteur: un visiteur est un objet qui associe à une classe (Value ou BinOp) un code à exécuter. Il peut donc être implanté avec une table de hachage qui associe à une classe (un objet de type java.lang.Class) une lambda contenant le code à exécuter.
On se propose d'écrire la classe ExprVisitor de telle sorte ce que le code suivant fonctionne:

ExprVisitor<Integer> evalVisitor = new ExprVisitor<>();
evalVisitor
  .when(Value.class, value -> {
    ...
  })
  .when(BinOp.class, binOp -> {
    ...
  });
evalVisitor.visit(expr); 

Quelle doit être la signature de la méthode when ? Quelle est la signature de la méthode visit ? Quelle structure de données doit être utilisée pour implanter les méthodes when et visit ?

Ecrire le code de la classe ExprVisitor.

Implémentez les visiteurs correspondant aux méthodes eval et toString.

Dans l'exercice précédent, on a réalisé le visiteur pour la méthode toString() en utilisant un seul StringBuilder. Comment adapter la classe ExprVisitor pour pouvoir faire de même ici ?

Réalisez un visiteur utilisant un seul StringBuilder calculant la méthode toString.

Quels sont les avantages et inconvénients d'écrire le code du visiteur de cette façon là par rapport au visiteur classique du GOF ou à utiliser un switch de Java 17?

Explorateur de fichiers

Le but de l'exercice est créer un petite librairie permettant d'accéder à un sytème de fichier. L'API Java contient déjà de nombreuses classes remplissant ce rôle, il ne s'agit donc vraiment que d'un exercice.

Dans un premier temps, on veut faire un librairie pour Java 23.

Ecrivez une hiérarchie de classes permettant de représenter un système de fichier contenant à la fois des fichiers et des répertoires. Pour les fichiers, on donnera le Path, le nom du fichier et l'extension et pour les répertoires, on donnera le Path, le nom du répertoire et son contenu. Quel design pattern avez-vous utilisé ?

Complétez votre code pour pouvoir construire un objet de votre hiérarchie à partir d'un Path avec un méthode static of(Path path).
Vous implanterez votre librairie avec la méthode Files.list(Path path) qui renvoie un Stream<Path> contenant les éléments du répertoire.

Dans une classe Application, écrivez une méthode static findFilesWithExtension(Path directory, String extension) qui renvoie la liste des noms des tous les fichiers ayant une extension donnée en utilisant votre librairie.

Le problème de votre librairie est qu'elle construit directement un énorme objet même si l'objet n'est pas utilisé. On voudrait rajouter un objet construit de façon lazy : le contenu du répertoire n'est calculé que si on y accède. Rajoutez une méthode static ofLazy(Path path). Attention, le code qui était écrit pour la version précédente de la librairie doit continuer à fonctionner. L'utilisateur ne doit voir aucune différence entre les versions lazy et non-lazy outre l'appel ofLazy.

On veut maintenant une version de la librairie utilisable sans le switch sur les interfaces sealed. Modifiez le code de votre librairie et la classe Application.

Idéalement, on voudrait ne pas créer de structure intermédiaire représentant le système de fichier car cet ensemble d'objet peut être énorme. C'est ce que permet FileVisitor. Après avoir compris cette classe, quels sont les inconvénients de cette approche.