Strategy pattern : le patron de conception logicielle pour les stratégies comportementales variables

Dans la programmation orientée objet, les Design patterns (patrons de conception) aident les développeurs grâce à des approches et des modèles de solutions éprouvés. Après avoir choisi un patron approprié, il suffit alors de procéder à des ajustements individuels. À l’heure actuelle, il existe un total de 70 patrons de conception adaptés à des domaines d’application spécifiques. Les Strategy design patterns se concentrent sur le comportement des logiciels.

Qu’est-ce que le Strategy pattern ?

Le Strategy pattern fait partie des Behavioral patterns (patrons comportementaux) qui équipent un logiciel avec différentes méthodes de résolution. Ces stratégies sont en réalité une famille d’algorithmes séparés du programme à proprement parler et autonomes (= interchangeables). Un patron de conception Stratégie inclut également des lignes directrices et des outils pour les développeurs. Les Strategy patterns décrivent ainsi comment structurer des classes, organiser un groupe de classes et créer des objets. L’une des particularités du Strategy pattern réside dans le fait qu’il est possible de réaliser un comportement de programme et d’objet variable même pendant l’exécution d’un logiciel.

À quoi ressemble la représentation UML d’un Strategy pattern ?

La conception des Strategy patterns passe normalement par le langage de modélisation graphique UML (Unified Modelling Language). Ce dernier permet de visualiser le modèle de conception avec une notation standardisée en utilisant des caractères et des symboles spéciaux. L’UML met à disposition différents types de diagrammes pour la programmation orientée objet. Pour la représentation d’un patron de conception Stratégie, on choisit en général un diagramme de classes avec au moins trois composants de base :

  • Context (contexte ou classe de contexte)
  • Strategy (stratégie ou classe de stratégie)
  • ConcreteStrategy (stratégie concrète)

Dans le Strategy design pattern, les composants de base assument des fonctionnalités spécifiques : les modèles de comportement de la classe Context sont externalisés dans différentes classes Strategy. Ces classes séparées abritent les algorithmes appelés ConcreteStrategies. Si nécessaire, le Context peut recourir aux variantes de calcul externalisées (ConcreteStrategyA, ConcreteStrategyB, etc.) en utilisant une référence (interne). Dans ce cadre, il n’interagit pas directement avec les algorithmes mais avec une interface.

L’interface Strategy encapsule les variantes de calcul et peut être implémentée simultanément par tous les algorithmes. Pour interagir avec le Context, l’interface générique met une seule méthode à disposition pour la résolution des algorithmes ConcreteStrategy. Outre l’appel de la stratégie, les interactions avec le Context incluent également l’échange de données. L’interface Strategy contribue également aux changements de stratégie qui ont lieu pendant l’exécution d’un programme.

Remarque

L’encapsulage permet d’empêcher l’accès direct aux algorithmes et aux structures de données internes. Les instances externes (client, Context) peuvent uniquement utiliser les calculs et les fonctionnalités via des interfaces définies. Dans ce cadre, seuls les méthodes et les éléments de données d’un objet pertinents pour l’instance externe sont accessibles.

Nous vous expliquons comment ce patron de conception est mis en œuvre dans un projet pratique à travers un exemple de Strategy pattern.

Le Strategy pattern expliqué à travers un exemple

Dans notre exemple, nous nous appuyons sur le projet d’étude de l’ingénieur allemand Philipp Hauer : une application de navigation doit être réalisée à l’aide d’un patron de conception Strategy. L’application doit exécuter un calcul d’itinéraire réalisé à partir de moyens de transport courants. L’utilisateur peut choisir parmi trois options :

  • piéton (ConcreteStrategyA)
  • voiture (ConcreteStrategyB)
  • transports en commun (ConcreteStrategyC)

Transposer ces objectifs dans un graphique UML permet de préciser la structure et le fonctionnement du Strategy pattern nécessaire :

Dans notre exemple, le client est l’interface d’utilisation (Graphical User Inferface, IGU) d’une application de navigation avec des boutons pour le calcul des itinéraires. Lorsque l’utilisateur fait un choix et appuie sur un bouton, un itinéraire concret est calculé. Le Context (classe navigateur) a pour tâche de calculer et d’afficher une série de points de contrôle sur la carte. La classe navigateur dispose d’une méthode pour changer la stratégie de calcul d’itinéraire active. Grâce aux boutons du client, il est alors possible de passer d’un moyen de transport à l’autre sans difficulté.

Par exemple, si l’on déclenche une commande correspondante avec le bouton « piéton » du client, le service « Calcule l’itinéraire pour un piéton » (ConcreteStrategyA) est appelé. La méthode executeAlgorithm() (dans notre exemple, la méthode : calculItineraire (A, B)) accepte une origine et une destination et renvoie un ensemble de points de contrôle de l’itinéraire. Le Context reçoit la commande du client et décide de la stratégie adaptée (setStrategy : piéton) sur la base de directives prédéfinies (Policy). À l’aide de Call, il délègue la requête à l’objet Strategy et à son interface.

