Exercices Patron Proxy-Décorateurs

Imagine

Votre entreprise Evil Corp. a édité une petit librairie qui permet de télécharger des images sur Internet et de récupérer quelques informations.

Pour des raisons pratiques, la librairie simule simplement le téléchargement. Elle affiche aussi des informations dans la console ce qui n'est pas une bonne pratique. Mais bon, c'est un exercice, vous ne travaillez pas (encore) pour Evil Corp.

Téléchargez la librairie Image.java

La librairie est très simple d'utilisation:

public static void main(String[] args) {
    var map = Map.of("cat","http://www.example.com/cat.png",
            "dog","http://www.example.com/dog.png",
            "mice","http://www.example.com/mice.png");
    var images =map.values().stream().map(Image::download).toList();
    System.out.println(images.get(0).hue());
}    

Le problème rencontré par les clients, c'est que leur code télécharge toutes les images même celles qui ne seront jamais utilisées. Bien sûr, les clients pourraient modifier leur code mais il n'y aurait pas d'exercice ! On vous demande de rajouter un méthode static Image downloadLazy(String url) qui renvoie une image qui ne sera effectivement téléchargée que lorsqu'on utilisera une de ses méthodes.

(Bonus) Le code des clients écrit avant cette modification doit continuer à fonctionner. Si les clients remplacent les appels à Image.download par des appels à Image.downloadLazy, leur code doit compiler.

Modifiez la libraire Image pour obtenir le résultat souhaité.

Canard et limitations des patterns Decorator et Proxy

Le but de cet exercice est de toucher du doigt une des limites des patterns Decorator et/ou Proxy qui est qu'on ne peut pas intercepter tous les appels à des méthodes publiques. Si une méthode publique d'une classe fait elle-même appel à une méthode publique de cette classe, cet appel ne sera pas interceptable.

Pour observer ce phénomène, considèrons les différentes classes implémentant l'interface Duck données ci-dessous:

public interface Duck {
    void quack();
    void quackManyTimes(int n);
}

public class AtomicDuck implements Duck{
    @Override
    public void quack() {
        System.out.println("Atomic Quack");
    }

    @Override
    public void quackManyTimes(int n) {
        for(int i=0; i < n; i++){
            quack();
        }
    }

}

public class RegularDuck implements Duck {

    @Override
    public void quack() {
        System.out.println("Regular Quack !");
    }

    @Override
    public void quackManyTimes(int n) {
        for(int i=0; i < n; i++){
            quack();
        }
    }
}

Ecrivez un décorateur LoggedDuck qui va logger sur la console à chaque fois qu'un Duck fait un quack et vérifiez que votre code fonctionne avec le main ci-dessous:

   public static void main(String[] args) {
        Duck duck1 = new RegularDuck();
        duck1.quack();
        Duck duck2 = new LoggedDuck(new RegularDuck());
        duck2.quack();
        duck1.quack();
        duck2.quack();
    }

Expliquez pourquoi votre décorateur ne logge aucun quack avec le code ci-dessous:

   public static void main(String[] args) {
        Duck duck = new LoggedDuck(new RegularDuck());
        duck.quackManyTimes(2);
    }

Vous devriez mieux comprendre maintenant pourquoi l'on vous a toujours déconseillé qu'une méthode publique appelle une autre méthode publique. Remarquez bien que ce n'est pas un interdit absolu.

Mettez le code de quackManyTimes en default dans l'interface. Expliquez pourquoi on obtient bien le comportement attendu. Est-ce que cette solution est robuste ?

Logger

Dans cet exercice, on imagine que vous travaillez sur une librairie. Contrairement à la majorité des exercices, vous avez le droit de modifier le code de la libraire pour en faire une version 2. Idéalement, on aimerait que le code qui était écrit pour la version 1 de la librairie fonctionne avec la version 2 de la librairie.

Suite au bug trouvé dans log4j, Evil Corp. décide de développer son propre logger.

Pour l'instant, la librairie est très modeste et ne permet d'écrire que sur la sortie d'erreur.

Récupérez la librairie SystemLogger.java.

On souhaite aussi pouvoir logger des messages dans un fichier, on se propose donc d'écrire une classe PathLogger qui prend un Path à la construction, ouvre un BufferedWriter sur un fichier et écrit les logs dedans.
En tant qu'utilisateur, on veut pouvoir manipuler indifféremment un SystemLogger ou un PathLogger à travers la même API.

Ecrivez la classe PathLogger.

On peut remarquer que créer plusieurs instances de SystemLogger est dommage car on pourrait partager la même instance.

Quel design pattern doit-on utiliser dans ce cas ? Faites les changements qui s'impose dans la classe SystemLogger.

On souhaite pouvoir filtrer les logs de nos loggeurs (PathLogger et SystemLogger, c'est-à-dire afficher les logs qui sont plus grands ou plus petits qu'un certain Level.

Ajoutez cette fonctionnalité à votre librairie.

Enfin, on souhaite pouvoir créer un loggeur qui affiche ses logs et sur la console et dans un fichier. Par exemple, on voudrait pouvoir afficher sur la sortie d'erreur les logs et écrire dans un fichier "/tmp/logs.txt" les logs WARNING et ERROR.

Ajoutez cette fonctionnalité à votre librairie. Dans le main d'une classe Application, construisez le loggeur mentionné ci-dessus.