MFormations
Modern Design Patterns

Chapitre 6

Chapitre 06 — Decorator et Proxy

Chapitre 06 — Decorator et Proxy

Cours 06 — Decorator et Proxy

Partie 1 : Decorator

1.1 Définition

Le Decorator permet d'ajouter dynamiquement des responsabilités à un objet en le plaçant dans un objet wrapper qui implémente la même interface.

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

1.2 Problème résolu

  • Ajouter des comportements à un objet sans modifier sa classe
  • Éviter l'explosion de sous-classes (NotificationWithLoggingAndCompression...)
  • Appliquer des combinaisons de comportements à runtime

1.3 Exemple : Système de notification

interface Notifier {
  send(message: string): void;
}

class EmailNotifier implements Notifier {
  send(message: string): void {
    console.log(`Email: ${message}`);
  }
}

class SMSNotifier implements Notifier {
  send(message: string): void {
    console.log(`SMS: ${message}`);
  }
}

// Decorateur de base
abstract class NotifierDecorator implements Notifier {
  constructor(protected wrappee: Notifier) {}

  abstract send(message: string): void;
}

// Decorateurs concrets
class LoggingNotifierDecorator extends NotifierDecorator {
  send(message: string): void {
    console.log(`[LOG] Sending: ${message}`);
    this.wrappee.send(message);
    console.log(`[LOG] Sent: ${message}`);
  }
}

class EncryptedNotifierDecorator extends NotifierDecorator {
  send(message: string): void {
    const encrypted = btoa(message); // Base64
    console.log(`[ENCRYPT] ${message} -> ${encrypted}`);
    this.wrappee.send(encrypted);
  }
}

class CompressedNotifierDecorator extends NotifierDecorator {
  send(message: string): void {
    const compressed = `[compressed:${message.length} chars]`;
    console.log(`[COMPRESS] Original: ${message.length} chars`);
    this.wrappee.send(compressed);
  }
}

// Usage : combinaisons dynamiques
let notifier: Notifier = new EmailNotifier();

// Ajout de logging
notifier = new LoggingNotifierDecorator(notifier);

// Ajout de chiffrement
notifier = new EncryptedNotifierDecorator(notifier);

// Ajout de compression
notifier = new CompressedNotifierDecorator(notifier);

notifier.send("Hello World");
// [COMPRESS] Original: 11 chars
// [ENCRYPT] Hello World -> SGVsbG8gV29ybGQ=
// [LOG] Sending: [compressed:11 chars]
// Email: [compressed:11 chars]
// [LOG] Sent: [compressed:11 chars]

1.4 Exemple : Middleware HTTP

Le pattern Decorator est la base des middleware chains (Express.js, Koa, etc.) :

interface HttpHandler {
  handle(request: any, response: any): void;
}

class BaseHandler implements HttpHandler {
  handle(request: any, response: any): void {
    console.log(`[Base] ${request.method} ${request.url}`);
    response.statusCode = 200;
    response.body = "OK";
  }
}

abstract class Middleware implements HttpHandler {
  constructor(protected next: HttpHandler) {}

  abstract handle(request: any, response: any): void;
}

class CorsMiddleware extends Middleware {
  handle(request: any, response: any): void {
    response.headers = { ...response.headers, "Access-Control-Allow-Origin": "*" };
    console.log("[CORS] Headers added");
    this.next.handle(request, response);
  }
}

class AuthMiddleware extends Middleware {
  handle(request: any, response: any): void {
    if (!request.headers?.authorization) {
      response.statusCode = 401;
      response.body = "Unauthorized";
      return;
    }
    console.log("[AUTH] Validated token");
    this.next.handle(request, response);
  }
}

class LoggerMiddleware extends Middleware {
  handle(request: any, response: any): void {
    const start = Date.now();
    console.log(`[LOG] ${request.method} ${request.url} - start`);
    this.next.handle(request, response);
    console.log(`[LOG] ${request.method} ${request.url} - ${Date.now() - start}ms`);
  }
}

// Usage : chaînage
let handler: HttpHandler = new BaseHandler();
handler = new LoggerMiddleware(handler);
handler = new AuthMiddleware(handler);
handler = new CorsMiddleware(handler);

