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
| Type | Objectif |
|---|---|
| Virtual Proxy | Lazy loading (création différée) |
| Protection Proxy | Contrôle d'accès |
| Cache Proxy | Mise en cache des résultats |
| Remote Proxy | Accès à un objet distant |
| Logging Proxy | Journalisation 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ère | Decorator | Proxy |
|---|---|---|
| Objectif | Ajouter des comportements | Contrôler l'accès |
| Création | Le client compose | Le proxy crée/contrôle le réel |
| Relation | "Enveloppe" l'objet | "Remplace" l'objet pour le client |
| Interface | Identique (composant) | Identique (sujet) |
| Usage | Middleware, logging, validation | Lazy loading, cache, sécurité |
Comment choisir ?
Diagramme en cours de génération...