Introduction au Patron Stratégie
Le patron de conception Stratégie (Strategy Pattern) est un modèle comportemental qui permet de définir une famille d'algorithmes, de les encapsuler dans des classes distinctes et de les rendre interchangeables. Ce patron permet à l'algorithme de varier indépendamment des clients qui l'utilisent, favorisant ainsi une architecture flexible et maintenable.
Identification du Besoin : Encapsuler les Comportements Variables
Imaginons un système de génération de rapports qui doit exporter des données brutes sous différents formats. Selon les exigences métier, le système doit pouvoir produire un fichier CSV, JSON ou XML. Une apprcohe naïve consisterait à utiliser de multiples instructions conditionnelles (if-else ou switch) dans une seule classe. Cependant, cela viole le principe Ouvert/Fermé et rend le code difficile à maintenir et à tester.
La solution consiste à extraire la logique de formatage dans une interface commune :
public interface DataFormatter {
String formatData(Map<String, Object> rawData);
}
Ensuite, nous créons des implémentations concrètes pour chaque format :
public class JsonFormatter implements DataFormatter {
@Override
public String formatData(Map<String, Object> rawData) {
// Logique de sérialisation JSON
return "{ \"format\": \"json\", \"records\": " + rawData.size() + " }";
}
}
public class CsvFormatter implements DataFormatter {
@Override
public String formatData(Map<String, Object> rawData) {
// Logique de conversion CSV
return "format,records\ncsv," + rawData.size();
}
}
Le Rôle du Contexte (Context)
Bien que l'interface et les implémentations concrètes constituent le cœur du patron, le client ne devrait pas interagir directement avec les stratégies concrètes. C'est ici qu'intervient la classe Contexte. Le contexte maintient une référence vers une stratégie et délègue l'exécution de l'algorithme à cette dernière. Il agit comme un isolateur, masquant la complexité des stratégies au module de haut niveau et centralisant les opérations communes.
Prenons l'exemple d'un système de notification multi-canal. Le contexte gère l'envoi sans se soucier du protocole sous-jacent.
public interface NotificationChannel {
void dispatch(String target, String payload);
}
public class EmailChannel implements NotificationChannel {
@Override
public void dispatch(String target, String payload) {
System.out.println("Expédition de l'e-mail à " + target + " avec le contenu : " + payload);
}
}
public class SmsChannel implements NotificationChannel {
@Override
public void dispatch(String target, String payload) {
System.out.println("Envoi du SMS au " + target + " : " + payload);
}
}
La classe Contexte encapsule l'appel et gère l'état :
public class NotificationManager {
private NotificationChannel channel;
public NotificationManager(NotificationChannel channel) {
this.channel = channel;
}
public void updateChannel(NotificationChannel newChannel) {
this.channel = newChannel;
}
public void notifyUser(String userContact, String message) {
// Délégation pure à la stratégie configurée
this.channel.dispatch(userContact, message);
}
}
Utilisation par le client :
public class Application {
public static void main(String[] args) {
NotificationManager manager = new NotificationManager(new EmailChannel());
manager.notifyUser("admin@domain.com", "Rapport quotidien généré.");
// Changement de stratégie à la volée
manager.updateChannel(new SmsChannel());
manager.notifyUser("+33612345678", "Alerte critique du système.");
}
}
Avantages et Inconvénients
Avantages :
- Respect du principe Ouvert/Fermé : L'ajout de nouvellse stratégies ne nécessite aucune modification du code existant.
- Élimination des structures conditionnelles cmoplexes : Remplace les longs blocs
if-elsepar une délégation orientée objet propre. - Interchangeabilité à l'exécution : Les algorithmes peuvent être modifiés dynamiquement pendant l'exécution du programme.
Inconvénients :
- Prolifération des classes : Chaque stratégie nécessite sa propre classe, ce qui peut augmenter la charge cognitive et le nombre de fichiers dans le projet.
- Couplage du client aux stratégies : Le module appelant doit avoir connaissance de toutes les stratégies disponibles pour choisir la bonne. Ce problème est souvent résolu en combinant le patron Stratégie avec le patron Fabrique (Factory).
Application dans le JDK Java : ThreadPoolExecutor
Le JDK de Java utilise abondamment ce patron. Un exemple classique se trouve dans la gestion des threads avec ThreadPoolExecutor. Lorsque la file d'attente des tâches est pleine et que le nombre maximum de threads est atteint, le pool doit adopter une politique de rejet. Cette politique est définie par l'interface RejectedExecutionHandler.
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable worker, ThreadPoolExecutor executor);
}
Le JDK fournit plusieurs implémentations concrètes telles que AbortPolicy (lance une exception), CallerRunsPolicy (exécute la tâche dans le thread appelant), DiscardPolicy (ignore silencieusement la tâche) et DiscardOldestPolicy (supprime la tâche la plus ancienne).
Le ThreadPoolExecutor agit comme le Contexte :
public class ThreadPoolExecutor extends AbstractExecutorService {
private volatile RejectedExecutionHandler rejectionHandler;
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
// ... initialisation des paramètres
this.rejectionHandler = handler;
}
final void reject(Runnable task) {
// Délégation à la stratégie de rejet configurée
rejectionHandler.rejectedExecution(task, this);
}
}
Cette architecture permet aux développeurs de configurer précisément le comportement du pool de threads face à la saturation, sans avoir à modifier le code source de l'exécuteur lui-même, illustrant parfaitement la puissance de la délégation comportementale.