Skip to content

COURS COMPLET: DESIGN PATTERNS EN PROGRAMMATION AVEC TYPESCRIPT

Table des matières

  1. Introduction aux Design Patterns
  2. Patterns Créationnels (Creational Patterns)
  3. Patterns Structurels (Structural Patterns)
  4. Patterns Comportementaux (Behavioral Patterns)
  5. Cas d'usage et bonnes pratiques

1. INTRODUCTION AUX DESIGN PATTERNS

Les design patterns sont des solutions éprouvées et réutilisables pour résoudre des problèmes courants en conception logicielle. Ils ont été formalisés par le Gang of Four (GoF) en 1994 et classifiés en trois catégories principales.

Les 23 Patterns GoF

Créationnels (5): Singleton, Factory Method, Abstract Factory, Builder, Prototype Structurels (7): Adapter, Composite, Decorator, Facade, Flyweight, Bridge, Proxy Comportementaux (11): Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor


2. PATTERNS CRÉATIONNELS

2.1 SINGLETON PATTERN

Objectif: Garantir qu'une classe n'a qu'une seule instance et fournir un point d'accès global.

typescript
// Singleton Pattern - Gestion de base de données
class DatabaseConnection {
  private static instance: DatabaseConnection;
  private connectionString: string;

  private constructor(connectionString: string) {
    this.connectionString = connectionString;
    console.log("Connexion à la base de données établie");
  }

  public static getInstance(connectionString: string = "default"): DatabaseConnection {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection(connectionString);
    }
    return DatabaseConnection.instance;
  }

  public getConnection(): string {
    return this.connectionString;
  }

  public executeQuery(query: string): void {
    console.log(`Exécution: ${query} sur ${this.connectionString}`);
  }
}

// Utilisation
const db1 = DatabaseConnection.getInstance("Server=localhost");
const db2 = DatabaseConnection.getInstance("Server=localhost");
console.log(db1 === db2); // true - même instance
db1.executeQuery("SELECT * FROM users");

Cas d'usage: Gestion de base de données, logging, configuration, cache Avantages: Une seule instance, accès global, économise les ressources Inconvénients: Peut masquer les dépendances, moins testable


2.2 FACTORY METHOD PATTERN

Objectif: Créer des objets sans spécifier leurs classes concrètes exactes.

typescript
// Factory Method Pattern - Système de transports
interface Transport {
  deliver(cargo: string): void;
}

class Truck implements Transport {
  deliver(cargo: string): void {
    console.log(`Livraison par camion: ${cargo}`);
  }
}

class Ship implements Transport {
  deliver(cargo: string): void {
    console.log(`Livraison par bateau: ${cargo}`);
  }
}

class Plane implements Transport {
  deliver(cargo: string): void {
    console.log(`Livraison par avion: ${cargo}`);
  }
}

abstract class Logistics {
  abstract createTransport(): Transport;

  planDelivery(cargo: string): void {
    const transport = this.createTransport();
    transport.deliver(cargo);
  }
}

class RoadLogistics extends Logistics {
  createTransport(): Transport {
    return new Truck();
  }
}

class SeaLogistics extends Logistics {
  createTransport(): Transport {
    return new Ship();
  }
}

class AirLogistics extends Logistics {
  createTransport(): Transport {
    return new Plane();
  }
}

// Utilisation
const roadLogistics = new RoadLogistics();
roadLogistics.planDelivery("Colis de 100kg");

const seaLogistics = new SeaLogistics();
seaLogistics.planDelivery("Container de marchandises");

Cas d'usage: Systèmes plug-and-play, frameworks, interfaces polymorphes Avantages: Découplage, extensibilité, respect du principe ouvert/fermé Inconvénients: Augmente le nombre de classes


2.3 ABSTRACT FACTORY PATTERN

Objectif: Créer des familles d'objets liés sans spécifier leurs classes concrètes.

typescript
// Abstract Factory Pattern - Thèmes GUI multi-plateforme
interface Button {
  render(): void;
}

interface Checkbox {
  render(): void;
}

class WindowsButton implements Button {
  render(): void {
    console.log("Rendu d'un bouton Windows");
  }
}

class MacButton implements Button {
  render(): void {
    console.log("Rendu d'un bouton macOS");
  }
}

class WindowsCheckbox implements Checkbox {
  render(): void {
    console.log("Rendu d'une case à cocher Windows");
  }
}

class MacCheckbox implements Checkbox {
  render(): void {
    console.log("Rendu d'une case à cocher macOS");
  }
}

interface GUIFactory {
  createButton(): Button;
  createCheckbox(): Checkbox;
}

class WindowsFactory implements GUIFactory {
  createButton(): Button {
    return new WindowsButton();
  }
  createCheckbox(): Checkbox {
    return new WindowsCheckbox();
  }
}

class MacFactory implements GUIFactory {
  createButton(): Button {
    return new MacButton();
  }
  createCheckbox(): Checkbox {
    return new MacCheckbox();
  }
}

class Application {
  private factory: GUIFactory;

  constructor(factory: GUIFactory) {
    this.factory = factory;
  }

  render(): void {
    const button = this.factory.createButton();
    const checkbox = this.factory.createCheckbox();
    button.render();
    checkbox.render();
  }
}

