MFormations
Modern Design Patterns

Chapitre 9

09 — Patterns Comportementaux : Strategy & Template Method

09 — Patterns Comportementaux : Strategy & Template Method

Chapitre 09 : Strategy & Template Method

Durée estimée : 4 séances de 3h Objectifs : Maîtriser les patterns Strategy et Template Method, savoir remplacer les conditionnels par des stratégies, comprendre l'inversion de contrôle.


Partie 1 : Le Pattern Strategy

1.1 Définition et Intention

Le pattern Strategy permet de définir une famille d'algorithmes, de les encapsuler chacun dans sa propre classe, et de les rendre interchangeables. Le client peut sélectionner l'algorithme souhaité à l'exécution.

Intention du GoF : "Définir une famille d'algorithmes, encapsuler chacun d'eux et les rendre interchangeables. Strategy permet à l'algorithme de varier indépendamment des clients qui l'utilisent."

1.2 Problème résolu

Diagramme en cours de génération...

Sans Strategy : Une classe avec des conditionnels monstres (if/else ou switch). Avec Strategy : Chaque variante d'algorithme est encapsulée dans sa propre classe.

1.3 Structure UML

Diagramme en cours de génération...

1.4 Implémentation — Validation de formulaires

// Strategy interface
interface ValidationStrategy {
    validate(value: any): { isValid: boolean; error?: string };
}

// Concrete strategies
class RequiredValidation implements ValidationStrategy {
    validate(value: any) {
        if (value === null || value === undefined || value === '') {
            return { isValid: false, error: 'This field is required' };
        }
        return { isValid: true };
    }
}

class EmailValidation implements ValidationStrategy {
    validate(value: string) {
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if (!emailRegex.test(value)) {
            return { isValid: false, error: 'Invalid email format' };
        }
        return { isValid: true };
    }
}

class MinLengthValidation implements ValidationStrategy {
    constructor(private min: number) {}

    validate(value: string) {
        if (value.length < this.min) {
            return { isValid: false, error: `Minimum ${this.min} characters required` };
        }
        return { isValid: true };
    }
}

class RangeValidation implements ValidationStrategy {
    constructor(private min: number, private max: number) {}

    validate(value: number) {
        if (value < this.min || value > this.max) {
            return { isValid: false, error: `Value must be between ${this.min} and ${this.max}` };
        }
        return { isValid: true };
    }
}

// Context
class FormField {
    private strategies: ValidationStrategy[] = [];

    constructor(public name: string, public value: any) {}

    addStrategy(strategy: ValidationStrategy): void {
        this.strategies.push(strategy);
    }

    validate(): { isValid: boolean; errors: string[] } {
        const errors: string[] = [];
        for (const strategy of this.strategies) {
            const result = strategy.validate(this.value);
            if (!result.isValid && result.error) {
                errors.push(result.error);
            }
        }
        return { isValid: errors.length === 0, errors };
    }
}

// Usage
const emailField = new FormField('email', 'test@example.com');
emailField.addStrategy(new RequiredValidation());
emailField.addStrategy(new EmailValidation());
emailField.addStrategy(new MinLengthValidation(5));

console.log(emailField.validate()); // { isValid: true, errors: [] }

1.5 Implémentation — Paiement

interface PaymentStrategy {
    pay(amount: number): Promise<{ success: boolean; transactionId: string }>;
}

class CreditCardPayment implements PaymentStrategy {
    constructor(
        private cardNumber: string,
        private cvv: string,
        private expiryDate: string
    ) {}

    async pay(amount: number) {
        console.log(`Processing credit card payment of $${amount}`);
        // Simulate API call
        return {
            success: true,
            transactionId: `CC-${Date.now()}`
        };
    }
}

class PayPalPayment implements PaymentStrategy {
    constructor(private email: string) {}

    async pay(amount: number) {
        console.log(`Processing PayPal payment of $${amount} for ${this.email}`);
        return {
            success: true,
            transactionId: `PP-${Date.now()}`
        };
    }
}

class CryptoPayment implements PaymentStrategy {
    constructor(private walletAddress: string) {}

    async pay(amount: number) {
        console.log(`Processing crypto payment of $${amount} to ${this.walletAddress}`);
        return {
            success: true,
            transactionId: `CR-${Date.now()}`
        };
    }
}

