Dans cette séance, nous allons voir deux patrons qui permettent d'ajouter des fonctionnalités à une classe sans l'ouvrir et sans lui rajouter de responsabilité.
Open-Close principle + Single Responsability principle
L'idée est simple: on utilise la composition à la place de l'héritage.
public interface Duck { void quack(); } public class RegularDuck implements Duck { @Override public void quack() { System.out.println("Quack !"); } } public class AtomicDuck implements Duck{ @Override public void quack() { System.out.println("Atomic Quack"); } }
On voudrait pouvoir rajouter la possibilité de mettre un log quand la méthode quack
est appelée.
public class LoggedDuck implements Duck{ private static final Logger LOGGER = Logger.getLogger(LoggedDuck.class.getClass().getName()); private final Duck duck; public LoggedDuck(Duck duck) { this.duck = duck; } @Override public void quack() { LOGGER.log(Level.INFO,"Call to Quack!"); duck.quack(); } }
LoggedDuck
prend n'importe quel Duck
à la création.LoggedDuck
est un Duck
public class Ducks { public static void main(String[] args) { Duck duck1 = new RegularDuck(); Duck duck2 = new LoggedDuck(new RegularDuck()); Duck duck3 = new LoggedDuck(new AtomicDuck()); duck1.quack(); duck2.quack(); duck3.quack(); } }
On peut rajouter une fonctionnalité à tous les Duck
sans ouvrir les classes et même pour des Duck
qui n'ont pas encore été écrits.
public interface PrimeGenerator { long getNthPrime(int n); }
public class NaivePrimeGenerator implements PrimeGenerator { @Override public long getNthPrime(int n) { if (n<1){throw new IllegalArgumentException();} var currentCandidate=2L; for(var i=0;i<n-1;i++){ currentCandidate=nextPrime(currentCandidate); } return currentCandidate; } } ...
public class ErathosthenePrimeGenerator implements PrimeGenerator { @Override public long getNthPrime(int n) { // implémentation basée sur le crible d'Erathosthene } }
On voudrait pouvoir rajouter plusieurs optimisation à nos générateurs:
On ne veut pas dupliquer de code et on veut laisser le choix à l'utilisateur d'utiliser ou pas ces optimisations: une ou les deux.
public class CachedPrimeGenerator implements PrimeGenerator { private final PrimeGenerator generator; private final HashMap<Integer,Long> cache = new HashMap<>(); public CachedPrimeGenerator(PrimeGenerator generator) { this.generator = generator; } @Override public long getNthPrime(int n) { return cache.computeIfAbsent(n,generator::getNthPrime); } }
CachedPrimeGenerator
prend n'importe quel PrimeGenerator
à la construction.CachedPrimeGenerator
est lui-même un
PrimeGenerator
.public class PreComputedPrimeGenerator implements PrimeGenerator { private static final List<Long> FIRST_PRIMES = List.of(2L, 3L, 5L, 7L, 11L, 13L, 17L, 19L, 23L, 29L); private final PrimeGenerator generator; public PreComputedPrimeGenerator(PrimeGenerator generator) { this.generator = generator; } @Override public long getNthPrime(int n) { if (n<1) {throw new IllegalArgumentException();} if (n<FIRST_PRIMES.size()+1) {return FIRST_PRIMES.get(n-1);} return generator.getNthPrime(n); } }
public static void main(String[] args) { var generator = new PreComputedPrimeGenerator( new CachedPrimeGenerator( new NaivePrimeGenerator())); // var generator = new CachedPrimeGenerator( // new ErathosthenePrimeGenerator()); var nbPrimes = 10_000; var sum = IntStream.range(1,nbPrimes) .mapToLong(generator::getNthPrime) .sum(); }
L'utilisateur peut choisir son générateur de base et combiner les décorateurs à l'envie.
LoggedDuck
est un décorateur pour l'interface Duck
.
PreComputedPrimeGenerator
et CachedPrimeGenerator
sont des décorateurs pour l'interface PrimeGenerator
.
Un décorateur décore une interface A, il prend à la construction un objet de A et est lui-même de type A. In fine, il va appeler les méthodes de l'objet qu'il contient en faisant du travail avant et/ou après.
On trouve dans la littérature une version avec un classe abstraite ProductDecorator
pour factoriser le code des décorateurs.
N'introduire cette classe abstraite que si nécessaire!
Le patron Proxy
est une variante du décorateur.
Du point de vue du code, il y a peu de différence. La différence est essentiellement philosophique:
Il faut voir le proxy comme enveloppe autour de l'objet invisible pour l'utilisateur.
public interface WeatherService { int query(String city); }
public class WeatherServiceNonThreadSafe { // non-thread safe implementation }
public class WeatherServiceProxy implements WeatherService { private final Object lock = new Object(); private final WeatherServiceNonThreadSafe serviceNTS = new WeatherServiceNonThreadSafe(); @Override public int query(String city) { synchronized (lock){ return serviceNTS.query(city); } } }
public class WeatherServices { private static final WeatherServiceProxy INSTANCE = new WeatherServiceProxy(); public static WeatherService uniqueService() { return INSTANCE; } }
On ne peut intercepter que des appels à des méthodes publiques : vive l'encapsulation.
Les appels à des méthodes publiques fait par des méthodes publiques de la classe ne sont pas interceptables.
Cf. Exo 1