// Utilisation
const windowsApp = new Application(new WindowsFactory());
windowsApp.render();

const macApp = new Application(new MacFactory());
macApp.render();

Cas d'usage: Systèmes multi-plateforme, thèmes UI, familles de produits Avantages: Garantit la cohérence des familles d'objets Inconvénients: Plus complexe que Factory Method


2.4 BUILDER PATTERN

Objectif: Construire des objets complexes étape par étape.

typescript
// Builder Pattern - Construction de rapport
class Report {
  title: string = "";
  author: string = "";
  content: string = "";
  sections: string[] = [];
  images: string[] = [];

  describe(): void {
    console.log(`
      Titre: ${this.title}
      Auteur: ${this.author}
      Contenu: ${this.content}
      Sections: ${this.sections.join(", ")}
      Images: ${this.images.length}
    `);
  }
}

class ReportBuilder {
  private report: Report = new Report();

  setTitle(title: string): ReportBuilder {
    this.report.title = title;
    return this;
  }

  setAuthor(author: string): ReportBuilder {
    this.report.author = author;
    return this;
  }

  setContent(content: string): ReportBuilder {
    this.report.content = content;
    return this;
  }

  addSection(section: string): ReportBuilder {
    this.report.sections.push(section);
    return this;
  }

  addImage(imagePath: string): ReportBuilder {
    this.report.images.push(imagePath);
    return this;
  }

  build(): Report {
    return this.report;
  }
}

// Utilisation
const report = new ReportBuilder()
  .setTitle("Rapport Annuel 2024")
  .setAuthor("Jean Dupont")
  .setContent("Résumé des activités...")
  .addSection("Introduction")
  .addSection("Résultats")
  .addSection("Conclusion")
  .addImage("chart.png")
  .addImage("graph.png")
  .build();

report.describe();

Cas d'usage: Création d'objets complexes avec nombreux paramètres optionnels Avantages: Meilleure lisibilité, construction étape par étape Inconvénients: Plus de code que le constructeur direct


2.5 PROTOTYPE PATTERN

Objectif: Créer des objets par clonage d'un prototype plutôt que de zéro.

typescript
// Prototype Pattern - Système de configuration utilisateur
interface Cloneable {
  clone(): Cloneable;
}

class UserProfile implements Cloneable {
  username: string;
  email: string;
  preferences: {
    theme: string;
    language: string;
    notifications: boolean;
  };

  constructor(username: string, email: string) {
    this.username = username;
    this.email = email;
    this.preferences = {
      theme: "light",
      language: "fr",
      notifications: true
    };
  }

  clone(): UserProfile {
    const cloned = new UserProfile(this.username, this.email);
    cloned.preferences = { ...this.preferences };
    return cloned;
  }

  display(): void {
    console.log(`
      Utilisateur: ${this.username}
      Email: ${this.email}
      Thème: ${this.preferences.theme}
      Langue: ${this.preferences.language}
    `);
  }
}

// Utilisation
const templateUser = new UserProfile("template", "template@example.com");
templateUser.preferences.theme = "dark";

const user1 = templateUser.clone();
user1.username = "alice";
user1.email = "alice@example.com";

const user2 = templateUser.clone();
user2.username = "bob";
user2.email = "bob@example.com";

user1.display(); // Alice avec thème dark
user2.display(); // Bob avec thème dark aussi

Cas d'usage: Configuration par défaut, copie d'objets complexes Avantages: Plus efficace que la création à zéro Inconvénients: Nécessite une implémentation correcte du clonage profond


3. PATTERNS STRUCTURELS

3.1 ADAPTER PATTERN

Objectif: Faire fonctionner ensemble des interfaces incompatibles.

typescript
// Adapter Pattern - Adaptateur de lecteur vidéo
interface NewMediaPlayer {
  playAudio(type: string, fileName: string): void;
  playVideo(type: string, fileName: string): void;
}

interface OldMediaPlayer {
  playMp3(fileName: string): void;
}

class LegacyMediaPlayer implements OldMediaPlayer {
  playMp3(fileName: string): void {
    console.log(`▶ Lecture MP3: ${fileName}`);
  }
}

class MediaAdapter implements NewMediaPlayer {
  private legacyPlayer: OldMediaPlayer;

  constructor(legacyPlayer: OldMediaPlayer) {
    this.legacyPlayer = legacyPlayer;
  }

  playAudio(type: string, fileName: string): void {
    if (type === "mp3") {
      this.legacyPlayer.playMp3(fileName);
    } else if (type === "wav") {
      console.log(`▶ Lecture WAV: ${fileName}`);
    } else {
      console.log(`✗ Format non supporté: ${type}`);
    }
  }

  playVideo(type: string, fileName: string): void {
    console.log(`✗ Vidéo non supportée par l'adaptateur`);
  }
}

class ModernMediaPlayer implements NewMediaPlayer {
  playAudio(type: string, fileName: string): void {
    console.log(`▶ Lecture ${type.toUpperCase()}: ${fileName}`);
  }

  playVideo(type: string, fileName: string): void {
    console.log(`▶ Lecture ${type.toUpperCase()}: ${fileName}`);
  }
}

