

Introduction
Beaucoup de développeurs ont un jour été confrontés à un gigantesque <span class="css-span">switch</span> ou une longue série de <span class="css-span">if/else</span> pour gérer des comportements différents dans une application. Je l'ai vécu et c'est embêtant. Ces blocs de code deviennent rapidement difficiles à tester et à faire évoluer.
Heureusement, le pattern Stratégie existe pour nous aider à écrire un code plus élégant, plus extensible et surtout plus SOLID.
Dans cet article, je vous montre comment utiliser le pattern Stratégie pour écrire un code plus clair et plus maintenable, tout en respectant les principes SOLID. Nous verrons ensemble son implémentation ainsi que quelques variantes pratiques.
Les extraits de code seront en C#/.NET, qui est le langage que j’utilise le plus, mais les concepts présentés s’appliquent facilement à d’autres langages de programmation.
Contexte d'utilisation
Pour mieux comprendre ce pattern nous allons l'utiliser dans une application de "loyalty program" qui consiste à appliquer une remise sur un panier en fonction du type de programme choisi (Point, Cashback).
Implémentons cette fonctionnalité en créant la classe <span class="css-span">ShoppingCartService</span>
<pre><code>public enum DiscountType
{
None,
Point,
Cashback
}
public class ShoppingCartService
{
public decimal CalculateTotalAmount(decimal amount, string discountType)
{
if (discountType == DiscountType.None)
{
return amount;
}
else if (discountType == DiscountType.Point)
{
int pointsToRedeem = GetPointsToRedeem();
decimal redeemValue = CalculatePointRedeemValue(pointsToRedeem);
return amount - redeemValue;
}
else if (discountType == DiscountType.Cashback)
{
decimal redeemValue = GetCashBackRedeemValue();
return amount - redeemValue;
}
else
{
throw new InvalidOperationException();
}
}
}</code></pre>
Ce code présente déjà quelques problèmes :
1. Multiples conditions (<span class="css-span">if/else</span>) :
Chaque fois que vous ajoutez un nouveau type de remise (ex : coupon, promo spéciale), vous devez modifier cette méthode, ce qui enfreint le principe O de SOLID (Ouvert/fermé) : le code devrait être ouvert à l’extension mais fermé à la modification.
2. Méthode monolithique :
Toute la logique de calcul est centrée dans une seule méthode, ce qui la rend plus difficile à tester, à maintenir et à réutiliser.
3. Violation de la responsabilité unique :
<span class="css-span">CalculateTotalAmount()</span> fait à la fois le choix du type de remise et le calcul lui-même. Idéalement, chaque classe/méthode devrait avoir une seule responsabilité.
La solution : le pattern Stratégie
Le pattern Stratégie consiste à encapsuler chaque comportement (ou algorithme) dans une classe distincte, qui implémente une interface commune. On peut alors choisir dynamiquement quelle stratégie utiliser à l'exécution.
En d'autres termes : nous remplaçons un gros code par des classes élégantes et interchangeables.
Quand l'utiliser ?
- Quand un comportement varie selon le contexte (et peut évoluer).
- Quand notre code a besoin d'être facilement testable et extensible.
- Quand nous voulons respecter le principe d'ouverture/fermeture (OCP).
Pour mieux comprendre, regardons l’image ci-dessous. Elle montre une tâche et trois solutions possibles : Solution 1, Solution 2 et Solution 3. Avec le pattern Strategy, on décide à l’exécution quelle solution utiliser. Autrement dit, au moment de réaliser la tâche, on choisira la solution adaptée.

Représentation UML

- Context : Le <span class="css-span">Context</span> garde une référence vers une des stratégies concrètes et communique avec cet objet uniquement au travers de l’interface <span class="css-span">Strategy</span>.
- Strategy : L’interface <span class="css-span">Strategy</span> est la même pour toutes les stratégies concrètes. Elle définit une méthode que le <span class="css-span">Context</span> utilise pour exécuter la stratégie choisie.
- ConcreteStrategyA et ConcreteStrategyB : les classes qui implémentent l’interface <span class="css-span">Strategy</span> et fournissent l’implémentation réelle des algorithmes.
Implémenter le pattern Stratégie pas à pas
Le pattern Stratégie définit un ensemble d’algorithmes, les encapsule et les rend interchangeables. Il permet ainsi à chaque algorithme d’évoluer indépendamment du code qui l’utilise.
Voyons comment l’implémenter en C# avec un exemple concret : l’application de "loyalty program" présentée plus haut.
Maintenant que nous connaissons la représentation UML du pattern stratégie, adaptons-la à notre exemple :

