Le Strategy Pattern


Dans la suite de cette série d’articles sur les design patterns, nous allons aborder le Strategy pattern (ou stratégie en bon français). Ce pattern appartient à la famille des patrons de conception comportementaux puisqu’il permet de sélectionner le comportement d’un objet (ou une stratégie, d’où le nom de ce pattern) durant l’exécution (runtime).


En effet, lorsqu’on débute, on a souvent l’habitude d’utiliser l’héritage et le polymorphisme pour implémenter et surcharger des comportements. Prenons, par exemple, le cas d’une classe Animal qui implémente une méthode avancer() :

abstract class Animal {
	 private String name;
	 abstract void avancer();
}

class Oiseau extends Animal {
	public Oiseau(String name){
	this.name = name;
}

	@Override
	public void avancer(){
		System.out.println("Moi je vole...");
	}
}

class Main {
	public static void main(){
		Animal oiseau = new Oiseau("Oiseau");
		oiseau.avancer();
		//Output: "Moi je vole..."
	}
}

Ici, la classe Oiseau hérite de la classe abstraite Animal pour surcharger le comportement avancer() et permettre aux objets de la classe Oiseau de « voler ».

Quel est le problème avec le code ci-dessus ? Imaginons qu’on nous demande de pouvoir faire se déplacer tous types d’animaux. Avec cette approche, nous nous retrouverions très vite avec un nombre incalculable de sous-classes héritant de la classe Animal pour chaque animal sur Terre, ce qui serait ingérable à long terme pour diverses raisons évidentes. En effet si l’héritage favorise la réutilisation du code, il peut conduir à une structure rigide dificile à maintenir car il induit à un couplage fort entre les classes.

Mais il y a aussi une autre contrainte importante dans ce code qui ne saute pas tout de suite aux yeux : le comportement d’un oiseau est figé à la compilation. Il est impossible pour les objets de cette classe d’avancer autrement qu’en volant. Une fois le programme lancé, il ne sera plus jamais possible de faire marcher notre oiseau.

Cet exemple illustre bien le principe de la « Composition over inheritance » en POO, qui consiste à favoriser la composition d’un objet par un autre afin d’implémenter un nouveau comportement, plutôt que d’utiliser l’héritage. Cela permet de changer de comportement de manière dynamique pendant l’exécution de notre programme et d’ajouter des comportements sans modifier la classe de base. Ainsi, notre oiseau pourra marcher ! En faisant cela, on respecte l’un des principes SOLID : le principe Ouvert/Fermé qui stipule que le code doit être ouvert à l’extension et fermé à la modification.

Voici l’implémentation de notre classe Oiseau en utilisant ce pattern :

interface Deplacement {
	void avancer();
}

class Marche implements Deplacement {
	@Override
	public void avancer(){
	  System.out.println("Moi je marche...");
  }
}

class Vol implements Deplacement {
	@Override
	public void avancer(){
		System.out.println("Moi je vole...");
	}
}

class Animal {
	 private String name;
	 private Deplacement deplacement;

	public Animal(String name, Deplacement deplacement){
		this.name = name;
		this.deplacement = deplacement;
	}

	public void setDeplacement(Deplacement deplacement){
		this.deplacement = deplacement;
	}

	public void avancer(){
		this.deplacement.avancer();
	}
}

class Main {
	public static void main(){
		Animal oiseau = new Animal("Oiseau", new Vol());
		oiseau.avancer();
		//Output: "Moi je vole..."
		oiseau.setDeplacement(new Marche());
		oiseau.avancer();
		//Output: "Moi je marche..."
	}
}

L’interface Deplacement et ses implémentations regroupent nos stratégies de déplacement. C’est une famille d’algorithmes interchangeables dynamiquement grâce au polymorphisme. Notre classe Animal est désormais composée de cette famille d’algorithmes, et nous avons implémenté un setter pour remplacer cette propriété par n’importe quelle implémentation de l’interface Deplacement. Nous pouvons maintenant nous passer de la sous-classe Oiseau, et notre instance d’Animal peut changer de stratégie de déplacement pendant l’exécution du programme. Pour ajouter une nouvelle façon de se déplacer, il suffit de créer une nouvelle classe implémentant notre interface Deplacement.