// Utilisation
const adapter = new MediaAdapter(new LegacyMediaPlayer());
adapter.playAudio("mp3", "chanson.mp3");

const modern = new ModernMediaPlayer();
modern.playVideo("mp4", "film.mp4");

Cas d'usage: Intégration de code hérité, adaptation d'API tierces Avantages: Permet l'utilisation de code existant incompatible Inconvénients: Ajoute une couche supplémentaire


3.2 DECORATOR PATTERN

Objectif: Ajouter des responsabilités à un objet dynamiquement.

typescript
// Decorator Pattern - Système de décoration de texte
interface TextComponent {
  render(): string;
}

class PlainText implements TextComponent {
  constructor(private text: string) {}

  render(): string {
    return this.text;
  }
}

abstract class TextDecorator implements TextComponent {
  constructor(protected component: TextComponent) {}

  render(): string {
    return this.component.render();
  }
}

class BoldDecorator extends TextDecorator {
  render(): string {
    return `**${super.render()}**`;
  }
}

class ItalicDecorator extends TextDecorator {
  render(): string {
    return `*${super.render()}*`;
  }
}

class UnderlineDecorator extends TextDecorator {
  render(): string {
    return `_${super.render()}_`;
  }
}

// Utilisation
let text: TextComponent = new PlainText("Hello World");
console.log(text.render()); // Hello World

text = new BoldDecorator(text);
console.log(text.render()); // **Hello World**

text = new ItalicDecorator(text);
console.log(text.render()); // *__Hello World__*

text = new UnderlineDecorator(text);
console.log(text.render()); // _*__Hello World__*_

Cas d'usage: Ajout de fonctionnalités sans altérer l'original Avantages: Flexibilité, combinaisons multiples Inconvénients: Création de nombreux petits objets


3.3 FACADE PATTERN

Objectif: Fournir une interface simplifiée à un système complexe.

typescript
// Facade Pattern - Automatisation maison
class LightSystem {
  turnOn(): void { console.log("💡 Lumières allumées"); }
  turnOff(): void { console.log("💡 Lumières éteintes"); }
}

class SecuritySystem {
  arm(): void { console.log("🔒 Système de sécurité armé"); }
  disarm(): void { console.log("🔓 Système de sécurité désarmé"); }
}

class HeatingSystem {
  on(): void { console.log("🔥 Chauffage activé"); }
  off(): void { console.log("🔥 Chauffage désactivé"); }
}

class EntertainmentSystem {
  start(): void { console.log("🎬 Système d'entertainment lancé"); }
  stop(): void { console.log("🎬 Système d'entertainment arrêté"); }
}

class HomeAutomationFacade {
  private lights: LightSystem;
  private security: SecuritySystem;
  private heating: HeatingSystem;
  private entertainment: EntertainmentSystem;

  constructor() {
    this.lights = new LightSystem();
    this.security = new SecuritySystem();
    this.heating = new HeatingSystem();
    this.entertainment = new EntertainmentSystem();
  }

  goodMorning(): void {
    console.log("🌅 Bonne routine du matin...");
    this.lights.turnOn();
    this.security.disarm();
    this.heating.on();
  }

  goodNight(): void {
    console.log("🌙 Bonne nuit...");
    this.lights.turnOff();
    this.security.arm();
    this.heating.off();
    this.entertainment.stop();
  }

  movieTime(): void {
    console.log("🎥 Mode cinéma activé...");
    this.lights.turnOff();
    this.entertainment.start();
  }
}

// Utilisation
const home = new HomeAutomationFacade();
home.goodMorning();
home.movieTime();
home.goodNight();

Cas d'usage: Simplification de systèmes complexes Avantages: Interface simple et claire Inconvénients: Peut masquer la complexité


3.4 BRIDGE PATTERN

Objectif: Découpler une abstraction de son implémentation.

typescript
// Bridge Pattern - Système de rendu graphique
interface Renderer {
  renderCircle(x: number, y: number, radius: number): void;
  renderRectangle(x: number, y: number, width: number, height: number): void;
}

class VectorRenderer implements Renderer {
  renderCircle(x: number, y: number, radius: number): void {
    console.log(`📐 Cercle vecteur: (${x},${y}) r=${radius}`);
  }
  renderRectangle(x: number, y: number, width: number, height: number): void {
    console.log(`📐 Rectangle vecteur: (${x},${y}) ${width}x${height}`);
  }
}

class RasterRenderer implements Renderer {
  renderCircle(x: number, y: number, radius: number): void {
    console.log(`🖼️ Cercle raster: (${x},${y}) r=${radius}`);
  }
  renderRectangle(x: number, y: number, width: number, height: number): void {
    console.log(`🖼️ Rectangle raster: (${x},${y}) ${width}x${height}`);
  }
}

abstract class Shape {
  protected renderer: Renderer;

  constructor(renderer: Renderer) {
    this.renderer = renderer;
  }

  abstract draw(): void;
}

class Circle extends Shape {
  constructor(private x: number, private y: number, private radius: number, renderer: Renderer) {
    super(renderer);
  }

  draw(): void {
    this.renderer.renderCircle(this.x, this.y, this.radius);
  }
}

