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é.
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 ?
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
.
BufferedWriter
avec:
Files.newBufferedWriter(path, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
PathLogger
pourra implémenter
l'interface Closeable
.IOException
levées par BufferedWriter.write()
et BufferedWrite.flush()
comme des
UncheckedIOException
.
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.