MFormations
Modern Design Patterns

Chapitre 18

18. Exercices - Design Patterns

18. Exercices - Design Patterns

Exercices de Design Patterns - 40 Énoncés


Niveau Débutant (10 exercices)

Exercice 1 : Identifier le Singleton

class DatabaseConnection {
  private static instance: DatabaseConnection;
  private constructor() {}
  static getInstance(): DatabaseConnection {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection();
    }
    return DatabaseConnection.instance;
  }
}

Question : Quel pattern est implémenté ci-dessus ? Pourquoi utilise-t-on un constructeur privé ?


Exercice 2 : Identifier l'Observateur

class NewsPublisher {
  private subscribers: Subscriber[] = [];
  subscribe(s: Subscriber) { this.subscribers.push(s); }
  unsubscribe(s: Subscriber) { this.subscribers = this.subscribers.filter(x => x !== s); }
  notify(data: string) { this.subscribers.forEach(s => s.update(data)); }
}

Question : Identifiez le pattern. Quels sont les rôles des différentes classes ?


Exercice 3 : Identifier la Stratégie

interface PaymentStrategy {
  pay(amount: number): void;
}
class CreditCardPayment implements PaymentStrategy { /* ... */ }
class PayPalPayment implements PaymentStrategy { /* ... */ }
class PaymentContext {
  constructor(private strategy: PaymentStrategy) {}
  executePayment(amount: number) { this.strategy.pay(amount); }
}

Question : Expliquez le pattern Stratégie. Comment ajouter un nouveau moyen de paiement ?


Exercice 4 : Identifier l'Adaptateur

interface NewLogger { log(message: string): void; }
class LegacyLogger {
  writeLog(text: string): void { /* legacy implementation */ }
}
class LoggerAdapter implements NewLogger {
  constructor(private legacy: LegacyLogger) {}
  log(message: string): void { this.legacy.writeLog(message); }
}

Question : Quel problème résout l'Adaptateur ? Que se passe-t-il sans lui ?


Exercice 5 : Identifier le Décorateur

interface Coffee { cost(): number; description(): string; }
class SimpleCoffee implements Coffee { /* base implementation */ }
class MilkDecorator implements Coffee {
  constructor(private coffee: Coffee) {}
  cost() { return this.coffee.cost() + 1.5; }
}

Question : Comment le Décorateur permet-il d'ajouter des fonctionnalités sans modifier la classe de base ?


Exercice 6 : Identifier le Factory Method

abstract class DocumentCreator {
  abstract createDocument(): Document;
  openDocument() {
    const doc = this.createDocument();
    doc.open();
  }
}
class PdfCreator extends DocumentCreator {
  createDocument(): Document { return new PDFDocument(); }
}

Question : Expliquez le Factory Method. Quel est l'avantage par rapport à new PDFDocument() ?


Exercice 7 : Identifier le Composite

interface FileSystemNode {
  getName(): string;
  getSize(): number;
}
class File implements FileSystemNode { /* ... */ }
class Directory implements FileSystemNode {
  private children: FileSystemNode[] = [];
  getSize() { return this.children.reduce((sum, c) => sum + c.getSize(), 0); }
}

Question : Pourquoi Directory et File partagent-ils la même interface ?


Exercice 8 : Identifier l'État (State)

interface State {
  handle(context: Context): void;
}
class PublishedState implements State {
  handle(context: Context) { /* allow publishing */ }
}
class DraftState implements State {
  handle(context: Context) { /* allow editing */ }
}

Question : Comment le pattern State simplifie-t-il la gestion des transitions d'état ?


Exercice 9 : Identifier le Template Method

abstract class DataProcessor {
  process(): void {
    this.loadData();
    this.processData();
    this.saveData();
  }
  abstract loadData(): void;
  abstract processData(): void;
  abstract saveData(): void;
}

Question : Quelle méthode est le template ? Qu'est-ce qui est fixe et variable ?


Exercice 10 : Identifier le Proxy

interface Image {
  display(): void;
}
class RealImage implements Image {
  constructor(private filename: string) { this.loadFromDisk(); }
  display() { /* show image */ }
}
class ProxyImage implements Image {
  private realImage: RealImage | null = null;
  display() {
    if (!this.realImage) this.realImage = new RealImage(this.filename);
    this.realImage.display();
  }
}

Question : Quel est l'avantage du Proxy dans cet exemple (lazy loading) ?


Niveau Intermédiaire (10 exercices)

Exercice 11 : Implémenter un Builder

Créez un Builder pour construire des objets HttpRequest avec : method, url, headers, body, timeout.

// À compléter
class HttpRequest {
  // ...
}
class HttpRequestBuilder {
  // ...
}
// Utilisation :
// new HttpRequestBuilder()
//   .setMethod('POST')
//   .setUrl('/api/users')
//   .addHeader('Content-Type', 'application/json')
//   .setBody({ name: 'Alice' })
//   .setTimeout(5000)
//   .build();