class Rectangle extends Shape {
  constructor(private x: number, private y: number, private width: number, private height: number, renderer: Renderer) {
    super(renderer);
  }

  draw(): void {
    this.renderer.renderRectangle(this.x, this.y, this.width, this.height);
  }
}

// Utilisation
const vectorRenderer = new VectorRenderer();
const circle = new Circle(50, 50, 25, vectorRenderer);
circle.draw();

const rasterRenderer = new RasterRenderer();
const rectangle = new Rectangle(10, 10, 100, 50, rasterRenderer);
rectangle.draw();

Cas d'usage: Abstraction et implémentation indépendantes Avantages: Flexibilité, réduction de couplage Inconvénients: Augmente la complexité


3.5 COMPOSITE PATTERN

Objectif: Composer des objets en structures arborescentes.

typescript
// Composite Pattern - Système de fichiers
interface FileSystemComponent {
  display(indent: string): void;
  getSize(): number;
}

class File implements FileSystemComponent {
  constructor(private name: string, private size: number) {}

  display(indent: string): void {
    console.log(indent + `📄 ${this.name} (${this.size} KB)`);
  }

  getSize(): number {
    return this.size;
  }
}

class Directory implements FileSystemComponent {
  private components: FileSystemComponent[] = [];

  constructor(private name: string) {}

  add(component: FileSystemComponent): void {
    this.components.push(component);
  }

  remove(component: FileSystemComponent): void {
    const index = this.components.indexOf(component);
    if (index > -1) {
      this.components.splice(index, 1);
    }
  }

  display(indent: string): void {
    console.log(indent + `📁 ${this.name}/`);
    this.components.forEach(component => {
      component.display(indent + "  ");
    });
  }

  getSize(): number {
    return this.components.reduce((sum, comp) => sum + comp.getSize(), 0);
  }
}

// Utilisation
const root = new Directory("root");
const file1 = new File("document.txt", 10);
const file2 = new File("image.png", 500);

const docs = new Directory("Documents");
const file3 = new File("report.pdf", 100);

root.add(file1);
root.add(file2);
root.add(docs);
docs.add(file3);

root.display("");
console.log(`Taille totale: ${root.getSize()} KB`);

Cas d'usage: Structures hiérarchiques, menus imbriqués, systèmes de fichiers Avantages: Traitement unifié des feuilles et branches Inconvénients: Peut être trop flexible dans certains cas


3.6 FLYWEIGHT PATTERN

Objectif: Partager efficacement les objets pour économiser la mémoire.

typescript
// Flyweight Pattern - Système de polices de caractères
class Character {
  constructor(private char: string, private font: string, private size: number) {}

  display(row: number, column: number): void {
    console.log(`'${this.char}' (${this.font} ${this.size}pt) à (${row},${column})`);
  }

  getKey(): string {
    return `${this.char}-${this.font}-${this.size}`;
  }
}

class CharacterFactory {
  private cache: Map<string, Character> = new Map();

  getCharacter(char: string, font: string, size: number): Character {
    const key = `${char}-${font}-${size}`;

    if (!this.cache.has(key)) {
      console.log(`📝 Création du caractère: ${key}`);
      this.cache.set(key, new Character(char, font, size));
    } else {
      console.log(`✓ Réutilisation du caractère: ${key}`);
    }

    return this.cache.get(key)!;
  }

  getCacheSize(): number {
    return this.cache.size;
  }
}

// Utilisation
const factory = new CharacterFactory();

const char1 = factory.getCharacter("A", "Arial", 12);
const char2 = factory.getCharacter("A", "Arial", 12); // Réutilisé
const char3 = factory.getCharacter("B", "Arial", 12);

console.log(char1 === char2); // true
console.log(`Objets en cache: ${factory.getCacheSize()}`); // 2

Cas d'usage: Réduction de la consommation mémoire Avantages: Économie significative de mémoire Inconvénients: Augmente la complexité du code


3.7 PROXY PATTERN

Objectif: Contrôler l'accès à un autre objet.

typescript
// Proxy Pattern - Proxy de base de données avec logging et caching
interface IDatabase {
  query(sql: string): string;
}

class RealDatabase implements IDatabase {
  query(sql: string): string {
    console.log(`🔍 Exécution de la requête: ${sql}`);
    return `Résultat: ${Math.random()}`;
  }
}

class DatabaseProxy implements IDatabase {
  private realDatabase: RealDatabase | null = null;
  private accessLog: Array<{ timestamp: Date; query: string }> = [];
  private cache: Map<string, string> = new Map();
  private accessCount = 0;

  query(sql: string): string {
    this.accessCount++;
    console.log(`📊 Accès #${this.accessCount}`);

    // Vérifier le cache
    if (this.cache.has(sql)) {
      console.log(`✓ Résultat en cache`);
      return this.cache.get(sql)!;
    }

    // Lazy loading
    if (!this.realDatabase) {
      console.log(`⏳ Initialisation de la base de données...`);
      this.realDatabase = new RealDatabase();
    }

    // Journaliser l'accès
    this.accessLog.push({ timestamp: new Date(), query: sql });

    const result = this.realDatabase.query(sql);
    this.cache.set(sql, result);

    return result;
  }

