Les plans du bon logiciel : les design patterns expliqués
Les mêmes problèmes apparaissent dans presque tous les projets logiciels — initialisation, gestion des dépendances, changement de comportement. Les design patterns sont les solutions éprouvées. Voici comment ils ressemblent dans du code réel, pas dans des diagrammes de manuel.
Les design patterns sont des solutions typiques aux problèmes courants en conception logicielle. Ce sont comme des plans que vous pouvez personnaliser pour résoudre un problème de conception récurrent dans votre code. Dans ce guide, nous explorons les trois catégories principales de patterns — Créationnels, Structurels et Comportementaux — avec des exemples TypeScript pratiques adaptés à l'écosystème NestJS et React moderne.
1. Patterns Créationnels
Ces patterns fournissent des mécanismes de création d'objets qui augmentent la flexibilité et la réutilisation du code existant.
Singleton
Intention : S'assurer qu'une classe n'a qu'une seule instance, tout en fournissant un point d'accès global à cette instance.
Contexte NestJS : Dans NestJS, presque chaque provider (Service, Repository) est un Singleton par défaut. Le conteneur d'Injection de Dépendances (DI) gère l'instance unique pour vous, donc vous avez rarement besoin d'implémenter le pattern manuellement.
// La Méthode Manuelle (Singleton Classique)
class DatabaseConnection {
private static instance: DatabaseConnection;
private constructor() {} // Private empêche 'new'
public static getInstance(): DatabaseConnection {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new DatabaseConnection();
}
return DatabaseConnection.instance;
}
}
// La Méthode NestJS (Meilleure)
@Injectable() // Le conteneur gère le cycle de vie du singleton
export class DatabaseService {
constructor(private readonly config: ConfigService) {}
}
Méthode Fabrique (Factory Method)
Intention : Définir une interface pour créer un objet, mais laisser les sous-classes décider quelle classe instancier.
Cas d'Usage : Créer différents processeurs de paiement (Stripe, PayPal) selon la configuration de l'utilisateur.
abstract class PaymentAdapter {
abstract process(amount: number): Promise;
}
class StripeAdapter extends PaymentAdapter {
async process(amount: number) { /* ... */ }
}
class PayPalAdapter extends PaymentAdapter {
async process(amount: number) { /* ... */ }
}
class PaymentFactory {
static create(provider: 'stripe' | 'paypal'): PaymentAdapter {
if (provider === 'stripe') return new StripeAdapter();
if (provider === 'paypal') return new PayPalAdapter();
throw new Error('Unknown provider');
}
}
Constructeur (Builder)
Intention : Construire des objets complexes étape par étape. En TypeScript, nous pouvons souvent y parvenir proprement avec des littéraux d'objets ou des partials, mais le pattern builder fluent est utile pour les validations complexes.
// Approche Builder Fonctionnel
class HttpRequestBuilder {
private config: Partial = { method: 'GET' };
setUrl(url: string) {
this.config.url = url;
return this;
}
setMethod(method: 'GET' | 'POST') {
this.config.method = method;
return this;
}
setBody(data: unknown) {
this.config.body = JSON.stringify(data);
return this;
}
build() {
if (!this.config.url) throw new Error('URL is required');
return this.config as RequestConfig;
}
}
// Utilisation
const req = new HttpRequestBuilder()
.setUrl('https://api.example.com')
.setMethod('POST')
.setBody({ id: 1 })
.build();
2. Patterns Structurels
Ces patterns expliquent comment assembler des objets et des classes en structures plus larges tout en gardant ces structures flexibles et efficaces.
Adaptateur (Adapter)
Intention : Permet à des objets avec des interfaces incompatibles de collaborer. Essentiel lors de l'intégration de code hérité ou de bibliothèques tierces.
Client ──▶ Interface Cible ◀── Adaptateur ──▶ Adapté (3rd Party)
(processPayment) (implémente (makeCharge)
processPayment)
// Bibliothèque Externe (Interface Incompatible)
class OldLogger {
public writeLog(msg: string, severity: number) { /* ... */ }
}
// Interface de Notre Application
interface ILogger {
info(message: string): void;
error(message: string): void;
}
// Adaptateur
class LoggerAdapter implements ILogger {
constructor(private oldLogger: OldLogger) {}
info(message: string) {
this.oldLogger.writeLog(message, 1);
}
error(message: string) {
this.oldLogger.writeLog(message, 2);
}
}
Décorateur (Decorator)
Intention : Attacher de nouveaux comportements à des objets en plaçant ces objets dans des objets enveloppeurs spéciaux qui contiennent les comportements. En TypeScript/NestJS, c'est une fonctionnalité centrale du langage.
Cas d'Usage : Journalisation du temps d'exécution, validation, définitions d'endpoints API.
// Décorateur personnalisé pour mesurer le temps d'exécution
function LogExecutionTime() {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const start = performance.now();
const result = await originalMethod.apply(this, args);
const end = performance.now();
console.log(`${propertyKey} took ${(end - start).toFixed(2)}ms`);
return result;
};
};
}
class UserService {
@LogExecutionTime()
async findUser(id: string) {
// Appel base de données...
return { id, name: 'John' };
}
}
Façade (Facade)
Intention : Fournit une interface simplifiée à une bibliothèque, un framework ou tout autre ensemble complexe de classes.
┌─────────────┐
│ Façade │ (Interface Simple)
└──────┬──────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Décodeur│ │ Décodeur│ │ Service │
│ Vidéo │ │ Audio │ │ Sous- │
│ │ │ │ │ titres │
└─────────┘ └─────────┘ └─────────┘
class VideoConversionFacade {
convert(file: string, format: string) {
const video = new VideoFile(file);
const audio = new AudioExtractor(video).extract();
const result = new Muxer().mux(video, audio, format);
return result;
}
}
3. Patterns Comportementaux
Ces patterns concernent les algorithmes et la répartition des responsabilités entre les objets.
Observateur (Observer)
Intention : Vous permet de définir un mécanisme d'abonnement pour notifier plusieurs objets de tout événement survenant à l'objet qu'ils observent.
Contexte Moderne : Dans l'écosystème Angular/NestJS, RxJS est l'implémentation standard du pattern Observateur.
import { Subject } from 'rxjs';
// Le Subject est à la fois un Observable et un Observer
const newsFeed$ = new Subject();
// Abonné 1 (Observer)
newsFeed$.subscribe(news => console.log(`Consumer A received: ${news}`));
// Abonné 2 (Observer)
newsFeed$.subscribe(news => console.log(`Consumer B received: ${news}`));
// Émission d'un événement
newsFeed$.next('Breaking News: TypeScript 6.0 Released!');
// Les deux consommateurs journalisent le message immédiatement
Stratégie (Strategy)
Intention : Vous permet de définir une famille d'algorithmes, de les encapsuler chacun dans une classe séparée, et de rendre leurs objets interchangeables.
Contexte NestJS : L'intégration de Passport.js dans NestJS est une implémentation exemplaire du pattern Stratégie. Vous basculez entre les stratégies 'jwt', 'local' ou 'google' sans modifier la logique du contrôleur d'authentification.
// L'Interface Stratégie est effectivement définie par Passport
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
// Contexte : AuthGuard utilise la stratégie configurée
@UseGuards(AuthGuard('jwt'))
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
Conclusion
Comprendre ces patterns vous transforme d'un développeur qui écrit de la syntaxe en un ingénieur logiciel qui conçoit des systèmes. En tirant parti des patterns intégrés de NestJS (Services Singleton, Décorateurs, Stratégies) et des fonctionnalités modernes de TypeScript, vous pouvez implémenter ces concepts intemporels avec un minimum de code répétitif.
Écrit par

Tech Lead et Ingénieur Full Stack pilotant une équipe de 5 ingénieurs chez Fygurs (Paris, Remote) sur un SaaS cloud-native Azure. Diplômé de 1337 Coding School (42 Network / UM6P). Écrit sur l'architecture, l'infrastructure cloud et le leadership technique.