POO & Design Patterns

Le patron Observer

Le patron Observer

Le patron Observer (GoF) permet à un objet d'être informé du changement d'état d'un autre objet sans créer de couplage fort entre les deux classes.

Il y a deux idées clé:

  • Une interface commune pour tous les observateurs (observers)
  • L'observé (observable) permet aux observateurs de s'inscrire et c'est lui qui les notifie.

Exemples:

  • re-dessiner la fenêtre quand l'état du plateau de jeu change,
  • faire un traitement à chaque commit sur git.

Exemple

public class Game{
    private String homeTeam;
    private int scoreHomeTeam;
    private String awayTeam;
    private int scoreAwayTeam;

    public String getHomeTeam() { ... }

    public String getAwayTeam() { ... }

    public int getHomeTeamScore() { ... }

    public int getAwayTeamScore() { ... }

    public void startGame(){ ... }

    public void endGame() { ... }

    public void setScore(int scoreHomeTeam, int scoreVisitorTeam) { ... }
} 

On veut:

  • envoyer un Tweet quand il y a des buts,
  • poster un article avec le score à la fin du match,
  • écrire le déroulé du match dans un fichier PDF...

Game devient observable

public interface GameObserver{
    public void onGameStart(Game game);
    public void onGameStop(Game game);
    public void onScoreChange(Game game);
}
public class Game {
    private String homeTeam;
    private int scoreHomeTeam;
    private String awayTeam;
    private int scoreAwayTeam;
    private final List<GameObserver> observers = new ArrayList<>();

    public void register(GameObserver obs){
        observers.add(Objects.requireNonNull(obs));
    }

    public void unregister(GameObserver obs){
        if (!observers.remove(Objects.requireNonNull(obs))){ throw new IllegalStateException(); }
    }

    public void startGame(){ 
       notifyGameStart();
       // ...
    }

    private void notifyGameStart(){
        for(var obs : observers){  // notify all observers
            obs.onGameStart(this);
        }
    }
    // ...
}

Observer Twitter

L'observer peut être intéressé seulement par certaines notifications.

public class TwitterObserver implements GameObserver {
    @Override
    public void onGameStart(Game game){
        // nothing
    }
    @Override    
    public void onGameStop(Game game){
        // nothing
    }
    @Override    
    public void onScoreChange(Game game){
        TwitterAPI.tweet(game.getHomeTeam() + ": " + game.getHomeTeamScore() + " versus " 
                       + game.getAwayTeam() + ": " + game.getAwayTeamScore());
    }
}

envoyer un Tweet quand il y a des buts

On peut ne rien faire par défaut..

public interface GameObserver {
    public default void onGameStart(Game game) { }
    public default void onGameStop(Game game) { }
    public default void onScoreChange(Game game) { }
}

... ce qui permet de ne spécifier que ce qui nous intéresse.

public class TwitterObserver implements GameObserver {
    @Override
    public void onScoreChange(Game game){
        TwitterAPI.tweet(game.getHomeTeam() + ": " + game.getHomeTeamScore() + " versus " 
                       + game.getAwayTeam() + ": " + game.getAwayTeamScore());
    }
}

envoyer un Tweet quand il y a des buts

Observer Summary

L'observer peut nécessiter de maintenir un état.

public class SummaryObserver implements GameObserver {
    private int scoreChanges;

    @Override
    public void onGameStop(Game game){
        PublishAPI.post("After " + scoreChanges + " score changes, the game ends with "
                      + game.getHomeTeamScore() + " for " + game.getHomeTeam() + " versus " 
                      + game.getAwayTeamScore() + " for " + game.getAwayTeam());
    }    
    @Override
    public void onScoreChange(Game game){
         scoreChanges++;
    }
}

poster un article avec le score à la fin du match

Observer PDFGenerator

L'observer peut offrir d'autres méthodes.

public class PDFGeneratorObserver implements GameObserver {
    private final PdfGenerator<GameReportStyle> pdfGen = new PdfGenerator<>();

    public void onGameStart(Game game){
        pdfGen.addTitle(game.getHomeTeam() + " versus " + game.getAwayTeam());
        pdfGen.addParagraph("Game starts");
    }
    public void onGameStop(Game game){
        pdfGen.addParagraph("Game ends between " + game.getHomeTeam() + " and " + game.getAwayTeam());
        pdfGen.addParagraph("Final score: " + game.getHomeTeamScore() + "/" + game.getAwayTeamScore());
    }
    public void onScoreChange(Game game){
    	pdfGen.addParagraph("Score changes: " + game.getHomeTeamScore() + "/" + game.getAwayTeamScore());
    }
    public void saveReportInFile(Path pdfFile) throws IOException {
    	pdfGen.writeIn(pdfFile);
    }
}

écrire le déroulé du match dans un fichier PDF...

Push vs Pull

Dans notre exemple, les observeurs sont pull, c'est à dire qu'on les notifie et qu'ils vont chercher les informations dans l'objet observé.

On peut passer les informations (par exemple, le score) au moment de la notification, on parle d'observers push.

Avantages/Inconvénients

Comme toujours avec les design patterns, on peut faire un mix des deux et adapter aux besoins.

Push vs Pull

En Pull, on donne plus de flexibilité aux observateurs mais on doit faire des méthodes publiques pour que l'observateur est accès aux informations. On affaiblit l'encapsulation.

En Push, on construit l'ensemble des informations dont l'observeur a besoin. Cela peut-être coûteux et c'est moins flexible.

Il n'y a pas de bonne ou mauvaise solution, il faut réfléchir à chaque fois.

Diagramme UML