getStrategy() indique la stratégie actuellement sélectionnée dans le Context (classe navigateur). Les résultats des calculs ConcreteStrategy sont utilisés dans le traitement ultérieur ainsi que dans l’affichage graphique de l’itinéraire dans l’application de navigation. Si l’utilisateur opte pour un autre itinéraire, par exemple en cliquant sur le bouton « Voiture », le Context passe à la stratégie demandée (ConcreteStrategyB) et déclenche un nouveau calcul via un autre Call. À la fin de la procédure, un itinéraire modifié est indiqué pour le moyen de transport « voiture ».

Dans notre exemple, le mécanisme du patron peut être mis en œuvre avec un code relativement clair :

Context :

public class Context {
    //Valeur standard prédéfinie (comportement par défaut) : ConcreteStrategyA
    private Strategy strategy = new ConcreteStrategyA(); 
    public void execute() { 
        //délègue le comportement à un objet Strategy
        strategy.executeAlgorithm(); 
    }
    public void setStrategy(Strategy strategy) {
        strategy = strategy;
    }
    public Strategy getStrategy() { 
        return strategy; 
    } 
} 

Strategy, ConcreteStrategyA, ConcreteStrategyB :

interface Strategy { 
    public void executeAlgorithm(); 
} 
class ConcreteStrategyA implements Strategy { 
    public void executeAlgorithm() { 
        System.out.println("Concrete Strategy A"); 
    } 
} 
class ConcreteStrategyB implements Strategy { 
    public void executeAlgorithm() { 
        System.out.println("Concrete Strategy B"); 
    } 
}  

Client :

public class Client {

public static void main(String[] args) {

//Comportement par défaut

Context context = new Context();

context.execute();

//Modifier le comportement

context.setStrategy(new ConcreteStrategyB());

context.execute();

}

}

Quels sont les avantages et les inconvénients du Strategy pattern ?

Les avantages d’un Strategy pattern sont visibles lorsque l’on prend la perspective d’un programmeur ou d’un administrateur système. En général, la décomposition en modules et en classes autonomes entraîne une meilleure structuration du code du programme. Dans les sous-parties segmentées, le programmeur de l’application de notre exemple aura affaire à des segments de code plus fins. Il est ainsi possible de diminuer l’ampleur de la classe Navigateur par l’externalisation des stratégies et de renoncer à former des sous-classes dans le domaine du Context.

Dans ce code segmenté de façon plus fine et plus propre, les répercussions des modifications sont limitées puisque les dépendances internes des segments restent cadrées. Par conséquent, les reprogrammations ultérieures – qui peuvent être laborieuses – sont plus rarement nécessaires et peuvent, pour partie, être entièrement exclues. Sur le long terme, la transparence des segments de code permet un meilleur entretien et facilite les diagnostics de problèmes ainsi que la recherche d’erreurs.

L’utilisation en bénéficie également puisque l’application de notre exemple peut être dotée d’une interface conviviale. Grâce à ses boutons, les utilisateurs peuvent commander le comportement du programme (ici, le calcul d’itinéraires) de façon simple et variable et choisir parmi plusieurs options en toute simplicité.

Comme le Context de l’application de navigation interagit uniquement avec une interface (du fait de l’encapsulage des algorithmes), il ne dépend pas de l’implémentation concrète des différents algorithmes. Si par la suite, les algorithmes sont modifiés ou de nouvelles stratégies sont introduites, le code du Context n’aura pas à être modifié. À titre d’exemple, on pourrait facilement et rapidement compléter le calcul des itinéraires avec des ConcreteStrategies supplémentaires pour des trajets en avion, en bateau ou en train. Les nouvelles stratégies devront simplement implémenter l’interface Strategy correctement.

Une autre caractéristique avantageuse des patrons Strategy permet de faciliter la programmation complexe des logiciels orientés objet. Ces patrons permettent en effet de concevoir des (modules) logiciels réutilisables dont le développement est considéré comme particulièrement exigeant. Par exemple, les classes de Context apparentées pourraient également utiliser les stratégies externalisées pour le calcul des itinéraires via l’interface et n’auraient plus à les implémenter personnellement.

Malgré ses nombreux avantages, le patron Strategy comporte également quelques inconvénients. Du fait de sa structure plus complexe, la conception de logiciel peut générer des redondances et des inefficacités dans la communication interne. Dans certains cas, l’interface Strategy générique – responsable de l’implémentation simultanée de tous les algorithmes – peut ainsi être surdimensionnée.

Un exemple : après avoir créé et initialisé certains paramètres, le Context les transmet à l’interface générique et aux méthodes qui y sont définies. Cependant, la dernière stratégie implémentée n’a pas nécessairement besoin de tous les paramètres Context communiqués et ne les traite pas. Dans le patron Strategy, l’interface mise à disposition n’est donc pas toujours utilisée de façon optimale et il n’est pas toujours possible d’éviter un effort de communication accru avec des transferts de données superflus.