Cependant, depuis Java 8, il est possible d’implémenter ce pattern de façon plus fonctionnelle en utilisant les Functional Interfaces, nous délestant ainsi de la nécessité de créer une classe et ses implémentations pour définir nos stratégies.

@FunctionalInterface
interface Deplacement {
	void avancer();
}

class Animal {

	private String name;
	public Animal(String name){
		this.name = name;
	}

	public void avancer(Deplacement deplacement){
		deplacement.avancer();
	}
}

class Main {
	public static void main(){
		Animal oiseau = new Animal("Oiseau");
		oiseau.avancer(() -> System.out.println("Moi je vole..."));
		//Output: "Moi je vole..."
		oiseau.avancer(() -> System.out.println("Moi je marche..."));
		//Output: "Moi je marche..."
	}
}

Ici, on définit une Functional Interface représentant une fonction sans paramètre et sans valeur de retour. Cette fonction est passée en paramètre de la méthode avancer() de la classe Animal pour être appelée à l’intérieur de celle-ci. La Functional Interface nous permet ainsi de définir le comportement de la fonction avancer() à la volée lors de son appel, grâce à une expression lambda. Cette approche avec les lambdas s’avère particulièrement utile pour des comportements ponctuels ou des stratégies peu complexes. Cependant, si les stratégies se complexifient, il est préférable de revenir à l’approche avec des classes concrètes, offrant une meilleure maintenabilité et évitant la répétition de code. L’avantage principal de cette implémentation du pattern réside dans la possibilité de définir la logique au plus près de son utilisation.

Si l’on reprend l’exemple donné dans mon précédent article sur le proxy pattern : https://anisikram.fr/le-proxy-pattern/, vous remarquerez que nous utilisions déjà le Strategy pattern. En effet, l’interface HeavyDataFetcher représente une famille de stratégies à implémenter (par HeavyDataSqlFetcher, par exemple), sauf qu’ici, la classe cliente n’est pas Animal, mais bien la classe Application et sa méthode main().


Pour conclure, le Strategy pattern offre une solution souple pour gérer les variations de comportement en favorisant la composition sur l’héritage. Il permet de modifier les comportements dynamiquement, tout en respectant le principe Ouvert/Fermé des règles SOLID, ce qui améliore la maintenabilité du code. Avec l’arrivée des functional interfaces en Java 8, l’implémentation de ce pattern devient encore plus simple grâce aux expressions lambda, bien qu’il faille veiller à éviter la duplication de code. En résumé, le Strategy pattern est une approche efficace pour concevoir des objets capables d’adopter plusieurs comportements de manière flexible et évolutive.

Le Proxy Pattern

Dans cet article, je vais tâcher d’expliquer en quoi consiste le patron de conception appelé design pattern proxy. Un proxy, comme on le nomme communément entre développeurs, est un patron de conception qui entre dans la catégorie des design patterns structurels. Cette catégorie englobe les patterns qui utilisent des interfaces, l’héritage et le polymorphisme afin de définir des ensembles plus larges.


Un proxy permet au client un accès distant à une ressource grâce à une interface commune. Il se substitue à cette ressource afin d’effectuer des actions avant ou après son accès.

En effet, il permet de contrôler l’accès à un objet, de faire une instantiation différée (lazy loading), de logger des informations, de rafraîchir un cache, etc.

Dans la pratique, il est très utilisé dans de nombreux frameworks, et il arrive que l’on en manipule sans même s’en rendre compte. Prenons quelques exemples dans l’écosystème Java, car c’est celui que je connais le mieux :

  • L’utilisation de l’annotation @Transactional sur une méthode déclenche la création d’un proxy sur la classe porteuse de cette annotation afin de créer, si nécessaire (les transactions feront également l’objet d’un futur article), une transaction au début de vos méthodes, et de la committer à la fin de celles-ci.
  • Lorsque l’on manipule des entités qui portent des associations avec un FetchType défini à LAZY, Hibernate utilise un proxy à la place de l’association afin de provoquer son chargement depuis la base de données lorsque la méthode est appelée sur cette dernière (attention aux n+1 queries 😉).