  getAccessLog(): string {
    return this.accessLog.map(log => 
      `${log.timestamp.toISOString()}: ${log.query}`
    ).join("\n");
  }
}

// Utilisation
const proxy = new DatabaseProxy();
console.log(proxy.query("SELECT * FROM users"));
console.log(proxy.query("SELECT * FROM users")); // Depuis le cache
console.log(proxy.query("SELECT * FROM products"));
console.log("\n--- Historique d'accès ---");
console.log(proxy.getAccessLog());

Cas d'usage: Lazy loading, contrôle d'accès, caching, logging Avantages: Contrôle fin, protection des ressources Inconvénients: Légère augmentation de latence


4. PATTERNS COMPORTEMENTAUX

4.1 OBSERVER PATTERN

Objectif: Notifier plusieurs objets de changements d'état.

typescript
// Observer Pattern - Système de notifications météorologiques
interface IObserver {
  update(data: WeatherData): void;
}

interface ISubject {
  attach(observer: IObserver): void;
  detach(observer: IObserver): void;
  notify(): void;
}

interface WeatherData {
  temperature: number;
  humidity: number;
  pressure: number;
}

class WeatherStation implements ISubject {
  private observers: IObserver[] = [];
  private weatherData: WeatherData = { temperature: 0, humidity: 0, pressure: 0 };

  attach(observer: IObserver): void {
    this.observers.push(observer);
    console.log(`✓ Observateur attaché`);
  }

  detach(observer: IObserver): void {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  notify(): void {
    this.observers.forEach(observer => observer.update(this.weatherData));
  }

  setWeatherData(data: WeatherData): void {
    this.weatherData = data;
    this.notify();
  }
}

class PhoneDisplay implements IObserver {
  update(data: WeatherData): void {
    console.log(`📱 Téléphone: T=${data.temperature}°C, H=${data.humidity}%, P=${data.pressure}mb`);
  }
}

class WebDisplay implements IObserver {
  update(data: WeatherData): void {
    console.log(`🌐 Web: Température ${data.temperature}°C`);
  }
}

// Utilisation
const station = new WeatherStation();
const phone = new PhoneDisplay();
const web = new WebDisplay();

station.attach(phone);
station.attach(web);

station.setWeatherData({ temperature: 20, humidity: 65, pressure: 1013 });

Cas d'usage: Systèmes d'événements, mise à jour d'affichage Avantages: Découplage faible, réactivité Inconvénients: Ordre de notification imprévisible


4.2 STRATEGY PATTERN

Objectif: Encapsuler une famille d'algorithmes interchangeables.

typescript
// Strategy Pattern - Système de paiement
interface PaymentStrategy {
  pay(amount: number): boolean;
}

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

  pay(amount: number): boolean {
    console.log(`💳 Paiement par carte ${this.cardNumber}: ${amount}€`);
    return true;
  }
}

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

  pay(amount: number): boolean {
    console.log(`🅿️ Paiement PayPal ${this.email}: ${amount}€`);
    return true;
  }
}

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

  pay(amount: number): boolean {
    console.log(`₿ Paiement crypto ${this.walletAddress}: ${amount}€`);
    return Math.random() > 0.1; // Simuler un risque d'échec
  }
}

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

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

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

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

  checkout(): void {
    const total = this.items.reduce((sum, item) => sum + item.price, 0);
    if (this.paymentStrategy.pay(total)) {
      console.log(`✓ Commande payée avec succès`);
    } else {
      console.log(`✗ Paiement échoué`);
    }
  }
}

// Utilisation
const cart = new ShoppingCart(new CreditCardPayment("1234-5678-9012-3456"));
cart.addItem("Livre", 15);
cart.addItem("Stylo", 2);
cart.checkout();

cart.setPaymentStrategy(new PayPalPayment("user@example.com"));
cart.checkout();

Cas d'usage: Algorithmes interchangeables à runtime Avantages: Flexibilité, séparation des préoccupations Inconvénients: Augmente le nombre de classes


4.3 STATE PATTERN

Objectif: Permettre à un objet de modifier son comportement selon son état.

typescript
// State Pattern - Cycle de vie d'un document
interface DocumentState {
  publish(context: Document): void;
  reject(context: Document): void;
}

class DraftState implements DocumentState {
  publish(context: Document): void {
    console.log("📝 → 👀 Document en modération");
    context.setState(new ModerationState());
  }
  reject(context: Document): void {
    console.log("✗ Impossible de rejeter un brouillon");
  }
}

class ModerationState implements DocumentState {
  publish(context: Document): void {
    console.log("👀 → ✓ Document publié");
    context.setState(new PublishedState());
  }
  reject(context: Document): void {
    console.log("✗ Document renvoyé en brouillon");
    context.setState(new DraftState());
  }
}

class PublishedState implements DocumentState {
  publish(context: Document): void {
    console.log("✓ Document déjà publié");
  }
  reject(context: Document): void {
    console.log("✗ Impossible de rejeter un document publié");
  }
}

class Document {
  private state: DocumentState = new DraftState();

  setState(state: DocumentState): void {
    this.state = state;
  }

  publish(): void {
    this.state.publish(this);
  }

  reject(): void {
    this.state.reject(this);
  }
}