class ShoppingCart {
    private items: { name: string; price: number }[] = [];
    private paymentStrategy?: PaymentStrategy;

    addItem(item: { name: string; price: number }): void {
        this.items.push(item);
    }

    setPaymentStrategy(strategy: PaymentStrategy): void {
        this.paymentStrategy = strategy;
    }

    getTotal(): number {
        return this.items.reduce((sum, item) => sum + item.price, 0);
    }

    async checkout(): Promise<void> {
        if (!this.paymentStrategy) {
            throw new Error('Payment strategy not set');
        }
        const total = this.getTotal();
        const result = await this.paymentStrategy.pay(total);
        console.log(`Checkout ${result.success ? 'successful' : 'failed'}: ${result.transactionId}`);
    }
}

// Usage
const cart = new ShoppingCart();
cart.addItem({ name: 'Laptop', price: 1200 });
cart.addItem({ name: 'Mouse', price: 50 });

cart.setPaymentStrategy(new CreditCardPayment('4111-1111-1111-1111', '123', '12/26'));
await cart.checkout();

1.6 Strategy en Java

import java.util.List;
import java.util.ArrayList;

// Strategy interface
interface SortStrategy<T extends Comparable<T>> {
    void sort(List<T> items);
}

// Concrete strategies
class BubbleSort<T extends Comparable<T>> implements SortStrategy<T> {
    public void sort(List<T> items) {
        int n = items.size();
        for (int i = 0; i < n - 1; i++) {
            for (int j = 0; j < n - i - 1; j++) {
                if (items.get(j).compareTo(items.get(j + 1)) > 0) {
                    T temp = items.get(j);
                    items.set(j, items.get(j + 1));
                    items.set(j + 1, temp);
                }
            }
        }
    }
}

class QuickSort<T extends Comparable<T>> implements SortStrategy<T> {
    public void sort(List<T> items) {
        quickSort(items, 0, items.size() - 1);
    }
    
    private void quickSort(List<T> items, int low, int high) {
        if (low < high) {
            int pi = partition(items, low, high);
            quickSort(items, low, pi - 1);
            quickSort(items, pi + 1, high);
        }
    }
    
    private int partition(List<T> items, int low, int high) {
        T pivot = items.get(high);
        int i = low - 1;
        for (int j = low; j < high; j++) {
            if (items.get(j).compareTo(pivot) <= 0) {
                i++;
                T temp = items.get(i);
                items.set(i, items.get(j));
                items.set(j, temp);
            }
        }
        T temp = items.get(i + 1);
        items.set(i + 1, items.get(high));
        items.set(high, temp);
        return i + 1;
    }
}

class MergeSort<T extends Comparable<T>> implements SortStrategy<T> {
    public void sort(List<T> items) {
        if (items.size() <= 1) return;
        int mid = items.size() / 2;
        List<T> left = new ArrayList<>(items.subList(0, mid));
        List<T> right = new ArrayList<>(items.subList(mid, items.size()));
        sort(left);
        sort(right);
        merge(items, left, right);
    }
    
    private void merge(List<T> items, List<T> left, List<T> right) {
        int i = 0, j = 0, k = 0;
        while (i < left.size() && j < right.size()) {
            if (left.get(i).compareTo(right.get(j)) <= 0) {
                items.set(k++, left.get(i++));
            } else {
                items.set(k++, right.get(j++));
            }
        }
        while (i < left.size()) items.set(k++, left.get(i++));
        while (j < right.size()) items.set(k++, right.get(j++));
    }
}

// Context
class Sorter<T extends Comparable<T>> {
    private SortStrategy<T> strategy;
    
    public void setStrategy(SortStrategy<T> strategy) {
        this.strategy = strategy;
    }
    
    public void sort(List<T> items) {
        if (strategy == null) {
            throw new IllegalStateException("Strategy not set");
        }
        strategy.sort(items);
    }
}

// Usage
public class StrategyDemo {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>(List.of(5, 2, 8, 1, 9, 3));
        
        Sorter<Integer> sorter = new Sorter<>();
        
        sorter.setStrategy(new BubbleSort<>());
        sorter.sort(numbers);
        System.out.println("Bubble sort: " + numbers);
        
        sorter.setStrategy(new QuickSort<>());
        sorter.sort(numbers);
        System.out.println("Quick sort: " + numbers);
    }
}