Lors de l’implémentation, il existe par ailleurs une dépendance interne étroite entre le client et les stratégies. Comme le client fait un choix et appelle la stratégie concrète à l’aide d’une commande de déclenchement (dans notre exemple, le calcul des itinéraires pour les piétons), il doit connaître les ConcreteStrategies. Par conséquent, ce patron de conception ne devrait être utilisé que si les changements de stratégie et de comportement sont essentiels ou fondamentaux pour l’utilisation et le fonctionnement d’un logiciel.

Il est possible de contourner ou de compenser en partie les inconvénients présentés. Dans un Strategy pattern, le nombre d’instances d’objet peut être considérable, mais il peut souvent être réduit par une implémentation dans un Flyweight pattern. Une telle mesure aura également des effets bénéfiques sur l’efficacité et l’utilisation de la mémoire par une application.

Dans quel cas le Strategy pattern est-il utilisé ?

En tant que patron de conception fondamental, le Strategy pattern ne se limite pas à un domaine d’utilisation particulier dans le développement logiciel. L’utilisation de ce patron de conception sera davantage déterminée par la nature de la problématique. Ce modèle de conception est idéal pour tous les logiciels devant résoudre des tâches et des problèmes avec flexibilité et en proposant des options et des modifications de comportement.

Le patron de conception Strategy est par exemple utilisé par les programmes offrant différents formats de stockage pour les fichiers ou diverses fonctionnalités de tri et de recherche. Dans le domaine de la compression de données, on utilise également des programmes implémentant différents algorithmes de compression sur la base de ce modèle de conception. De cette manière, vous pouvez par exemple convertir différentes vidéos dans le format de fichier peu volumineux de votre choix ou reconvertir des fichiers compressés (par exemple des fichiers ZIP ou RAR) dans leur format d’origine avec des stratégies de décompression spécifiques. L’enregistrement d’un document ou d’une image dans différents formats de fichiers constitue un autre exemple.

Ce patron de conception participe également au développement et à l’implémentation des logiciels de jeu qui doivent par exemple réagir de façon flexible à des situations de jeu changeantes pendant l’exécution. Il est ainsi possible d’enregistrer différents personnages, des équipements spécifiques, un modèle de comportement ou différents mouvements d’un personnage sous la forme de ConcreteStrategies.

Les logiciels de commande constituent un autre domaine d’application du Strategy pattern. En échangeant des ConcreteStrategies, il est possible d’adapter sans difficulté des ensembles de calculs à des catégories professionnelles, des pays et des régions. Par ailleurs, les programmes transposant des données dans différents graphiques (par ex. sous forme de diagrammes linéaires, circulaires ou à barres) utilisent aussi des patrons Strategy.

On trouve des applications plus spécifiques des Strategy patterns dans la bibliothèque standard Java (Java API) et dans les Java GUI-Toolkits (par ex. AWT, Swing et SWT) qui utilisent un gestionnaire de mise en page dans le développement et la génération des interfaces utilisateurs. Ce gestionnaire peut implémenter différentes stratégies pour l’organisation des composants lors du développement de l’interface. D’autres applications des patrons de conception Strategy peuvent être trouvées dans les systèmes de bases de données, les pilotes de périphériques et les programmes de serveurs.

Aperçu des principales caractéristiques du Strategy pattern

Au sein de la vaste palette de patrons de conception, le patron de conception Stratégie se démarque par les caractéristiques suivantes :

  • orienté comportement (les modes et les modifications de comportement sont plus facilement programmables et implémentables ; les modifications sont également possibles pendant l’exécution d’un programme)
  • orienté efficacité (les externalisations simplifient et optimisent le code et son entretien)
  • orienté vers l’avenir (les modifications et les optimisations peuvent facilement être réalisées à moyen et à long terme)
  • vise la modularité (favorisée par le système modulaire et l’indépendance des objets et des classes)
  • vise la réutilisabilité (par ex. l’utilisation multiple des stratégies)
  • vise une convivialité, une contrôlabilité et une configurabilité des logiciels optimisées
  • nécessite des connaissances préalables en conception (que peut-on externaliser, comment et à quel endroit des classes de stratégie ?)
En résumé

Dans la programmation orientée objet, les Strategy patterns permettent un développement logiciel efficace et économique grâce à des solutions sur mesure. Les potentielles modifications et améliorations futures sont préparées de façon optimale dès la phase de conception. De façon générale, ce système axé sur la flexibilité et le dynamisme peut être mieux commandé et contrôlé. Les erreurs et les incohérences peuvent être plus rapidement corrigées. Des composants réutilisables et remplaçables permettent d’économiser des coûts de développement, en particulier dans les projets complexes avec une perspective à long terme. Il convient toutefois de trouver le bon équilibre. Il n’est pas rare que les modèles de conception soient utilisés de façon trop parcimonieuse ou trop fréquemment.