Exemple d’implémentation

Maintenant que nous avons vu en quoi consiste le pattern proxy, voici un exemple d’implémentation.

Nous avons ici une classe qui effectue une tâche coûteuse afin de récupérer des données qui demeurent identiques sur une même journée. Nous aimerions ajouter quelques fonctionnalités afin de logger la date à laquelle on la récupère à chaque fois et de mettre les données en cache afin d’optimiser les performances :

public interface HeavyDataFetcher {
    HeavyData fetchData();
}

public class HeavySqlDataFetcher implements HeavyDataFetcher {

    @Override
    public HeavyData fetchData() {
        // Du code pour récupérer les données en base
    }
}

public class Application {
    public static void main(String[] args) {
        HeavyDataFetcher dataFetcher = new HeavySqlDataFetcher();
        HeavyData data = dataFetcher.fetchData();
        System.out.println(data);
    }
}

Cette classe HeavySqlDataFetcher implémente une interface HeavyDataFetcher, ce qui est une bonne pratique, car cela laisse la possibilité de définir une autre classe pour récupérer les données d’une manière différente (par exemple, à partir d’une autre source que la base de données SQL). Afin de créer notre proxy, nous allons implémenter cette interface pour bénéficier du polymorphisme, et pouvoir ainsi substituer la classe HeavySqlDataFetcher par notre proxy. Un proxy doit avoir, comme propriété, l’objet qu’il substitue afin de pouvoir y accéder.

public class HeavySqlDataFetcherProxy implements HeavyDataFetcher {

    private HeavySqlDataFetcher sqlDataFetcher;

    public HeavySqlDataFetcherProxy(HeavySqlDataFetcher sqlDataFetcher) {
        this.sqlDataFetcher = sqlDataFetcher;
    }

    @Override
    public HeavyData fetchData() {
        return this.sqlDataFetcher.fetchData();
    }
}

Actuellement, notre classe proxy ne fait que passer la requête sans induire de changement dans le comportement de l’objet substitué. Nous pouvons maintenant mettre en place un cache pour stocker les données récupérées dans une variable d’instance, ainsi que logger chaque mise à jour du cache.

public class HeavySqlDataFetcherProxy implements HeavyDataFetcher {

    private final HeavySqlDataFetcher sqlDataFetcher;
    private final Logger logger;
    private HeavyData cachedData;
    private Instant cacheDate;

    public HeavySqlDataFetcherProxy(HeavySqlDataFetcher sqlDataFetcher) {
        this.sqlDataFetcher = sqlDataFetcher;
        this.logger = Logger.getLogger(HeavySqlDataFetcherProxy.class.getName());
    }

    @Override
    public HeavyData fetchData() {
        if (cachedData == null || Instant.now().minus(1, ChronoUnit.DAYS).isAfter(cacheDate)) {
            this.cacheDate = Instant.now();
            logger.info("Data accessed at: " + this.cacheDate);
            this.cachedData = this.sqlDataFetcher.fetchData();
        }
        return cachedData;
    }
}

Comme on peut le voir, la méthode fetchData() a été enrichie. Si le cache est null ou que sa date de mise à jour dépasse un jour, les données sont rechargées depuis la base pour mettre à jour le cache et la date de mise à jour. Les nouvelles données sont ensuite retournées. Si les données sont toujours valides, elles sont simplement retournées à partir du cache sans être rechargées. Nous loggons également la date de mise à jour à chaque fois que les données sont actualisées.

Utilisation du proxy

Maintenant que notre proxy est implémenté, il peut être utilisé en remplacement de l’objet substitué en modifiant très légèrement le code client :