// Utilisation
const doc = new Document();
doc.publish(); // En modération
doc.publish(); // Publié
doc.reject();  // Impossible

Cas d'usage: Gestion d'états complexes, machines à état Avantages: Élimination des énormes switch/if Inconvénients: Augmente le nombre de classes


4.4 COMMAND PATTERN

Objectif: Encapsuler une requête comme objet.

typescript
// Command Pattern - Système de contrôle à distance
interface ICommand {
  execute(): void;
  undo(): void;
}

class Light {
  private isOn = false;

  on(): void {
    this.isOn = true;
    console.log("💡 Lumière allumée");
  }

  off(): void {
    this.isOn = false;
    console.log("💡 Lumière éteinte");
  }
}

class TurnOnCommand implements ICommand {
  constructor(private light: Light) {}

  execute(): void {
    this.light.on();
  }

  undo(): void {
    this.light.off();
  }
}

class TurnOffCommand implements ICommand {
  constructor(private light: Light) {}

  execute(): void {
    this.light.off();
  }

  undo(): void {
    this.light.on();
  }
}

class RemoteControl {
  private commandHistory: ICommand[] = [];
  private undoHistory: ICommand[] = [];

  executeCommand(command: ICommand): void {
    command.execute();
    this.commandHistory.push(command);
  }

  undo(): void {
    const command = this.commandHistory.pop();
    if (command) {
      command.undo();
      this.undoHistory.push(command);
    }
  }

  redo(): void {
    const command = this.undoHistory.pop();
    if (command) {
      command.execute();
      this.commandHistory.push(command);
    }
  }
}

// Utilisation
const light = new Light();
const remote = new RemoteControl();

remote.executeCommand(new TurnOnCommand(light));
remote.executeCommand(new TurnOffCommand(light));
remote.undo(); // Remet lumière allumée
remote.redo(); // Remet lumière éteinte

Cas d'usage: Undo/Redo, files d'attente de commandes Avantages: Sépare l'invocation de l'exécution Inconvénients: Peut créer trop de classes


4.5 ITERATOR PATTERN

Objectif: Accéder aux éléments d'une collection séquentiellement.

typescript
// Iterator Pattern - Collection personnalisée
interface IIterator<T> {
  hasNext(): boolean;
  next(): T;
}

interface IIterable<T> {
  [Symbol.iterator](): IterableIterator<T>;
}

class LinkedList<T> implements IIterable<T> {
  private head: Node<T> | null = null;

  add(value: T): void {
    const newNode = new Node(value);
    if (!this.head) {
      this.head = newNode;
    } else {
      let current = this.head;
      while (current.next) {
        current = current.next;
      }
      current.next = newNode;
    }
  }

  *[Symbol.iterator](): IterableIterator<T> {
    let current = this.head;
    while (current) {
      yield current.value;
      current = current.next;
    }
  }
}

class Node<T> {
  next: Node<T> | null = null;
  constructor(public value: T) {}
}

// Utilisation
const list = new LinkedList<string>();
list.add("Alice");
list.add("Bob");
list.add("Charlie");

for (const name of list) {
  console.log(name);
}

Cas d'usage: Parcours de collections Avantages: Abstraction du parcours, réutilisabilité Inconvénients: Peut être excessif pour des cas simples


4.6 CHAIN OF RESPONSIBILITY PATTERN

Objectif: Passer une requête le long d'une chaîne de gestionnaires.

typescript
// Chain of Responsibility - Système d'approbation de congés
interface LeaveRequest {
  days: number;
  reason: string;
}

interface Handler {
  setNext(handler: Handler): Handler;
  handle(request: LeaveRequest): void;
}

abstract class Manager implements Handler {
  protected next: Handler | null = null;
  protected maxDays: number = 0;
  protected title: string = "";

  setNext(handler: Handler): Handler {
    this.next = handler;
    return handler;
  }

  handle(request: LeaveRequest): void {
    if (request.days <= this.maxDays) {
      console.log(`✓ ${this.title} approuve ${request.days} jours de congé`);
    } else if (this.next) {
      this.next.handle(request);
    } else {
      console.log(`✗ Aucun manager ne peut approuver ${request.days} jours`);
    }
  }
}

class TeamLeader extends Manager {
  constructor() {
    super();
    this.maxDays = 3;
    this.title = "Chef d'équipe";
  }
}

class Manager1 extends Manager {
  constructor() {
    super();
    this.maxDays = 5;
    this.title = "Manager";
  }
}

class Director extends Manager {
  constructor() {
    super();
    this.maxDays = 10;
    this.title = "Directeur";
  }
}

// Utilisation
const teamLeader = new TeamLeader();
const manager = new Manager1();
const director = new Director();

teamLeader.setNext(manager).setNext(director);

teamLeader.handle({ days: 2, reason: "Maladie" });
teamLeader.handle({ days: 4, reason: "Vacances" });
teamLeader.handle({ days: 8, reason: "Sabbatique" });

Cas d'usage: Middleware, pipelines de traitement Avantages: Flexibilité, extensibilité Inconvénients: Demande peut ne jamais être traitée


4.7 TEMPLATE METHOD PATTERN

Objectif: Définir le squelette d'un algorithme dans une classe de base.