Exercice 12 : Implémenter un Observateur

Créez un système d'events avec EventEmitter qui supporte on, off, emit.

class EventEmitter {
  // Implémentez on(event, handler), off(event, handler), emit(event, data)
}
// Test :
// const emitter = new EventEmitter();
// emitter.on('user:created', (user) => console.log(user));
// emitter.emit('user:created', { id: 1, name: 'Alice' });

Exercice 13 : Implémenter une Stratégie de tri

Créez un système de tri avec différentes stratégies (QuickSort, MergeSort, BubbleSort).

interface SortStrategy {
  sort<T>(items: T[]): T[];
}
class Sorter {
  constructor(private strategy: SortStrategy) {}
  sort<T>(items: T[]): T[] { return this.strategy.sort(items); }
}
// Implémentez QuickSortStrategy, MergeSortStrategy

Exercice 14 : Implémenter un Décorateur de données

Créez un système de chiffrement/décoration de flux de données.

interface DataSource {
  write(data: string): void;
  read(): string;
}
// Implémentez FileDataSource, EncryptionDecorator, CompressionDecorator

Exercice 15 : Implémenter un Adaptateur pour une API

// API externe (incompatible)
class ExternalWeatherAPI {
  getTemperature(city: string): number { /* en Fahrenheit */ }
}
// Interface attendue
interface WeatherService {
  getTemperatureCelsius(city: string): number;
}
// Créez l'Adaptateur

Exercice 16 : Implémenter un Proxy de cache

interface UserService {
  getUser(id: number): Promise<User>;
}
class RealUserService implements UserService {
  async getUser(id: number): Promise<User> {
    // Appel API lent
  }
}
// Implémentez CachedUserService avec TTL

Exercice 17 : Implémenter un Factory Simple

Créez une factory pour des objets Notification (Email, SMS, Push).

enum NotificationType { EMAIL, SMS, PUSH }
interface Notification { send(message: string): void; }
class NotificationFactory {
  static create(type: NotificationType): Notification {
    // À implémenter
  }
}

Exercice 18 : Implémenter un Memento

Créez un système d'undo pour un éditeur de texte.

class TextEditor {
  private content: string = '';
  // Ajoutez save(), restore(memento)
}
class Memento {
  constructor(private state: string) {}
  getState(): string { return this.state; }
}
class History {
  // Pile de mementos
}

Exercice 19 : Implémenter une Chaîne de Responsabilité

Créez un pipeline de validation de requête.

abstract class ValidationHandler {
  protected next: ValidationHandler | null = null;
  setNext(handler: ValidationHandler): ValidationHandler { this.next = handler; return handler; }
  abstract handle(request: Request): boolean;
}
// Implémentez : AuthHandler, RateLimitHandler, BodyValidationHandler

Exercice 20 : Implémenter un Visitor

Créez un visiteur pour exporter des éléments de document en HTML/Markdown.

interface DocumentElement {
  accept(visitor: Visitor): void;
}
class Paragraph implements DocumentElement { /* ... */ }
class Image implements DocumentElement { /* ... */ }
class Table implements DocumentElement { /* ... */ }
interface Visitor {
  visitParagraph(p: Paragraph): void;
  visitImage(i: Image): void;
}
// Implémentez HTMLVisitor, MarkdownVisitor

Niveau Avancé (10 exercices)

Exercice 21 : Refactoring - Remplacer les conditionnels par la Stratégie

// Code à refactorer
class OrderCalculator {
  calculateDiscount(order: Order): number {
    if (order.customer.type === 'VIP') return order.total * 0.2;
    if (order.customer.type === 'LOYAL') return order.total * 0.15;
    if (order.customer.type === 'NEW') return order.total * 0.1;
    if (order.total > 1000) return order.total * 0.05;
    return 0;
  }
}

Refactorez avec le pattern Stratégie.


Exercice 22 : Refactoring - Remplacer les switch par le pattern State

class Document {
  private status: string = 'DRAFT';
  publish(): void {
    switch (this.status) {
      case 'DRAFT': this.status = 'MODERATION'; break;
      case 'MODERATION': this.status = 'PUBLISHED'; break;
      case 'PUBLISHED': throw new Error('Already published');
    }
  }
}

Refactorez avec le pattern State.


Exercice 23 : Refactoring - Extraire un Decorator

class Notifier {
  send(message: string): void {
    const emailService = new EmailService();
    emailService.send(message);
    const smsService = new SMSService();
    smsService.send(message);
    const slackService = new SlackService();
    slackService.send(message);
  }
}

Refactorez pour permettre de n'envoyer que les notifications souhaitées.


Exercice 24 : Combinaison Factory + Stratégie + Observateur