public class Application {
    public static void main(String[] args) {
        HeavyDataFetcher dataFetcher = new HeavySqlDataFetcherProxy(new  HeavySqlDataFetcher());
        HeavyData data = dataFetcher.fetchData();
        System.out.println(data);
    }
}

En effet, il suffit de modifier l’instantiation d’un objet de type HeavyDataFetcher pour utiliser notre proxy, et le tour est joué. Nous avons modifié le comportement pour accéder à nos données de manière (presque) transparente pour le client ! Il est ici encore possible d’améliorer notre proxy en changeant le type de la propriété sqlDataFetcher par l’interface HeavyDataFetcher ainsi notre proxy pourrait se substituer à n’importe quelle implémentation !

Voici la version finale de la classe proxy :

public class HeavyDataFetcherProxy implements HeavyDataFetcher {

    private final HeavyDataFetcher dataFetcher;
    private final Logger logger;
    private HeavyData cachedData;
    private Instant cacheDate;

    public HeavyDataFetcherProxy(HeavyDataFetcher dataFetcher) {
        this.dataFetcher = dataFetcher;
        this.logger = Logger.getLogger(HeavySqlDataFetcherProxy.class.getName());
    }

    @Override
    public HeavyData fetchData() {
        if (cachedData == null || Instant.now().minus(1, ChronoUnit.DAYS).isAfter(cacheDate)) {
            this.cacheDate = Instant.now();
            logger.info("Data accessed at: " + this.cacheDate);
            this.cachedData = this.dataFetcher.fetchData();
        }
        return cachedData;
    }
}

Conclusion

Le design pattern proxy permet de contrôler l’accès à un objet et d’ajouter des fonctionnalités annexes sans changer le code de la classe d’origine. Grâce à l’utilisation d’une interface commune, il offre une grande flexibilité, permettant d’optimiser les performances (par exemple avec le cache), de journaliser les accès ou de gérer des transactions. Comme montré dans cet exemple, l’utilisation du proxy se fait de manière transparente pour le code client, ce qui facilite son intégration dans des applications existantes.

Dans l’écosystème Java, le proxy est omniprésent et utilisé implicitement dans des frameworks comme Spring ou Hibernate, offrant ainsi des fonctionnalités avancées sans intervention explicite du développeur. Sa capacité à séparer les préoccupations techniques (gestion de transactions, journalisation, cache) des préoccupations métiers en fait un outil puissant pour structurer et optimiser des applications complexes.

Les Design Pattern

Vous en avez surement déjà entendu parler au travail, dans des articles ou dans un livre mais les deign pattern sont prépondérants dans le monde du génie logiciel. Cet article vise à vous présenter ce qu’ils sont & à quoi ils servent, il sera compléter par une suite d’articles qui expliqueront à leur tour quelques design pattern. Alors les design pattern qu’est-ce que c’est ?

Les design patterns (ou « patrons de conception » en français) sont des solutions éprouvées pour résoudre des problèmes courants rencontrés dans le développement orienté objet. Ces solutions ne sont pas des morceaux de code spécifiques, mais plutôt des concepts réutilisables qui peuvent être appliqués à de nombreux contextes de programmation.

Il est intéressant de noter qu’en informatique, les défis que nous rencontrons ne sont presque jamais nouveaux. Chaque problème que vous recontrez a probablement déjà été résolu quelque part dans le monde. Cela a conduit à la nécessité de formaliser ces solutions pour les rendre accessibles et réutilisables par d’autres développeurs. Alors pourquoi réinventer la roue quand on peut s’appuyer sur des solutions éprouvées ?

On peut comparer les design patterns à des plans architecturaux : ils ne vous donnent pas les détails de chaque brique à poser, mais ils fournissent une vue d’ensemble de la manière de construire une solution solide et efficace.

Quand et par qui ?

