Le patron Visitor permet d'ajouter des fonctionnalités à des classes qui implémentent une interface sans modifier l'interface ni le code des classes.
⇛ Open-close principle.
public sealed interface Attachment permits Zip,Image,Video{ public String getName(); public Path getPath(); } public final class Zip implements Attachment { ... public String getName(){...} public boolean isEncrypted(){...} public Path getPath(){...} } public final class Image implements Attachment { ... public String getName(){...} public ImageType getType(){...} public Path getPath(){...} } public final class Video implements Attachment { ... public String getName(){...} public int getDurationInSeconds(){...} public Path getPath(){...} }
Une interface qui représente le type des différents attachements possibles dans des mails.
public class Email{ ... public String getSender(){...} public List<Attachment> getAttachments(){...} }
public void simpleMailTreatement(Email email){ for(Attachment att : email.getAttachments()){ System.out.println(att.getName()); } }
Jusqu'ici tout va bien, on utilise le polymorphisme.
public void complexTreatement(Email email){ for(Attachment att : email.getAttachments()){ if (att instanceof Zip){ Zip zip = (Zip) att; if (zip.isEncrypted()){ String password = UI.promptPassword(); Zip.decryptes(zip.getPath(),password); } } if (att instanceof Image){ Image image = (Image) att; Files.mv(image.getPath(),Email.imageFolder()); } ... } }
L'utilisation de instanceof
est un code smell. Le premier réflexe doit être d'utiliser le polymorphisme.
public void complexTreatement(Email email){ for(Attachment att : email.getAttachments()){ att.complexTreatment(); // method resolution is done w.r.t the receiver: single dispatch } }
public interface Attachment{ public String getName(); public Path getPath(); public void complexTreatment(); // method declaration to offer the functionality (entry point) } public class Zip implements Attachment { ... public void complexTreatment() { // specific method implementation for each concrete type if (isEncrypted()){ String password = UI.promptPassword(); Zip.decryptes(getPath(),password); } } } public class Image implements Attachment { ... public void complexTreatment() { // specific method implementation for each concrete type Files.mv(getPath(),Email.imageFolder()); } } ...
A priori fonctionnel, mais très lié au traitement.
Que va-t-il se passer si on a plusieurs traitements différents à faire sur les attachments?
att.complexTreatment(); att.complexTreatmentBis(); att.complexTreatmentTer();
Les classes qui implémentent Attachment
vont avoir trop de responsabilités.
public interface Attachment{ public String getName(); public Path getPath(); public void complexTreatment(); // multiple method declaration to offer multiple entry points public void complexTreatmentBis(); public void complexTreatmentTer(); }
On ne respecte ni le Single Responsability Principle ni le Interface segregation principle.
L'idée est de regrouper dans une même classe les traitements à effectuer sur chaque type concret d'Attachment
:
public interface AttachmentVisitor{ public void visit(Zip zip); public void visit(Image image); public void visit(Video video); }
public class ComplexTreatmentVisitor implements AttachmentVisitor{ public void visit(Zip zip){ if (zip.isEncrypted()){ String password = UI.promptPassword(); Zip.decryptes(zip.getPath(), password); } } public void visit(Image image){ Files.move(image.getPath(), Email.imageFolder()); } public void visit(Video video){ // do nothing } }
Cette classe est un visiteur, chargé de "visiter" les attachements.
public void complexTreatement(Email email){ ComplexTreatmentVisitor visitor = new ComplexTreatementVisitor(); for(Attachment att : email.getAttachments()){ visitor.visit(att); } }
⇛ Ne compile pas !!!!!!!!!
Pourquoi ?
public void complexTreatement(Email email){ ComplexTreatmentVisitor visitor = new ComplexTreatementVisitor(); for(Attachment att : email.getAttachments()){ visitor.visit(att); } }
⇛ Ne compile pas !!!!!!!!!
1. Le compilateur ne trouve pas de méthode "générique"
visit(Attachement att)
dans l'interface (on ne sait visiter QUE des classes concrètes).
2. Il n'y a pas de polymorphisme sur l'argument dans visitor.visit(att)
, contrairement au receveur dans att.complexTreatment()
3. Il faudrait "aider" le compilateur à choisir la bonne méthode visit()
en fonction de l'attachement concret (Zip, Image, Video
).
L'idée-clé: faire passer le visiteur en paramètre aux Attachment
.
public interface Attachment{ public String getName(); public Path getPath(); public void accept(AttachmentVisitor visitor); }
Pour que chaque attachement concret appelle la "bonne" méthode visit().
public class Zip implements Attachment { @Override public void accept(AttachmentVisitor visitor){ visitor.visit(this); // here this has type Zip !!! } } public class Image implements Attachment { @Override public void accept(AttachmentVisitor visitor){ visitor.visit(this); // here this has type Image !!! } } public class Video implements Attachment { @Override public void accept(AttachmentVisitor visitor){ visitor.visit(this); // here this has type Video !!! } }
public void complexTreatement(Email email){ ComplexTreatmentVisitor visitor = new ComplexTreatementVisitor(); for(Attachement att : email.getAttachments()){ att.accept(visitor); } }
Le polyorphisme sur att
va dynamiquement appeler la méthode accept(visitor)
de l'attachement concret rencontré (Zip
ou Image
ou Video
) qui appellera à son tour la "bonne" méthode
visitor.visit(Zip)
ou visitor.visit(Image)
ou visitor.visit(Video)
Visitor
accept(Visitor visitor)
accept(Visitor visitor)
Le code de la méthode accept(Visitor visitor)
semble être le même dans toutes les classes ...
pourquoi ne pas le mettre en default
dans l'interface ?
Tous les traitements peuvent être ainsi acceptés s'ils sont représentés par un visiteur (même interface): Open-close principle
Dans notre exemple, la méthode visit
de l'interface Visitor
ne renvoie rien, cela n'a rien d'obligatoire.
Depuis la version 17, Java permet de faire des switch
sur des interfaces
sealed
.
C'est beaucoup plus simple à mettre en oeuvre que le pattern visiteur.
Mais il faut savoir utiliser et implémenter le pattern visiteur car:
Pour l'examen, le switch de Java 17 ne sera pas autorisé!
public interface AttachmentProcessor { public void process(Zip zip); public void process(Image image); public void process(Video video); default public void process(Attachment att){ switch(att){ case Zip zip -> process(zip); case Image image -> process(image); case Video video -> process(video); } } } public class ComplexTreatmentProcessor implements AttachmentProcessor{ @Override public void process(Zip zip){ if (zip.isEncrypted()){ String password = UI.promptPassword(); Zip.decryptes(zip.getPath(), password); } } @Override public void process(Image image){ Files.move(image.getPath(), Email.imageFolder()); } @Override public void process(Video video){ // do nothing } }
public void complexTreatement(Email email){ var processor = new ComplexTreatementProcessor(); for(Attachment att : email.getAttachments()){ processor.process(att); } }
On n'a plus besoin du double dispatch !