1.7 Strategy en PHP

<?php
interface CompressionStrategy {
    public function compress(string $filePath): string;
}

class ZipCompression implements CompressionStrategy {
    public function compress(string $filePath): string {
        $zip = new ZipArchive();
        $zipPath = $filePath . '.zip';
        
        if ($zip->open($zipPath, ZipArchive::CREATE) === TRUE) {
            $zip->addFile($filePath);
            $zip->close();
        }
        
        return $zipPath;
    }
}

class GzipCompression implements CompressionStrategy {
    public function compress(string $filePath): string {
        $data = file_get_contents($filePath);
        $compressed = gzencode($data, 9);
        $gzPath = $filePath . '.gz';
        file_put_contents($gzPath, $compressed);
        return $gzPath;
    }
}

class RarCompression implements CompressionStrategy {
    public function compress(string $filePath): string {
        // RAR compression logic (requires external library)
        throw new \Exception('Not implemented');
    }
}

class FileCompressor {
    private CompressionStrategy $strategy;
    
    public function __construct(CompressionStrategy $strategy) {
        $this->strategy = $strategy;
    }
    
    public function setStrategy(CompressionStrategy $strategy): void {
        $this->strategy = $strategy;
    }
    
    public function compress(string $filePath): string {
        return $this->strategy->compress($filePath);
    }
}

// Usage
$compressor = new FileCompressor(new ZipCompression());
$result = $compressor->compress('/path/to/file.txt');
echo "Compressed to: $result\n";

$compressor->setStrategy(new GzipCompression());
$result = $compressor->compress('/path/to/file.txt');
echo "Compressed to: $result\n";
?>

Partie 2 : Le Pattern Template Method

2.1 Définition et Intention

Le Template Method définit le squelette d'un algorithme dans une méthode, en déléguant certaines étapes aux sous-classes. Les sous-classes peuvent redéfinir certaines parties sans changer la structure globale.

Intention du GoF : "Définir le squelette d'un algorithme dans une opération, en reportant certaines étapes aux sous-classes. Template Method permet aux sous-classes de redéfinir certaines étapes d'un algorithme sans en modifier la structure."

2.2 Principe d'Hollywood

"Don't call us, we'll call you" — la classe mère contrôle le déroulement, les sous-classes fournissent les détails.

2.3 Structure UML

Diagramme en cours de génération...

2.4 Implémentation — Data Mining

abstract class DataMiner {
    // Template method
    public mine(path: string): void {
        const file = this.openFile(path);
        const rawData = this.extractData(file);
        const data = this.parseData(rawData);
        this.analyze(data);
        this.sendReport(data);
        this.closeFile(file);
    }

    // Steps with default implementation
    protected openFile(path: string): string {
        console.log(`Opening file: ${path}`);
        return path;
    }

    protected closeFile(file: string): void {
        console.log(`Closing file: ${file}`);
    }

    // Abstract steps (must be overridden)
    protected abstract extractData(file: string): string;
    protected abstract parseData(rawData: string): any;

    // Steps with default
    protected analyze(data: any): void {
        console.log('Performing standard analysis');
    }

    // Hook (optional override)
    protected sendReport(data: any): void {
        console.log('Sending default report');
    }
}

class PDFDataMiner extends DataMiner {
    protected extractData(file: string): string {
        console.log('Extracting text from PDF');
        return 'raw PDF content';
    }

    protected parseData(rawData: string): any {
        console.log('Parsing PDF structure');
        return { format: 'pdf', content: rawData };
    }

    protected sendReport(data: any): void {
        console.log('Sending PDF-specific report');
    }
}

class CSVDataMiner extends DataMiner {
    protected extractData(file: string): string {
        console.log('Reading CSV file line by line');
        return 'name,age\nAlice,30\nBob,25';
    }

    protected parseData(rawData: string): any {
        console.log('Parsing CSV into records');
        const lines = rawData.split('\n');
        const headers = lines[0].split(',');
        return lines.slice(1).map(line => {
            const values = line.split(',');
            return headers.reduce((obj, h, i) => ({ ...obj, [h]: values[i] }), {});
        });
    }

    // Uses default analyze and sendReport
}

// Usage
const pdfMiner = new PDFDataMiner();
pdfMiner.mine('report.pdf');