Le concept de design pattern a été introduit par Christopher Alexander, un architecte américain, dans son livre A Pattern Language, publié en 1977. Bien que son travail se concentre sur l’architecture et l’urbanisme, l’idée de capturer des solutions récurrentes à des problèmes communs dans un format structuré a rapidement trouvé un écho dans le domaine du génie logiciel. Alexander a souligné que ces patterns pouvaient être appliqués à tout processus de conception, qu’il soit architectural, urbain ou informatique.

Quelques années plus tard, au début des années 1990, les design patterns ont été formalisés dans le domaine de l’informatique par un groupe de quatre auteurs souvent désignés comme le Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, et John Vlissides). Leur livre, Design Patterns: Elements of Reusable Object-Oriented Software, publié en 1994, a joué un rôle crucial en rendant ces concepts largement connus et utilisés par les développeurs du monde entier. Ce livre est encore aujourd’hui considéré comme une lecture essentielle pour tout développeur, malgré les évolutions constantes des langages de programmation et des méthodologies de développement.

La référence des livres sur les design pattern

À quoi ça sert ?

Les design patterns servent à résoudre des problèmes spécifiques de manière efficace et structurée. Ils répondent à des situations qui se présentent fréquemment dans le développement de logiciels, comme la création d’objets complexes, la gestion d’états, la communication entre objets, et bien d’autres. Leur utilité réside dans leur capacité à fournir des solutions qui sont à la fois compréhensibles et modifiables, tout en encourageant les bonnes pratiques de programmation.

En plus d’offrir des solutions techniques, les design patterns jouent un rôle crucial dans la communication entre développeurs. En adoptant un vocabulaire commun, les équipes de développement peuvent plus facilement partager des idées, discuter de solutions et collaborer plus efficacement. Imaginez pouvoir dire « Utilisons le pattern Singleton ici » et que toute l’équipe comprenne immédiatement le concept sans avoir besoin de longs détails explicatifs.

Il est donc essentiel de maîtriser les design patterns, car ils constituent des outils incontournables pour tout développeur souhaitant écrire du code robuste, maintenable et extensible. C’est pourquoi je vais aborder ces patterns dans une série d’articles sur ce blog. Chaque article explorera un pattern différent, ses avantages, ses inconvénients, et des exemples d’utilisation.

Comment les utiliser ?

Il n’est pas nécessaire de connaître par cœur tous les design patterns, mais il est crucial de savoir qu’ils existent et de pouvoir identifier les situations où leur application serait bénéfique. Une bonne pratique consiste à étudier les problèmes que chaque pattern vise à résoudre et de réfléchir à la manière dont ces problèmes peuvent se manifester dans vos propres projets.

Dans la pratique, l’utilisation des design patterns nécessite de l’expérience et de la prudence. Bien qu’ils soient des outils puissants, il est facile de tomber dans le piège de les utiliser de manière excessive ou inappropriée, ce qui peut rendre le code plus complexe que nécessaire. Comme pour tout outil de développement, la clé est de trouver un équilibre et d’appliquer le bon pattern au bon moment.

Il faut également savoir que les deisgn pattern sont classifiés en 3 catégories :

  • Les design pattern de création qui permettent d’abstraire & de rendre plus flexible la création d’objet.
  • Les design pattern structurelles permettent de définir comment des objets & classes peuvent former de plus grand structures en utilisant les interfaces, leurs implémentations & l’héritage.
  • Les design pattern comportementales décrivent des façons d’encapsuler des algorithmes, de les changer dynamiquement & de définir la responsabilités de chaque objet

En conclusion, les design patterns sont bien plus que des recettes toutes faites ; ce sont des guides pour structurer et penser le développement logiciel de manière efficace et intelligente. Ils font partie intégrante du parcours de tout développeur, et maîtriser leur utilisation est essentiel pour créer des logiciels de qualité.

Pour en apprendre plus sur chaque patron, vous pouvez attendre le suite des articles sur ce blog ou lire le livre du gang of four bien qu’il soit maintenant un peu vieux & obsolète. C’est pourquoi j’ai une préférence pour le livre Head First Design Pattern avec de nouvelles éditions mise à jour régulièrement & une approche très ludique.