POO & Design Patterns

Le design pattern Visitor

Le patron Visitor

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.

Exemple

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.

Un exemple d'utilisation simple

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.

Un exemple plus compliqué

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.

Solution à base de 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.

Solution à base de polymorphisme (2)

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.

Le patron Visitor

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.

Le patron Visitor (2)

public void complexTreatement(Email email){
    ComplexTreatmentVisitor visitor = new ComplexTreatementVisitor();
    for(Attachment att : email.getAttachments()){
         visitor.visit(att);    
    }
}

Ne compile pas !!!!!!!!!

Pourquoi ?

Le patron Visitor (3)

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).

Le double dispatch

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 !!!
    }    
}

Le double dispatch: utilisation

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)

Mise en œuvre

  • Une interface pour les visiteurs Visitor
  • L'interface cible (des types à visiter) doit pouvoir accepter les visiteurs: méthode accept(Visitor visitor)
  • Dans chaque classe de l'interface cible, il faut écrire la méthode 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.

Diagramme Patron Visiteur

Nouveauté Java 17

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:

  • il y a de nombres langages objets qui n'ont pas le switch,
  • il y a aura encore beaucoup de code développé dans des versions antirieure à la 17.
  • Pour l'examen, le switch de Java 17 ne sera pas autorisé!

Le visitor à la sauce Java 17 (1/2)

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
    }
}

Le visitor à la sauce Java 17

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 !