Créez un système de traitement de fichiers où :

  • Factory crée le bon processeur selon le type
  • Stratégie définit l'algorithme de traitement
  • Observateur notifie les étapes du traitement

Exercice 25 : Refactoring - Anti-pattern God Object

class UserManager {
  createUser() { /* ... */ }
  deleteUser() { /* ... */ }
  sendEmail() { /* ... */ }
  generateReport() { /* ... */ }
  validateAddress() { /* ... */ }
  processPayment() { /* ... */ }
  calculateTax() { /* ... */ }
}

Refactorez en séparant les responsabilités avec les patterns appropriés.


Exercice 26 : Architecture - Système de plugins

Concevez un système de plugins en utilisant : Strategy (pour les algorithmes), Factory (pour la création), Observateur (pour les événements).


Exercice 27 : Refactoring - Anti-pattern Spaghetti Code

function processOrder(order: any) {
  if (order.type === 'physical') {
    // 50 lignes de logistique
  } else if (order.type === 'digital') {
    // 30 lignes de téléchargement
  } else if (order.type === 'subscription') {
    // 40 lignes d'abonnement
  }
}

Refactorez avec les patterns Template Method, Strategy, ou State.


Exercice 28 : Combinaison Composite + Visitor

Créez une structure arborescente de composants UI avec Composite, et un Visitor pour le rendu (HTML, texte, JSON).


Exercice 29 : Architecture - Microservices avec Saga

Concevez les interactions entre services pour un système e-commerce en utilisant Saga (orchestration) et Domain Events.


Exercice 30 : Implémentation - Circuit Breaker

Implémentez un Circuit Breaker complet avec les 3 états, metrics, et fallback.


Niveau Expert (10 exercices)

Exercice 31 : Architecture - CQRS + Event Sourcing

Concevez un système de gestion de comptes bancaires avec CQRS et Event Sourcing. Commandes : deposit, withdraw, transfer. Requêtes : getBalance, getTransactions.


Exercice 32 : Architecture Hexagonale

Implémentez une architecture hexagonale (ports/adapters) pour un service de réservation :

  • Core domain : Booking (use cases)
  • Ports : BookingRepository, PaymentGateway, NotificationService
  • Adapters : PostgreSQL adapter, Stripe adapter, Email adapter

Exercice 33 : Détection d'anti-patterns

class Service {
  private static instance: Service;
  public static getInstance() { /* Singleton */ }
  public process(data: any): any {
    this.validate(data);
    this.transform(data);
    this.saveToDatabase(data);
    this.sendNotification(data);
    this.logOperation(data);
    return data;
  }
}

Identifiez TOUS les anti-patterns présents et proposez une correction.


Exercice 34 : Architecture - Event-Driven Microservices

Concevez un système de traitement de commandes avec :

  • Event sourcing pour les commandes
  • CQRS pour la lecture
  • Saga pour les transactions distribuées
  • Outbox pattern pour la fiabilité

Exercice 35 : Refactoring - Legacy Code vers Clean Architecture

// code legacy
function handleRequest(req: any, res: any) {
  const user = db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);
  const html = `<h1>${user.name}</h1><p>${user.email}</p>`;
  fs.writeFileSync(`/tmp/report-${user.id}.html`, html);
  email.send(user.email, 'Report', html);
  res.send(html);
}

Refactorez en Clean Architecture / Hexagonale.


Exercice 36 : Meta-Programming - Générateur de patterns

Créez un générateur de code qui produit le boilerplate pour :

  • Singleton
  • Factory Method
  • Observateur
  • Stratégie À partir d'une configuration YAML/JSON.

Exercice 37 : Pattern Matching - Anti-pattern Detection Tool

Créez un analyseur statique qui détecte les anti-patterns dans du code :

  • God Class (trop de méthodes)
  • Long Method (> 20 lignes)
  • Switch/ifelse abuse
  • Duplicate code

Exercice 38 : Architecture - Design Patterns Decision Tree

Créez un arbre de décision qui guide le choix du pattern selon :

  • Le problème (création, structure, comportement)
  • Les contraintes (performance, flexibilité, couplage)
  • Le contexte (monolithique, microservices, serverless)

Exercice 39 : Réécriture complète - Système de logging

class Logger {
  static log(level: string, msg: string, format: string, destination: string) {
    // 200+ lignes de code avec conditions et formats multiples
  }
}

Réécrivez avec : Stratégie (formats), Décorateur (enrichissement), Chaîne de responsabilité (niveaux), Adaptateur (destinations).


Exercice 40 : Architecture - Design Patterns Cross-Cutting

Concevez l'architecture complète d'une application enterprise avec au moins 8 patterns différents, en justifiant chaque choix. L'application gère : auth, users, produits, commandes, paiements, notifications, rapports.