const csvMiner = new CSVDataMiner();
csvMiner.mine('data.csv');

2.5 Template Method en Java — Framework

// Abstract framework class
abstract class HttpServlet {
    // Template method
    public void service(String request, String response) {
        if (request.startsWith("GET")) {
            doGet(request, response);
        } else if (request.startsWith("POST")) {
            doPost(request, response);
        }
    }
    
    // Hooks — default implementations
    protected void doGet(String request, String response) {
        responseNotAllowed(response);
    }
    
    protected void doPost(String request, String response) {
        responseNotAllowed(response);
    }
    
    private void responseNotAllowed(String response) {
        System.out.println("405 Method Not Allowed");
    }
}

// Concrete implementation
class UserServlet extends HttpServlet {
    @Override
    protected void doGet(String request, String response) {
        System.out.println("GET /users — returning user list");
    }
    
    @Override
    protected void doPost(String request, String response) {
        System.out.println("POST /users — creating user");
    }
}

2.6 Hook Methods

Les hooks sont des méthodes avec une implémentation vide (ou par défaut) que les sous-classes peuvent optionnellement override.

abstract class BeverageMaker {
    // Template method
    makeBeverage(): void {
        this.boilWater();
        this.brew();
        this.pourInCup();
        if (this.customerWantsCondiments()) {
            this.addCondiments();
        }
    }

    protected boilWater(): void {
        console.log('Boiling water');
    }

    protected pourInCup(): void {
        console.log('Pouring into cup');
    }

    protected abstract brew(): void;
    protected abstract addCondiments(): void;

    // Hook — subclass can override to customize behavior
    protected customerWantsCondiments(): boolean {
        return true; // default behavior
    }
}

class CoffeeMaker extends BeverageMaker {
    protected brew(): void {
        console.log('Brewing coffee grounds');
    }

    protected addCondiments(): void {
        console.log('Adding sugar and milk');
    }

    protected customerWantsCondiments(): boolean {
        const answer = prompt('Would you like sugar and milk? (y/n): ');
        return answer?.toLowerCase() === 'y';
    }
}

class TeaMaker extends BeverageMaker {
    protected brew(): void {
        console.log('Steeping the tea');
    }

    protected addCondiments(): void {
        console.log('Adding lemon');
    }

    // Uses default hook (always adds condiments)
}

Partie 3 : Strategy vs Template Method — Analyse Approfondie

3.1 Différences Fondamentales

CritèreStrategyTemplate Method
PrincipeCompositionHéritage
FlexibilitéRuntime (changement dynamique)Compile-time (lié à la sous-classe)
Nombre d'algorithmesPlusieurs interchangeablesStructure fixe, étapes variables
CouplageFaible (interface)Fort (héritage)
TestabilitéTrès facile (mock strategy)Moins facile (héritage rigide)
Usage typiqueValidation, paiement, triFrameworks, pipelines, workflows

3.2 Quand choisir l'un ou l'autre ?

Strategy quand :

  • Vous avez plusieurs algorithmes interchangeables
  • Vous devez changer d'algorithme à l'exécution
  • Vous voulez éviter les conditionnels (if/else ou switch)
  • Les algorithmes sont indépendants du contexte

Template Method quand :

  • Vous avez un algorithme avec une structure fixe mais des étapes variables
  • Vous voulez éviter la duplication de code dans des algorithmes similaires
  • Vous créez un framework où les utilisateurs fournissent des implémentations partielles
  • Le principe d'Hollywood s'applique (inversion de contrôle)

3.3 Anti-patterns

Strategy :

  • Strategy Overkill : Créer une stratégie pour une simple variation
  • Strategy God Class : Une stratégie qui fait trop de choses
  • Stateful Strategy : Une stratégie avec état interne (problème de thread safety)

Template Method :

  • Template Too Rigid : Squelette trop contraignant
  • Template Too Permissive : Trop de hooks → complexité inutile
  • Deep Inheritance : Hiérarchie d'héritage trop profonde

Partie 4 : Implémentations Modernes

4.1 Strategy avec fonctions first-class

En JavaScript/TypeScript, les stratégies peuvent être de simples fonctions :

type TaxStrategy = (amount: number) => number;

