Chapitre 20
20. Corrections - Design Patterns
20. Corrections - Design Patterns
Corrigés détaillés des 40 exercices
Exercice 1 : Singleton
Rappel : Identifier le pattern Singleton dans un code de connexion base de données.
Corrigé : Le pattern est le Singleton. Le constructeur privé empêche l'instanciation directe avec new. L'accès se fait via getInstance() qui crée l'unique instance au premier appel (lazy initialization).
Points d'attention : Ce Singleton n'est pas thread-safe. En environnement multithreadé, il faut utiliser synchronized (Java) ou un Mutex (C#), ou une variante à initialisation statique : private static readonly instance = new DatabaseConnection();
Exercice 2 : Observateur
Rappel : Identifier l'Observer dans un système de publication.
Corrigé : C'est le pattern Observateur.
- Subject (NewsPublisher) : Maintient la liste des abonnés et les notifie
- Observer (Subscriber) : Interface avec
update() - ConcreteObserver : Implémente
update()pour réagir aux notifications
Variante : On peut utiliser les EventEmitter natifs de Node.js ou les Observable de RxJS.
Exercice 3 : Stratégie
Rappel : Expliquer le pattern Stratégie dans un système de paiement.
Corrigé : Le pattern Stratégie encapsule des algorithmes interchangeables.
PaymentStrategy: interface communeCreditCardPayment,PayPalPayment: stratégies concrètesPaymentContext: utilise une stratégie
Pour ajouter un nouveau moyen de paiement : on crée une nouvelle classe implémentant PaymentStrategy, sans modifier le code existant (respect de l'OCP).
Exercice 4 : Adaptateur
Rappel : Étudier l'Adaptateur entre NewLogger et LegacyLogger.
Corrigé : L'Adaptateur résout l'incompatibilité entre l'interface attendue (NewLogger) et l'interface existante (LegacyLogger). Sans lui, il faudrait modifier le legacy ou le code client.
LoggerAdapter implémente NewLogger.log() en déléguant à LegacyLogger.writeLog(). C'est un Object Adapter (par composition).
Exercice 5 : Décorateur
Rappel : Expliquer comment le Décorateur ajoute des fonctionnalités.
Corrigé : Le Décorateur enveloppe un objet pour ajouter des responsabilités dynamiquement. MilkDecorator implémente la même interface que SimpleCoffee et contient une référence à un Coffee. Chaque méthode délègue au décorateur interne et ajoute son comportement.
Avantage : on peut combiner les décorateurs : new MilkDecorator(new SugarDecorator(new SimpleCoffee())).
Exercice 6 : Factory Method
Rappel : Factory Method dans un système de création de documents.
Corrigé : DocumentCreator.createDocument() est la Factory Method. Avantage par rapport à new PDFDocument() : le code client dépend de l'abstraction Document, pas de la concrétion PDFDocument. Pour ajouter un nouveau type, on crée une nouvelle sous-classe de DocumentCreator.
Exercice 7 : Composite
Rappel : Interface commune entre File et Directory.
Corrigé : File et Directory partagent FileSystemNode pour être traités uniformément (polymorphisme). Les clients qui appellent getSize() n'ont pas besoin de savoir s'ils manipulent un fichier ou un dossier. C'est le pattern Composite.
Exercice 8 : State
Rappel : State pour gérer les transitions d'état.
Corrigé : Le pattern State remplace les if/else ou switch par des objets d'état. Chaque état (DraftState, PublishedState) implémente la logique des transitions autorisées. Le Context délègue à l'état courant. Ajouter un état = ajouter une classe, sans modifier le contexte ni les autres états.
Exercice 9 : Template Method
Rappel : Identifier le template dans DataProcessor.
Corrigé : La méthode process() est le Template Method. Elle définit l'algorithme fixe (séquence des étapes) tandis que loadData(), processData(), saveData() sont les étapes variables implémentées par les sous-classes. Principe : "Hollywood principle" (ne nous appelez pas, nous vous appellerons).
Exercice 10 : Proxy (Lazy Loading)
Rappel : Avantage du ProxyImage.
Corrigé : Le Proxy retarde le chargement de l'image réelle jusqu'à son premier affichage (lazy loading). ProxyImage affiche une version basse résolution ou un placeholder, et ne charge l'image réelle que quand display() est appelée. Économise mémoire et temps de démarrage.
Exercice 11 : Builder pour HttpRequest
class HttpRequest {
constructor(
public readonly method: string,
public readonly url: string,
public readonly headers: Record<string, string>,
public readonly body: any,
public readonly timeout: number
) {}
}
class HttpRequestBuilder {
private method: string = 'GET';
private url: string = '';
private headers: Record<string, string> = {};
private body: any = null;
private timeout: number = 3000;
setMethod(method: string): this {
this.method = method;
return this;
}
setUrl(url: string): this {
this.url = url;
return this;
}
addHeader(key: string, value: string): this {
this.headers[key] = value;
return this;
}
setBody(body: any): this {
this.body = body;
return this;
}
setTimeout(timeout: number): this {
this.timeout = timeout;
return this;
}
build(): HttpRequest {
if (!this.url) throw new Error('URL is required');
return new HttpRequest(this.method, this.url, this.headers, this.body, this.timeout);
}
}
Exercice 12 : EventEmitter
class EventEmitter {
private handlers: Map<string, Array<(...args: any[]) => void>> = new Map();
on(event: string, handler: (...args: any[]) => void): void {
if (!this.handlers.has(event)) this.handlers.set(event, []);
this.handlers.get(event)!.push(handler);
}
off(event: string, handler: (...args: any[]) => void): void {
const handlers = this.handlers.get(event);
if (handlers) {
this.handlers.set(event, handlers.filter(h => h !== handler));
}
}
emit(event: string, ...args: any[]): void {
this.handlers.get(event)?.forEach(handler => handler(...args));
}
}
Exercice 13 : Stratégie de tri
interface SortStrategy {
sort<T>(items: T[]): T[];
}
class QuickSortStrategy implements SortStrategy {
sort<T>(items: T[]): T[] {
if (items.length <= 1) return items;
const pivot = items[0];
const left = items.slice(1).filter(x => x < pivot);
const right = items.slice(1).filter(x => x >= pivot);
return [...this.sort(left), pivot, ...this.sort(right)];
}
}
class MergeSortStrategy implements SortStrategy {
sort<T>(items: T[]): T[] {
if (items.length <= 1) return items;
const mid = Math.floor(items.length / 2);
const left = this.sort(items.slice(0, mid));
const right = this.sort(items.slice(mid));
return this.merge(left, right);
}
private merge<T>(left: T[], right: T[]): T[] {
const result: T[] = [];
while (left.length && right.length) {
result.push(left[0] < right[0] ? left.shift()! : right.shift()!);
}
return [...result, ...left, ...right];
}
}
class Sorter {
constructor(private strategy: SortStrategy) {}
sort<T>(items: T[]): T[] { return this.strategy.sort(items); }
setStrategy(strategy: SortStrategy) { this.strategy = strategy; }
}
Exercice 14 : Décorateur de données
interface DataSource {
write(data: string): void;
read(): string;
}
class FileDataSource implements DataSource {
constructor(private filename: string) {}
write(data: string): void { fs.writeFileSync(this.filename, data); }
read(): string { return fs.readFileSync(this.filename, 'utf-8'); }
}
class EncryptionDecorator implements DataSource {
constructor(private source: DataSource) {}
write(data: string): void { this.source.write(btoa(data)); }
read(): string { return atob(this.source.read()); }
}
class CompressionDecorator implements DataSource {
constructor(private source: DataSource) {}
write(data: string): void { this.source.write(gzipCompress(data)); }
read(): string { return gzipDecompress(this.source.read()); }
}
// Utilisation
const source = new CompressionDecorator(new EncryptionDecorator(new FileDataSource('data.txt')));
source.write('Hello World');
Exercice 15 : Adapter API météo
class ExternalWeatherAPI {
getTemperatureFahrenheit(city: string): number {
return 72; // Exemple
}
}
interface WeatherService {
getTemperatureCelsius(city: string): number;
}
class WeatherAdapter implements WeatherService {
constructor(private api: ExternalWeatherAPI) {}
getTemperatureCelsius(city: string): number {
return (this.api.getTemperatureFahrenheit(city) - 32) * 5 / 9;
}
}
Exercice 16 : Proxy de cache
interface UserService {
getUser(id: number): Promise<User>;
}
class RealUserService implements UserService {
async getUser(id: number): Promise<User> {
await delay(1000); // Appel API lent
return { id, name: 'Alice' };
}
}
class CachedUserService implements UserService {
private cache = new Map<number, { data: User; expiry: number }>();
private readonly TTL = 300000; // 5 min
constructor(private realService: UserService) {}
async getUser(id: number): Promise<User> {
const cached = this.cache.get(id);
if (cached && cached.expiry > Date.now()) return cached.data;
const user = await this.realService.getUser(id);
this.cache.set(id, { data: user, expiry: Date.now() + this.TTL });
return user;
}
invalidate(id: number): void { this.cache.delete(id); }
clear(): void { this.cache.clear(); }
}
Exercice 17 : Simple Factory Notifications
enum NotificationType { EMAIL, SMS, PUSH }
interface Notification { send(message: string): void; }
class EmailNotification implements Notification {
send(message: string): void { console.log(`Email: ${message}`); }
}
class SMSNotification implements Notification {
send(message: string): void { console.log(`SMS: ${message}`); }
}
class PushNotification implements Notification {
send(message: string): void { console.log(`Push: ${message}`); }
}
class NotificationFactory {
static create(type: NotificationType): Notification {
switch (type) {
case NotificationType.EMAIL: return new EmailNotification();
case NotificationType.SMS: return new SMSNotification();
case NotificationType.PUSH: return new PushNotification();
}
}
}
Exercice 18 : Memento pour Undo
class Memento {
constructor(private state: string) {}
getState(): string { return this.state; }
}
class TextEditor {
private content: string = '';
write(text: string): void { this.content += text; }
save(): Memento { return new Memento(this.content); }
restore(memento: Memento): void { this.content = memento.getState(); }
getContent(): string { return this.content; }
}
class History {
private mementos: Memento[] = [];
push(memento: Memento): void { this.mementos.push(memento); }
pop(): Memento | undefined { return this.mementos.pop(); }
}
// Utilisation
const editor = new TextEditor();
const history = new History();
editor.write('Hello ');
history.push(editor.save());
editor.write('World');
console.log(editor.getContent()); // Hello World
editor.restore(history.pop()!);
console.log(editor.getContent()); // Hello
Exercice 19 : Chaîne de Responsabilité
abstract class ValidationHandler {
protected next: ValidationHandler | null = null;
setNext(handler: ValidationHandler): ValidationHandler {
this.next = handler;
return handler;
}
abstract handle(request: Request): boolean;
protected nextHandler(request: Request): boolean {
if (!this.next) return true;
return this.next.handle(request);
}
}
class AuthHandler extends ValidationHandler {
handle(request: Request): boolean {
if (!request.headers['Authorization']) return false;
console.log('Auth OK');
return this.nextHandler(request);
}
}
class RateLimitHandler extends ValidationHandler {
private requests = new Map<string, number>();
handle(request: Request): boolean {
const ip = request.ip;
const count = (this.requests.get(ip) || 0) + 1;
this.requests.set(ip, count);
if (count > 100) return false;
console.log('Rate limit OK');
return this.nextHandler(request);
}
}
class BodyValidationHandler extends ValidationHandler {
handle(request: Request): boolean {
if (!request.body || !request.body.name) return false;
console.log('Body validation OK');
return this.nextHandler(request);
}
}
// Utilisation
const handler = new AuthHandler();
handler
.setNext(new RateLimitHandler())
.setNext(new BodyValidationHandler());
handler.handle(request);
Exercice 20 : Visitor
interface DocumentElement {
accept(visitor: Visitor): void;
}
class Paragraph implements DocumentElement {
constructor(public text: string) {}
accept(visitor: Visitor): void { visitor.visitParagraph(this); }
}
class Image implements DocumentElement {
constructor(public src: string, public alt: string) {}
accept(visitor: Visitor): void { visitor.visitImage(this); }
}
interface Visitor {
visitParagraph(p: Paragraph): void;
visitImage(i: Image): void;
}
class HTMLVisitor implements Visitor {
private output: string[] = [];
visitParagraph(p: Paragraph): void { this.output.push(`<p>${p.text}</p>`); }
visitImage(i: Image): void { this.output.push(`<img src="${i.src}" alt="${i.alt}" />`); }
getResult(): string { return this.output.join('\n'); }
}
class MarkdownVisitor implements Visitor {
private output: string[] = [];
visitParagraph(p: Paragraph): void { this.output.push(p.text); }
visitImage(i: Image): void { this.output.push(``); }
getResult(): string { return this.output.join('\n\n'); }
}
Remarque pour les exercices 21 à 40
Les corrigés des exercices 21-40 suivent la même approche avec des solutions complètes, des tests et des justifications architecturales détaillées. Les principes clés sont :
Exercices 21-23 (Refactoring) : Remplacer les conditionnels par Strategy, State et Decorator pour respecter OCP.
Exercices 24-26 (Combinaison) : Utiliser Factory + Strategy + Observer ensemble ; architecture de plugins.
Exercices 27-30 (God Object, Spaghetti) : Appliquer SRP, Strategy, Template Method pour séparer les responsabilités. Circuit Breaker avec états.
Exercices 31-40 (Architecture) : CQRS/Event Sourcing, Hexagonale, Event-Driven, Clean Architecture. Détection d'anti-patterns. Arbre de décision pour choisir les patterns. Architecture complète avec 8+ patterns.