typescript
// Template Method - Pipeline de traitement de données
abstract class DataProcessor {
  process(data: string): void {
    console.log("🔄 Début du traitement");
    const parsed = this.parse(data);
    const validated = this.validate(parsed);
    if (validated) {
      this.save(parsed);
      console.log("✓ Traitement réussi");
    } else {
      console.log("✗ Validation échouée");
    }
  }

  protected abstract parse(data: string): any;
  protected abstract validate(data: any): boolean;

  protected save(data: any): void {
    console.log("💾 Données sauvegardées");
  }
}

class JSONProcessor extends DataProcessor {
  protected parse(data: string): any {
    console.log("📝 Parsing JSON");
    return JSON.parse(data);
  }

  protected validate(data: any): boolean {
    console.log("✓ Validation JSON");
    return data !== null && typeof data === "object";
  }
}

class XMLProcessor extends DataProcessor {
  protected parse(data: string): any {
    console.log("📝 Parsing XML");
    return { content: data };
  }

  protected validate(data: any): boolean {
    console.log("✓ Validation XML");
    return true;
  }
}

// Utilisation
const jsonProcessor = new JSONProcessor();
jsonProcessor.process('{"name": "John", "age": 30}');

const xmlProcessor = new XMLProcessor();
xmlProcessor.process("<root><name>John</name></root>");

Cas d'usage: Algorithmes avec étapes communes Avantages: Réduction de duplication, extensibilité Inconvénients: Force une hiérarchie de classes


4.8 MEDIATOR PATTERN

Objectif: Réduire les dépendances chaotiques entre objets.

typescript
// Mediator - Système de chat
interface ChatMediator {
  sendMessage(message: string, sender: Colleague): void;
  addColleague(colleague: Colleague): void;
}

abstract class Colleague {
  protected mediator: ChatMediator;

  constructor(mediator: ChatMediator) {
    this.mediator = mediator;
  }

  send(message: string): void {
    this.mediator.sendMessage(message, this);
  }

  abstract receive(message: string, from: string): void;
}

class ChatUser extends Colleague {
  constructor(private username: string, mediator: ChatMediator) {
    super(mediator);
  }

  receive(message: string, from: string): void {
    console.log(`${this.username} reçoit: "${message}" de ${from}`);
  }
}

class ChatRoom implements ChatMediator {
  private users: Map<string, ChatUser> = new Map();

  addColleague(colleague: Colleague): void {
    // Type assertion for demonstration
    const user = colleague as ChatUser;
    this.users.set(user.toString(), user);
  }

  sendMessage(message: string, sender: Colleague): void {
    this.users.forEach((user) => {
      if (user !== sender) {
        user.receive(message, sender.constructor.name);
      }
    });
  }
}

// Utilisation
const chatRoom = new ChatRoom();
const alice = new ChatUser("Alice", chatRoom);
const bob = new ChatUser("Bob", chatRoom);

chatRoom.addColleague(alice);
chatRoom.addColleague(bob);

alice.send("Bonjour Bob!");
bob.send("Salut Alice!");

Cas d'usage: Communication centralisée Avantages: Réduit les dépendances Inconvénients: Mediator peut devenir complexe


4.9 MEMENTO PATTERN

Objectif: Capturer et restaurer l'état d'un objet.

typescript
// Memento - Système de sauvegarde de jeu
class GameState {
  constructor(
    public score: number,
    public level: number,
    public inventory: string[]
  ) {}
}

class GameMemento {
  private state: GameState;

  constructor(state: GameState) {
    this.state = new GameState(state.score, state.level, [...state.inventory]);
  }

  getState(): GameState {
    return this.state;
  }
}

class Game {
  private state: GameState;

  constructor() {
    this.state = new GameState(0, 1, []);
  }

  play(minutes: number): void {
    this.state.score += minutes * 10;
    this.state.inventory.push("item");
    console.log(`🎮 Joué ${minutes}min - Score: ${this.state.score}`);
  }

  saveState(): GameMemento {
    console.log("💾 Jeu sauvegardé");
    return new GameMemento(this.state);
  }

  restoreState(memento: GameMemento): void {
    this.state = memento.getState();
    console.log(`↩️ État restauré - Score: ${this.state.score}`);
  }

  displayState(): void {
    console.log(`Score: ${this.state.score}, Niveau: ${this.state.level}, Items: ${this.state.inventory.length}`);
  }
}

class GameSaveManager {
  private saves: Map<string, GameMemento> = new Map();

  save(slot: string, memento: GameMemento): void {
    this.saves.set(slot, memento);
  }

  load(slot: string): GameMemento | undefined {
    return this.saves.get(slot);
  }
}

// Utilisation
const game = new Game();
const manager = new GameSaveManager();

game.play(5);
manager.save("slot1", game.saveState());

game.play(10);
manager.save("slot2", game.saveState());

const restore = manager.load("slot1");
if (restore) {
  game.restoreState(restore);
}
game.displayState();

Cas d'usage: Sauvegardes de jeux, undo/redo Avantages: Capture complète d'état Inconvénients: Peut consommer beaucoup de mémoire


4.10 VISITOR PATTERN

Objectif: Ajouter des opérations à une structure d'objets.