const req = { method: "GET", url: "/api/users", headers: { authorization: "Bearer token" } };
const res: any = {};
handler.handle(req, res);

Partie 2 : Proxy

2.1 Définition

Le Proxy fournit un substitut ou un placeholder pour un autre objet, afin de contrôler l'accès à cet objet.

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

2.2 Types de Proxy

TypeObjectif
Virtual ProxyLazy loading (création différée)
Protection ProxyContrôle d'accès
Cache ProxyMise en cache des résultats
Remote ProxyAccès à un objet distant
Logging ProxyJournalisation des accès

2.3 Virtual Proxy (Lazy Loading)

interface Image {
  display(): void;
}

class RealImage implements Image {
  constructor(private filename: string) {
    this.loadFromDisk();
  }

  private loadFromDisk(): void {
    console.log(`Loading image from disk: ${this.filename} (expensive)`);
  }

  display(): void {
    console.log(`Displaying: ${this.filename}`);
  }
}

class ImageProxy implements Image {
  private realImage: RealImage | null = null;

  constructor(private filename: string) {}

  display(): void {
    if (!this.realImage) {
      this.realImage = new RealImage(this.filename); // Lazy loading
    }
    this.realImage.display();
  }
}

// Usage
const image = new ImageProxy("photo.jpg"); // Pas de chargement
image.display(); // Chargement + affichage
image.display(); // Affichage seulement (déjà chargé)

2.4 Protection Proxy

interface BankAccount {
  withdraw(amount: number): void;
  getBalance(): number;
}

class RealBankAccount implements BankAccount {
  private balance: number = 1000;

  withdraw(amount: number): void {
    this.balance -= amount;
    console.log(`Withdrawn: $${amount}, Balance: $${this.balance}`);
  }

  getBalance(): number {
    return this.balance;
  }
}

class ProtectionProxy implements BankAccount {
  constructor(
    private realAccount: RealBankAccount,
    private userRole: string
  ) {}

  withdraw(amount: number): void {
    if (this.userRole !== "admin") {
      console.log("Access denied: admin role required");
      return;
    }
    this.realAccount.withdraw(amount);
  }

  getBalance(): number {
    if (this.userRole === "admin" || this.userRole === "user") {
      return this.realAccount.getBalance();
    }
    console.log("Access denied");
    return 0;
  }
}

// Usage
const account = new RealBankAccount();
const adminProxy = new ProtectionProxy(account, "admin");
adminProxy.withdraw(100); // OK

const userProxy = new ProtectionProxy(account, "user");
userProxy.withdraw(100); // Denied

2.5 Cache Proxy

interface DataService {
  fetchData(key: string): any;
}

class RealDataService implements DataService {
  fetchData(key: string): any {
    console.log(`Fetching ${key} from database...`);
    return { key, data: `Data for ${key}`, timestamp: Date.now() };
  }
}

class CacheProxy implements DataService {
  private cache: Map<string, any> = new Map();

  constructor(private realService: RealDataService) {}

  fetchData(key: string): any {
    if (this.cache.has(key)) {
      console.log(`Cache HIT for ${key}`);
      return this.cache.get(key);
    }

    console.log(`Cache MISS for ${key}`);
    const data = this.realService.fetchData(key);
    this.cache.set(key, data);
    return data;
  }

  invalidate(key: string): void {
    this.cache.delete(key);
  }

  clear(): void {
    this.cache.clear();
  }
}

// Usage
const service = new CacheProxy(new RealDataService());
service.fetchData("user:1"); // Miss + DB query
service.fetchData("user:1"); // Hit
service.fetchData("user:2"); // Miss + DB query

Partie 3 : Comparaison Decorator vs Proxy

CritèreDecoratorProxy
ObjectifAjouter des comportementsContrôler l'accès
CréationLe client composeLe proxy crée/contrôle le réel
Relation"Enveloppe" l'objet"Remplace" l'objet pour le client
InterfaceIdentique (composant)Identique (sujet)
UsageMiddleware, logging, validationLazy loading, cache, sécurité

Comment choisir ?

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