Polar Code 🎭

Command Palette

Search for a command to run...

25
Pièce N°25

CHAPITRE 24 – STRATEGY

Le plan était là, sur la table. Plusieurs routes pour atteindre la cible : approche frontale, infiltration, bluff. Chaque tactic avait ses avantages, ses risques. Le chef d’équipe devait choisir en fonction du contexte : nombre de gardes, météo, temps disponible. Dans le code, c’était un énorme if/else qui choisissait l’algorithme à exécuter. La logique de choix était mélangée à l’exécution. Il fallait extraire chaque algorithme, le rendre interchangeable. Il fallait une Stratégie.

Encapsulation des algorithmes

Le pattern Strategy définit une famille d’algorithmes, les encapsule chacun dans une classe séparée, et les rend interchangeables. Il permet à l’algorithme de varier indépendamment des clients qui l’utilisent.

Le cœur du pattern est une interface commune (Strategy) que tous les algorithmes concrets implémentent. Le contexte (la classe qui utilise l’algorithme) contient une référence à un objet Strategy et délègue le travail à cet objet. On peut changer l’algorithme utilisé en changeant l’objet Strategy référencé.

Sélection dynamique

Contrairement à des conditions figées dans le code (if (condition) AlgoA else AlgoB), la stratégie peut être changée à l’exécution. On peut injecter la stratégie appropriée en fonction de la configuration, des préférences utilisateur, ou de l’état du système.

Exemple : Système de calcul de taxe (algorithme variable par pays)

// Interface Strategy (l'algorithme)
interface StrategieTaxe {
    calculer(montant: number): number;
    getNom(): string;
}

// Strategies Concrètes
class StrategieTaxeFR implements StrategieTaxe {
    calculer(montant: number): number {
        // TVA à 20%
        return montant * 0.20;
    }
    getNom(): string { return "TVA France (20%)"; }
}

class StrategieTaxeDE implements StrategieTaxe {
    calculer(montant: number): number {
        // TVA à 19%
        return montant * 0.19;
    }
    getNom(): string { return "TVA Allemagne (19%)"; }
}

class StrategieTaxeUS implements StrategieTaxe {
    calculer(montant: number): number {
        // Taxe dépendant de l'état. Ici, simplifié : 8%
        return montant * 0.08;
    }
    getNom(): string { return "Sales Tax US (~8%)"; }
}

class StrategieTaxeZero implements StrategieTaxe {
    calculer(montant: number): number {
        return 0; // Exonéré
    }
    getNom(): string { return "Taxe Zéro"; }
}

// Contexte : Le calculateur qui utilise une stratégie
class CalculateurFacture {
    private strategie: StrategieTaxe;

    // Injection de la stratégie par le constructeur
    constructor(strategie: StrategieTaxe) {
        this.strategie = strategie;
    }

    // Permet de changer la stratégie après coup
    setStrategie(strategie: StrategieTaxe): void {
        console.log(`🔄 Changement de stratégie: ${this.strategie.getNom()} -> ${strategie.getNom()}`);
        this.strategie = strategie;
    }

    // Méthode métier qui délègue le calcul à la stratégie
    calculerMontantAvecTaxe(montantHorsTaxe: number): number {
        const taxe = this.strategie.calculer(montantHorsTaxe);
        const total = montantHorsTaxe + taxe;
        console.log(`   Calcul (${this.strategie.getNom()}): ${montantHorsTaxe}€ + ${taxe.toFixed(2)}€ taxe = ${total.toFixed(2)}€`);
        return total;
    }
}

// --- Scénario d'utilisation ---
console.log("=== SYSTÈME DE FACTURATION (Strategy Pattern) ===\n");

// Client français
console.log("--- Client en France ---");
const strategieFR = new StrategieTaxeFR();
const factureFR = new CalculateurFacture(strategieFR);
factureFR.calculerMontantAvecTaxe(100);

// Client allemand
console.log("\n--- Client en Allemagne ---");
const factureDE = new CalculateurFacture(new StrategieTaxeDE());
factureDE.calculerMontantAvecTaxe(100);

// Client US
console.log("\n--- Client aux USA ---");
const factureUS = new CalculateurFacture(new StrategieTaxeUS());
factureUS.calculerMontantAvecTaxe(100);

// Changement dynamique de stratégie (ex: exonération)
console.log("\n--- Changement dynamique (Exonération) ---");
factureFR.setStrategie(new StrategieTaxeZero());
factureFR.calculerMontantAvecTaxe(100);

Cas business avancé : Validation de données avec stratégies composables

Un cas plus complexe où les stratégies peuvent être combinées.

// Interface Strategy pour la validation
interface Validateur {
    valider(valeur: any): { valide: boolean; erreur?: string };
}

