POO & Design Patterns

Les design patterns Proxy et Decorator

Les patron Proxy et Decorateur

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.

Des canards (1)

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.

Des canards (2)

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

Des canards (3)

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.

Générateur de nombres premiers (1)

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
    }

}

Générateur de nombres premiers (2)

On voudrait pouvoir rajouter plusieurs optimisation à nos générateurs:

  • précalculer les 10 premiers nombres premiers,
  • faire en sorte qu'une fois que le n-ième nombre premier a été calculé par un générateur, il le mémorise pour ne pas le recalculer lors d'un prochain appel.

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.

Générateur de nombres premiers (3)

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);
    }
}
  • Un CachedPrimeGenerator prend n'importe quel PrimeGenerator à la construction.
  • Un CachedPrimeGenerator est lui-même un PrimeGenerator.

Générateur de nombres premiers (4)

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

Générateur de nombres premiers (5)

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.

Le patron Decorator

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.

Diagramme UML

Diagramme UML

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!

Patron Proxy

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:

  • Le proxy contrôle l'accès à l'objet qu'il contient.
  • Le proxy connaît le type réel de l'objet qu'il encapsule.
  • Souvent le proxy est caché à l'utilisateur qui ne voit que l'interface.

Il faut voir le proxy comme enveloppe autour de l'objet invisible pour l'utilisateur.

Example

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

Limitiation des patterns

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