Nous allons créer l'interface <span class="css-span">IDiscountStrategy</span>, qui définit la méthode <span class="css-span">ApplyDiscount</span>, commune à toutes les stratégies, ainsi que la propriété <span class="css-span">DiscountType</span> pour identifier le type de réduction.
<pre><code>public interface IDiscountStrategy
{
string DiscountType { get; }
decimal ApplyDiscount(decimal amount);
} </code></pre>
Ensuite, ajoutons les classes concrètes <span class="css-span">NoDiscountStrategy</span>, <span class="css-span">PointDiscountStrategy</span>, <span class="css-span">CashbackDiscountStrategy</span>, qui implémentent l'interface.
<pre><code>public class NoDiscountStrategy : IDiscountStrategy
{
public string DiscountType => DiscountType.None;
public decimal ApplyDiscount(decimal amount) => amount;
}</code></pre>
<pre><code>public class PointDiscountStrategy : IDiscountStrategy
{
public string DiscountType => DiscountType.Point;
public decimal ApplyDiscount(decimal amount)
{
int pointsToRedeem = GetPointsToRedeem();
decimal redeemValue = CalculatePointRedeemValue(pointsToRedeem);
return amount - redeemValue;
}
}</code></pre>
<pre><code>public class CashbackDiscountStrategy : IDiscountStrategy
{
public string DiscountType => DiscountType.Cashback;
public decimal ApplyDiscount(decimal amount)
{
decimal redeemValue = GetCashBackRedeemValue();
return amount - redeemValue;
}
}</code></pre>
Et pour finir, créons la classe <span class="css-span">ShoppingCartService</span> qui appliquera nos différentes stratégies.
- Une version simple, en utilisant <span class="css-span">FirstOrDefault</span> :
<pre><code>public class ShoppingCartService(IEnumerable<IDiscountStrategy> strategies) : IShoppingCartService
{
private readonly IEnumerable<IDiscountStrategy> _strategies = strategies;
public decimal CalculateTotalAmount(decimal amount, string discountType)
{
var strategy = _strategies.FirstOrDefault(s => s.DiscountType == discountType)
?? throw new InvalidOperationException($"Unknown discount type: {discountType}");
return strategy.ApplyDiscount(amount);
}
}</code></pre>
Avec le pattern stratégie, <span class="css-span">FirstOrDefault</span> suffit pour quelques stratégies, mais un <span class="css-span">Dictionary</span> est préférable dès qu’elles sont nombreuses.
- Une version optimisée en <span class="css-span">Dictionary</span> pour une recherche en O(1) serait :
<pre><code>public class ShoppingCartService(IEnumerable<IDiscountStrategy> strategies) : IShoppingCartService
{
private readonly Dictionary<string, IDiscountStrategy> _strategyMap = strategies.ToDictionary(s => s.DiscountType, s => s);
public decimal CalculateTotalAmount(decimal amount, string discountType)
{
if (_strategyMap.TryGetValue(discountType, out var strategy))
{
return strategy.ApplyDiscount(amount);
}
throw new InvalidOperationException($"Unknown discount type: {discountType}");
}
}</code></pre>
Utilisons <span class="css-span">shoppingCartService</span> pour calculer le montant total selon différents types de réduction :
<pre><code>shoppingCartService.CalculateTotalAmount(200, DiscountType.None) //No discount
shoppingCartService.CalculateTotalAmount(200, DiscountType.Point) //Apply point discount
shoppingCartService.CalculateTotalAmount(200, DiscountType.Cashback) //Apply cashback discount</code></pre>
Refactoriser du code legacy avec le pattern Stratégie
Nous allons ici refactoriser un exemple de code legacy pour le rendre plus maintenable.
EasyReport est une application de génération de rapports dans plusieur formats (PDF, Excel, CSV, JSON, XML et HTML) pour des systèmes externes.
L'idée c'est de générer un rapport dans un format donné, le sauvegarder sur le disque et retourner un message à l'utilisateur.
Voici la version initiale du code:
<pre><code>public enum ReportType
{
Pdf,
Excel,
Csv,
Json,
Xml,
Html
}</code></pre>
<pre><code>public class ReportGeneratorManagerLegacy(
IReportPdfProvider reportPdfProvider,
IReportExcelProvider reportExcelProvider,
IReportCsvProvider reportCsvProvider,
IReportJsonProvider reportJsonProvider,
IReportXmlProvider reportXmlProvider,
IReportHtmlProvider reportHtmlProvider
)
{
public string GenerateReport(ReportType reportType, DataSet data)
{
switch (reportType)
{
case ReportType.Pdf:
return reportPdfProvider.GeneratePdf(data);
case ReportType.Excel:
return reportExcelProvider.GenerateExcel(data);
case ReportType.Csv:
return reportCsvProvider.GenerateCsv(data);
case ReportType.Json:
return reportJsonProvider.GenerateJson(data);
case ReportType.Xml:
return reportXmlProvider.GenerateXml(data);
case ReportType.Html:
return reportHtmlProvider.GenerateHtml(data);
default:
throw new InvalidOperationException($"Unknown report type: {reportType}");
}
}
}</code></pre>
Le <span class="css-span">ReportGeneratorManagerLegacy</span> utilise actuellement un <span class="css-span">switch/case</span> pour gérer les différents types de rapports et leurs providers correspondants, ce qui entraîne du code répétitif et difficile à faire évoluer.
Chaque <span class="css-span">ReportType</span> est traité individuellement et l’ajout d’un nouveau type de rapport implique de modifier la méthode <span class="css-span">GenerateReport</span>. Nous allons donc montrer comment le pattern Stratégie peut transformer ce code en un système centralisé, plus flexible et nettement plus maintenable.
Étape 1 : Identifier le code legacy
La première étape consiste à repérer toutes les méthodes qui utilisent des <span class="css-span">switch/case</span> ou de longues séries de <span class="css-span">if/else</span> pour gérer des comportements différents selon <span class="css-span">ReportType</span>.
Dans notre projet c'est la méthode <span class="css-span">GenerateReport</span> qui centralise toute la logique de génération de rapport via un grand <span class="css-span">switch</span>.
Étape 2 : Créer une interface commune
Toutes les stratégies (providers) doivent implémenter une interface commune.
<pre><code>public interface IReportProvider
{
/// <summary>
/// Type name of the report
/// </summary>
ReportType ReportType { get; }
/// <summary>
/// Export the data and return a success message
/// </summary>
string Export(DataSet data);
}</code></pre>
Étape 3 : Faire hériter les providers existants de l’interface commune
Chaque provider existant doit maintenant implémenter uniquement l’interface <span class="css-span">IReportProvider</span>.
Ensuite, les classes concrètes deviennent simplement :
<pre><code>public class ReportPdfProvider : IReportProvider
{
public ReportType ReportType => ReportType.Pdf;
public string Export(DataSet data)
{
// Specific implementation
}
}
public class ReportExcelProvider : IReportProvider
{
public ReportType ReportType => ReportType.Excel;
public string Export(DataSet data)
{
// Specific implementation
}
}
public class ReportCsvProvider : IReportProvider
{
public ReportType ReportType => ReportType.Csv;
public string Export(DataSet data)
{
// Specific implementation
}
}
public class ReportJsonProvider : IReportProvider
{
public ReportType ReportType => ReportType.Json;
public string Export(DataSet data)
{
// Specific implementation
}
}
public class ReportXmlProvider : IReportProvider
{
public ReportType ReportType => ReportType.Xml;
public string Export(DataSet data)
{
// Specific implementation
}
}
public class ReportHtmlProvider : IReportProvider
{
public ReportType ReportType => ReportType.Html;
public string Export(DataSet data)
{
// Specific implementation
}
}</code></pre>
Avantages :
- Absence de duplication d’interfaces spécifiques.
- Chaque provider expose la même interface, ce qui permet au manager de fonctionner de manière générique et extensible.
Étape 4 : Créer un dictionnaire ReportType → Provider
Dans la class <span class="css-span">ReportGeneratorManager</span>, au lieu des <span class="css-span">switch/case</span>, on centralise tous les providers dans un <span class="css-span">Dictionary</span> :
<pre><code>public class ReportGeneratorManager : IReportGeneratorManager
{
private readonly Dictionary<ReportType, IReportProvider> _reportProviders;
public ReportGeneratorManager(IEnumerable<IReportProvider> reportProviders)
{
_reportProviders = reportProviders.ToDictionary(p => p.ReportType, p => p);
}
}</code></pre>
- <span class="css-span">IEnumerable<IReportProvider></span> injecté via le constructeur.
- Le dictionnaire permet un accès direct à la stratégie correspondant à chaque <span class="css-span">ReportType</span>.
Étape 5 : Réécrire la méthode GenerateReport
Maintenant que nous avons un dictionnaire ReportType → Provider, utilisons-le pour sélectionner le bon provider dans la méthode <span class="css-span">GenerateReport</span>.
<pre><code>public string GenerateReport(ReportType reportType, DataSet data)
{
if (_reportProviders.TryGetValue(reportType, out var provider))
return provider.Export(data);
throw new InvalidOperationException($"Unknown report type: {reportType}");
}</code></pre>
À partir de là, le manager est complètement découplé des implémentations concrètes, facilement testable, et prêt à accueillir de nouveaux types de rapports sans modifier le code existant.
Pourquoi le pattern Stratégie est SOLID
Voici comment ce pattern respecte les principes SOLID :
<html>
<style>
table{
border: 2px solid #FBBE00;
width : 100%;
}
th, td {
border:1px solid #FBBE00;
text-align: center;
}
</style>
<body>
<table>
<tr>
<th>Principe</th>
<th>Description</th>
<th>Application dans la Stratégie</th>
</tr>
<tr>
<td><strong>S</strong></td>
<td>Single Responsibility</td>
<td>Chaque stratégie a une seule responsabilité.</td>
</tr>
<tr>
<td><strong>O</strong></td>
<td>Open/Closed</td>
<td>Nous ajoutons de nouvelles stratégies sans modifier le code existant.</td>
</tr>
<tr>
<td><strong>L</strong></td>
<td>Liskov Substitution</td>
<td>Toute stratégie peut être utilisée là où l’interface est attendue.</td>
</tr>
<tr>
<td><strong>I</strong></td>
<td>Interface Segregation</td>
<td>L’interface est spécifique et minimale.</td>
</tr>
<tr>
<td><strong>D</strong></td>
<td>Dependency Inversion</td>
<td>Le client dépend de l’abstraction (<span class="css-span">IDiscountStrategy</span>, <span class="css-span">IReportProvider</span>) et non des implémentations.</td>
</tr>
</table>
</body>
</html>
Conseils & bonnes pratiques
- Définir une interface claire pour toutes les stratégies.
- Injecter les stratégies via constructeur ou conteneur DI pour pouvoir en ajouter facilement.
- Nommer les stratégies clairement pour éviter toute confusion.
- Prévoir une stratégie par défaut ou une exception pour gérer les cas inconnus
- Tester les stratégies isolément, avec des tests unitaires.
Conclusion
Nous avons vu que le pattern Stratégie est un allié de choix pour rendre le code plus propre, évolutif et SOLID.
Pour l’avoir mis en œuvre à plusieurs reprises dans mes projets, je peux confirmer qu’il transforme réellement la façon dont on conçoit et maintient son code : il élimine les longues logiques conditionnelles, facilite les tests unitaires et prépare notre application à évoluer plus facilement, en accueillant de nouveaux comportements.
En complément du pattern stratégie, d'autres design patterns peuvent également être utilisés pour améliorer la flexibilité et rendre nos projets plus robustes.
📖 Sources :
- Strategy pattern
- Patron de conception
- Principe SOLID
.png)
Brahima est développeur full-stack avec une préférence pour le backend .Net.
Il s’intéresse particulièrement à l’architecture logicielle et aux applications concrètes de l’intelligence artificielle. Pour lui, une application, c’est comme un poème : chaque ligne de code est un vers, chaque fonction une strophe, et quand tout est bien structuré, l’ensemble devient fluide, lisible… et (presque) agréable à maintenir, sauf quand il faut déboguer à 17h un vendredi.
Curieux de nature, il aime partager ses réflexions et apprentissages. En dehors du code, il apprécie le sport.