// Strategies concrètes de validation
class ValidateurRequise implements Validateur {
    valider(valeur: any): { valide: boolean; erreur?: string } {
        if (valeur === null || valeur === undefined || valeur === '') {
            return { valide: false, erreur: 'Ce champ est requis.' };
        }
        return { valide: true };
    }
}

class ValidateurEmail implements Validateur {
    private regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    valider(valeur: any): { valide: boolean; erreur?: string } {
        if (valeur && !this.regex.test(valeur)) {
            return { valide: false, erreur: 'Format d\'email invalide.' };
        }
        return { valide: true };
    }
}

class ValidateurLongueurMin implements Validateur {
    constructor(private min: number) {}
    valider(valeur: any): { valide: boolean; erreur?: string } {
        if (valeur && valeur.length < this.min) {
            return { valide: false, erreur: `Doit contenir au moins ${this.min} caractères.` };
        }
        return { valide: true };
    }
}

class ValidateurLongueurMax implements Validateur {
    constructor(private max: number) {}
    valider(valeur: any): { valide: boolean; erreur?: string } {
        if (valeur && valeur.length > this.max) {
            return { valide: false, erreur: `Ne doit pas dépasser ${this.max} caractères.` };
        }
        return { valide: true };
    }
}

// Contexte : Un champ de formulaire qui utilise une liste de validateurs
class ChampFormulaire {
    private validateurs: Validateur[] = [];

    constructor(private nom: string) {}

    ajouterValidateur(validateur: Validateur): void {
        this.validateurs.push(validateur);
    }

    valider(valeur: any): { valide: boolean; erreurs: string[] } {
        const erreurs: string[] = [];
        for (const validateur of this.validateurs) {
            const resultat = validateur.valider(valeur);
            if (!resultat.valide && resultat.erreur) {
                erreurs.push(resultat.erreur);
            }
        }
        const valide = erreurs.length === 0;
        if (!valide) {
            console.log(`❌ Validation du champ "${this.nom}" échouée : ${erreurs.join(', ')}`);
        } else {
            console.log(`✅ Champ "${this.nom}" valide.`);
        }
        return { valide, erreurs };
    }
}

// --- Utilisation : Configuration flexible de validation ---
console.log("\n=== SYSTÈME DE VALIDATION (Strategy Composable) ===\n");

const champEmail = new ChampFormulaire('email');
champEmail.ajouterValidateur(new ValidateurRequise());
champEmail.ajouterValidateur(new ValidateurEmail());
champEmail.ajouterValidateur(new ValidateurLongueurMax(100));

champEmail.valider(''); // Requise
champEmail.valider('invalid-email'); // Format
champEmail.valider('a@b.c'); // OK

const champPassword = new ChampFormulaire('password');
champPassword.ajouterValidateur(new ValidateurRequise());
champPassword.ajouterValidateur(new ValidateurLongueurMin(8));
champPassword.ajouterValidateur(new ValidateurLongueurMax(50));

champPassword.valider('123'); // Trop court
champPassword.valider('monMotDePasseSecret'); // OK

Comparaison avec State (rappel)

Pour bien ancrer la différence avec le chapitre précédent :

  • Strategy : Le choix de l’algorithme est externe au contexte. Le contexte a une stratégie. On lui dit « utilise cet algorithme ». C’est un choix délibéré.
  • State : Le comportement change automatiquement en fonction d’un état interne. Le contexte est dans un état. Les états se connaissent et gèrent les transitions.

Avantages de Strategy

  • Élimine les conditions : Plus de if/else ou de switch pour choisir entre algorithmes.
  • Principe Ouvert/Fermé : On peut ajouter de nouvelles stratégies sans modifier le contexte ni les autres stratégies.
  • Testabilité : Chaque stratégie est une petite classe isolée, facile à tester unitairement.
  • Composition : Les stratégies peuvent souvent être combinées (comme les validateurs).

Inconvénients

  • Augmentation du nombre de classes : Comme pour State, on peut se retrouver avec beaucoup de petites classes.
  • Le client doit connaître les stratégies : C’est au client (ou à une factory) de choisir et d’instancier la bonne stratégie. Cela peut déplacer la complexité ailleurs.
  • Communication entre stratégie et contexte : Parfois, la stratégie a besoin de données du contexte. Il faut les lui passer en paramètre, ce qui peut entraîner une interface de stratégie trop large.

Le chef d’équipe avait maintenant un panel de tactiques claires, prêtes à l’emploi. Le pattern Strategy avait transformé un choix conditionnel en une collection d’objets interchangeables. C’était l’essence du polymorphisme appliqué aux algorithmes.

C’était aussi le dernier des patterns comportementaux « fondamentaux ». Les suivants (Template Method, Visitor, etc.) sont des variations ou des spécialisations.

Le prochain, Template Method, proposerait une autre approche : définir le squelette d’un algorithme dans une classe de base, en laissant les sous-classes implémenter certaines étapes. L’héritage au service de la réutilisation.