const usaTax: TaxStrategy = (amount) => amount * 0.08;
const franceTax: TaxStrategy = (amount) => amount * 0.20;
const uaeTax: TaxStrategy = (amount) => 0; // No tax

class TaxCalculator {
    constructor(private strategy: TaxStrategy) {}

    calculate(amount: number): number {
        return this.strategy(amount);
    }

    setStrategy(strategy: TaxStrategy): void {
        this.strategy = strategy;
    }
}

const calc = new TaxCalculator(usaTax);
console.log(calc.calculate(100)); // 8

calc.setStrategy(franceTax);
console.log(calc.calculate(100)); // 20

4.2 Strategy en Go avec interfaces

package main

import "fmt"

type PaymentStrategy interface {
    Pay(amount float64) string
}

type CreditCard struct {
    number string
}

func (c *CreditCard) Pay(amount float64) string {
    return fmt.Sprintf("Paid $%.2f with credit card %s", amount, c.number)
}

type PayPal struct {
    email string
}

func (p *PayPal) Pay(amount float64) string {
    return fmt.Sprintf("Paid $%.2f with PayPal (%s)", amount, p.email)
}

type Bitcoin struct {
    wallet string
}

func (b *Bitcoin) Pay(amount float64) string {
    return fmt.Sprintf("Paid $%.2f with Bitcoin (%s)", amount, b.wallet)
}

type Checkout struct {
    strategy PaymentStrategy
}

func (c *Checkout) SetStrategy(s PaymentStrategy) {
    c.strategy = s
}

func (c *Checkout) Process(amount float64) string {
    return c.strategy.Pay(amount)
}

func main() {
    checkout := &Checkout{}
    
    checkout.SetStrategy(&CreditCard{number: "4111-1111-1111-1111"})
    fmt.Println(checkout.Process(150.00))
    
    checkout.SetStrategy(&PayPal{email: "user@example.com"})
    fmt.Println(checkout.Process(89.99))
}

4.3 Template Method vs Strategy — exemple comparé

// Template Method approach
abstract class ReportGenerator {
    generate(): string {
        const data = this.collectData();
        const processed = this.processData(data);
        const formatted = this.formatData(processed);
        return this.output(formatted);
    }
    protected abstract collectData(): any;
    protected abstract processData(data: any): any;
    protected formatData(data: any): string {
        return JSON.stringify(data, null, 2);
    }
    protected output(content: string): string {
        console.log(content);
        return content;
    }
}

// Strategy approach
interface ReportStrategy {
    collectData(): any;
    processData(data: any): any;
}

class ReportContext {
    constructor(private strategy: ReportStrategy) {}
    generate(): string {
        const data = this.strategy.collectData();
        const processed = this.strategy.processData(data);
        const formatted = JSON.stringify(processed, null, 2);
        console.log(formatted);
        return formatted;
    }
}

4.4 Strategy Pattern dans les frameworks modernes

// Express.js middleware as Strategy
interface Middleware {
    (req: any, res: any, next: (err?: any) => void): void;
}

const authMiddleware: Middleware = (req, res, next) => {
    const token = req.headers.authorization;
    if (!token) {
        return res.status(401).json({ error: 'Unauthorized' });
    }
    next();
};

const loggingMiddleware: Middleware = (req, res, next) => {
    console.log(`${req.method} ${req.url}`);
    next();
};

const rateLimitMiddleware: Middleware = (req, res, next) => {
    // Rate limiting logic
    next();
};

// Usage
app.use(authMiddleware);
app.use(loggingMiddleware);

Partie 5 : Exercices et Cas Pratiques

5.1 Implémenter un compresseur de fichiers

Utiliser Strategy pour :

  • ZIP, GZIP, RAR, 7z
  • Ajouter une nouvelle stratégie sans modifier le client
  • Benchmarker chaque stratégie

5.2 Implémenter un pipeline ETL avec Template Method

Diagramme en cours de génération...

Le squelette est fixe mais chaque étape varie selon la source (CSV, API, DB).


Résumé

  • Strategy : Algorithme interchangeable via composition. Runtime flexible.
  • Template Method : Squelette d'algorithme via héritage. Compile-time stable.
  • Principe : Strategy pour la variabilité, Template Method pour l'inversion de contrôle.
  • Modernes : Fonctions first-class, closures, interfaces Go.

Prochain chapitre : Command & Chain of Responsibility.