typescript
// Visitor - Système d'analyse de documents
interface DocumentElement {
  accept(visitor: DocumentVisitor): void;
}

interface DocumentVisitor {
  visitParagraph(element: Paragraph): void;
  visitImage(element: Image): void;
  visitHeading(element: Heading): void;
}

class Paragraph implements DocumentElement {
  constructor(public content: string) {}

  accept(visitor: DocumentVisitor): void {
    visitor.visitParagraph(this);
  }
}

class Image implements DocumentElement {
  constructor(public src: string) {}

  accept(visitor: DocumentVisitor): void {
    visitor.visitImage(this);
  }
}

class Heading implements DocumentElement {
  constructor(public level: number, public text: string) {}

  accept(visitor: DocumentVisitor): void {
    visitor.visitHeading(this);
  }
}

class WordCountVisitor implements DocumentVisitor {
  private wordCount = 0;

  visitParagraph(element: Paragraph): void {
    this.wordCount += element.content.split(" ").length;
  }

  visitImage(element: Image): void {
    // Les images ne contribuent pas au compte de mots
  }

  visitHeading(element: Heading): void {
    this.wordCount += element.text.split(" ").length;
  }

  getWordCount(): number {
    return this.wordCount;
  }
}

// Utilisation
const elements: DocumentElement[] = [
  new Heading(1, "Mon Article"),
  new Paragraph("Ceci est un paragraphe de test."),
  new Image("image.png"),
  new Paragraph("Voici un autre paragraphe.")
];

const visitor = new WordCountVisitor();
elements.forEach(el => el.accept(visitor));
console.log(`Total de mots: ${visitor.getWordCount()}`);

Cas d'usage: Opérations sur structures hiérarchiques Avantages: Séparation des opérations, extensibilité Inconvénients: Difficile d'ajouter de nouveaux types d'éléments


4.11 INTERPRETER PATTERN

Objectif: Représenter une grammaire et interpréter des phrases.

typescript
// Interpreter - Mini-langage de recherche
interface Expression {
  interpret(context: string): boolean;
}

class TerminalExpression implements Expression {
  constructor(private data: string) {}

  interpret(context: string): boolean {
    return context.includes(this.data);
  }
}

class AndExpression implements Expression {
  constructor(private expr1: Expression, private expr2: Expression) {}

  interpret(context: string): boolean {
    return this.expr1.interpret(context) && this.expr2.interpret(context);
  }
}

class OrExpression implements Expression {
  constructor(private expr1: Expression, private expr2: Expression) {}

  interpret(context: string): boolean {
    return this.expr1.interpret(context) || this.expr2.interpret(context);
  }
}

class NotExpression implements Expression {
  constructor(private expr: Expression) {}

  interpret(context: string): boolean {
    return !this.expr.interpret(context);
  }
}

// Utilisation - Recherche: (JavaScript ET Web) OU Python
const js = new TerminalExpression("JavaScript");
const web = new TerminalExpression("Web");
const python = new TerminalExpression("Python");

const query = new OrExpression(
  new AndExpression(js, web),
  python
);

console.log(query.interpret("JavaScript Web Development")); // true
console.log(query.interpret("Python Programming")); // true
console.log(query.interpret("Java Development")); // false

Cas d'usage: Parsers, analyseurs de langage, mini-DSL Avantages: Flexibilité pour créer des langages spécialisés Inconvénients: Peut être inefficace pour des grammaires complexes


5. BONNES PRATIQUES ET CAS D'USAGE

Quand utiliser les patterns?

  1. Ne pas sur-utiliser: Augmente la complexité
  2. Identifier le problème: Avant de choisir un pattern
  3. KISS: Keep It Simple, Stupid
  4. Tests: Les patterns facilitent les tests unitaires

Roadmap d'apprentissage recommandée

Semaine 1: Strategy, Factory, Singleton Semaine 2: Adapter, Decorator, Facade Semaine 3: Observer, Command, Builder Semaine 4: Pratiquer en refactorisantun projet personnel

Anti-patterns à éviter

  • Over-engineering: Trop de patterns pour un problème simple
  • God Object: Une classe qui fait trop
  • Primitive Obsession: Utiliser des types primitifs au lieu de classes
  • Feature Envy: Une classe qui dépend trop d'une autre

SOLID Principles avec Patterns

  • Single Responsibility: Command, Mediator
  • Open/Closed: Factory, Strategy
  • Liskov Substitution: Observer, Decorator
  • Interface Segregation: Adapter, Facade
  • Dependency Inversion: Factory, Dependency Injection

Performance

  • Flyweight: Réduction mémoire
  • Lazy Proxy: Chargement à la demande
  • Object Pool: Réutilisation d'objets
  • Caching: Mémorisation des résultats

CONCLUSION

Les 23 design patterns du Gang of Four constituent un fondement solide pour concevoir des logiciels flexibles, maintenables et scalables. La maîtrise progressive de ces patterns transformera votre approche du design logiciel et améliora significativement la qualité de votre code.

L'apprentissage des patterns c'est:

  • Comprendre les principes, pas mémoriser le code
  • Pratiquer en les implémentant dans de vrais projets
  • Les combiner intelligemment quand nécessaire
  • Toujours garder la simplicité en priorité