Jour 18 Day 18 · mardi 25 août 2026 Tuesday 25 August 2026 Architecture Intermédiaire
SOLID, design patterns & architecture propre SOLID, design patterns & clean architecture
Les 5 principes SOLID avec violations et corrections, les patterns qu'on croise vraiment en entretien, et savoir quand NE PAS abstraire — la question d'architecture arrive dans presque tous les entretiens de stage. The 5 SOLID principles with violations and fixes, the patterns you actually meet in interviews, and knowing when NOT to abstract — the architecture question comes up in almost every internship interview.
L’essentiel
SOLID est un acronyme de cinq principes de conception orientée objet, popularisés par Robert C. Martin. Ce ne sont pas des lois : ce sont des heuristiques pour obtenir du code facile à modifier — le vrai critère de qualité d’un logiciel qui vit.
- S — Single Responsibility : une classe n’a qu’une seule raison de changer. Violation : une classe
Invoicequi calcule le total ET génère le PDF ET envoie l’email. Correction : trois classes (Invoice,InvoicePdfRenderer,InvoiceMailer) — quand le format PDF change, seule une classe bouge. - O — Open/Closed : ouvert à l’extension, fermé à la modification. Violation : un
switch (paymentType)qu’on rallonge à chaque nouveau moyen de paiement. Correction : une interfacePaymentMethodet une classe par moyen de paiement — on ajoute du code, on n’en modifie pas. - L — Liskov Substitution : un sous-type doit être utilisable partout où son parent l’est, sans surprise. Violation classique :
Square extends RectangleoùsetWidthmodifie aussi la hauteur — le code qui manipule unRectanglecasse. Correction : ne pas hériter, ou modéliser autrement (deux types distincts). - I — Interface Segregation : plusieurs petites interfaces spécifiques plutôt qu’une grosse. Violation :
Machineavecprint(),scan(),fax()— l’imprimante bas de gamme doit implémenterfax()en levant une exception. Correction :Printer,Scanner,Faxséparées, chaque classe implémente ce qu’elle sait faire. - D — Dependency Inversion : dépendre d’abstractions, pas d’implémentations concrètes. Violation :
OrderServicequi faitnew MySqlOrderRepository()en dur — impossible à tester sans MySQL. Correction :OrderServicereçoit unOrderRepository(interface) par son constructeur.
Comment ça marche
Le fil rouge des cinq principes : isoler ce qui change de ce qui ne change pas, et faire pointer les dépendances vers le stable. C’est exactement ce que formalise l’architecture hexagonale (ports & adapters) : le domaine métier au centre, sans aucune dépendance technique ; autour, des adapters interchangeables qui parlent au monde extérieur.
┌──────────────────────────────────┐
│ ADAPTERS │
│ HTTP (Express) CLI Tests │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───── ports (interfaces) ────┐ │
│ │ │ │
│ │ DOMAINE MÉTIER │ │
│ │ (règles, entités, use │ │
│ │ cases — zéro framework) │ │
│ │ │ │
│ └───── ports (interfaces) ────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Postgres Stripe SMTP │
└──────────────────────────────────┘
Règle : les flèches pointent vers le centre.
Le domaine définit des ports (interfaces : OrderRepository, PaymentGateway) ; l’infrastructure fournit des adapters qui les implémentent (PostgresOrderRepository, StripeGateway). Changer de base de données ou tester en mémoire = écrire un adapter, sans toucher au métier. C’est le principe D appliqué à l’échelle de l’application.
La dependency injection est le mécanisme concret : au lieu que la classe construise ses dépendances, on les lui fournit de l’extérieur (constructeur, le plus souvent). Un « container DI » (Spring, NestJS) automatise ce câblage, mais le principe tient sans framework.
// Port : le domaine définit ce dont il a besoin, rien de plus
interface OrderRepository {
save(order: Order): Promise<void>;
}
// Use case métier : aucune idée de Postgres, Stripe ou Express
class PlaceOrder {
// la dépendance est INJECTÉE : jamais de `new PostgresRepo()` ici
constructor(private readonly repo: OrderRepository) {}
async execute(order: Order): Promise<void> {
if (order.items.length === 0) throw new Error("Empty order");
await this.repo.save(order); // on parle au port, pas à l'adapter
}
}
// Composition root : le SEUL endroit qui connaît le concret
const placeOrder = new PlaceOrder(new PostgresOrderRepository(pool));
// En test : un faux repo en mémoire, zéro base de données
const testable = new PlaceOrder(new InMemoryOrderRepository());
💡 Le lien à faire — DI (injection, le mécanisme) applique le D de SOLID (inversion, le principe). Citer les deux et les distinguer, c’est exactement le niveau attendu d’un candidat stage.
Concepts clés à maîtriser
Les patterns du GoF qu’on croise vraiment (les 23 par cœur n’intéressent personne) :
| Pattern | Intention | Exemple concret |
|---|---|---|
| Factory | Centraliser la création d’objets | createLogger(env) → console en dev, JSON en prod |
| Strategy | Rendre un algorithme interchangeable | Calcul de frais de port : standard / express / retrait |
| Observer | Notifier des abonnés d’un événement | addEventListener, signaux, event emitters Node |
| Adapter | Faire coïncider deux interfaces | Wrapper Stripe derrière votre port PaymentGateway |
| Singleton | Une instance unique globale | Pool de connexions DB — à manier avec méfiance |
| Dependency injection | Fournir les dépendances de l’extérieur | Constructeurs NestJS/Spring, exemple ci-dessus |
Pourquoi se méfier du singleton : c’est un état global déguisé. Il couple tout le code qui l’appelle, rend les tests interdépendants (l’état fuit d’un test à l’autre) et cache les dépendances (rien dans la signature n’indique que la classe l’utilise). Le besoin légitime (une seule instance d’un pool) se résout mieux en créant l’objet une fois au démarrage et en l’injectant — instance unique, sans accès global.
Et le contrepoids indispensable : YAGNI (You Aren’t Gonna Need It). Une abstraction se paie comptant (indirection, fichiers, charge mentale) pour un bénéfice hypothétique. La bonne heuristique : abstraire à la deuxième ou troisième occurrence réelle, pas à la première intuition — une abstraction prématurée qui s’avère fausse coûte plus cher qu’une duplication temporaire, parce qu’il faut la défaire partout.
⚠️ Sur-ingénierie — une interface avec une seule implémentation « au cas où », une factory pour un seul produit, cinq couches pour un CRUD : c’est du SOLID de cargo cult. En entretien, dire « je n’abstrais qu’à la deuxième implémentation réelle » marque plus de points que réciter les 23 patterns du GoF. Les intervieweurs seniors ont tous été réveillés à 3h du matin par une architecture « propre » illisible.
En entretien
« Expliquez SOLID avec un exemple. » — Dérouler l’acronyme en une phrase chacun, puis approfondir UN principe avec violation + correction. Le plus parlant : S (la classe qui fait tout → découpage par raison de changer) ou D (le new en dur → injection par interface, et enchaîner sur la testabilité).
« C’est quoi l’injection de dépendances, et pourquoi ? » — Fournir les dépendances de l’extérieur (constructeur) au lieu de les construire dedans. Trois bénéfices : testabilité (on injecte un faux), découplage (on dépend d’une interface), flexibilité (on change d’implémentation sans toucher la classe). Bonus : le container DI n’est qu’une automatisation, le principe existe sans lui.
« Quel design pattern avez-vous utilisé récemment ? » — Préparer une histoire vraie. Strategy est le plus facile à raconter : « trois modes de calcul de X, un switch qui grossissait, je l’ai remplacé par une interface et trois implémentations — ajouter un mode = ajouter une classe ». Concret > catalogue.
« Pourquoi dit-on que le singleton est un anti-pattern ? » — État global caché : couplage fort, tests interdépendants, dépendances invisibles dans les signatures. L’alternative : créer une instance unique au démarrage et l’injecter — même garantie, sans les inconvénients.
« C’est quoi l’architecture hexagonale ? » — Domaine métier au centre sans dépendance technique ; il définit des ports (interfaces) ; l’infrastructure fournit des adapters (DB, HTTP, APIs). Les dépendances pointent vers le centre. Bénéfice concret : tester le métier sans DB, changer d’infra sans toucher aux règles.
Pièges & idées reçues
- « Plus il y a de patterns, mieux c’est » — non : un pattern est une solution nommée à un problème récurrent. Sans le problème, le pattern est du bruit. Le code le plus simple qui marche gagne.
- « SOLID impose des interfaces partout » — non : une interface se justifie quand il existe (ou va exister très bientôt) plusieurs implémentations, ou un besoin de substitution en test. Une interface à implémentation unique est une indirection gratuite.
- « L’héritage, c’est de la POO donc c’est bien » — l’héritage est le couplage le plus fort qui existe ; la composition est presque toujours préférable (composition over inheritance). Liskov est précisément le principe qu’on viole en héritant trop vite.
- Confondre le pattern et la bibliothèque —
addEventListenerEST l’observer pattern ; les hooks React s’apparentent à strategy/observer. Savoir nommer les patterns dans les outils qu’on utilise déjà impressionne plus que des UML théoriques. - Appliquer l’hexagonal à un CRUD de 500 lignes — l’architecture propre a un coût d’entrée ; sur un petit projet, un découpage simple en couches (routes / services / repositories) suffit largement.
Pour aller plus loin
- Refactoring.Guru — Design Patterns : le meilleur catalogue illustré, gratuit
- The Clean Architecture — Robert C. Martin : l’article fondateur
- Hexagonal Architecture — Alistair Cockburn : ports & adapters à la source
- Exercice concret : prendre un de vos projets, repérer un
switchqui grossit ou unnewen dur dans un service, et refactorer en strategy ou en injection — c’est l’histoire à raconter en entretien
The essentials
SOLID is an acronym for five object-oriented design principles, popularized by Robert C. Martin. They are not laws: they are heuristics for getting code that is easy to change — the real quality criterion for software that lives on.
- S — Single Responsibility: a class has only one reason to change. Violation: an
Invoiceclass that computes the total AND generates the PDF AND sends the email. Fix: three classes (Invoice,InvoicePdfRenderer,InvoiceMailer) — when the PDF format changes, only one class moves. - O — Open/Closed: open for extension, closed for modification. Violation: a
switch (paymentType)you extend for every new payment method. Fix: aPaymentMethodinterface and one class per payment method — you add code, you don’t modify it. - L — Liskov Substitution: a subtype must be usable anywhere its parent is, without surprises. Classic violation:
Square extends RectanglewheresetWidthalso changes the height — code handling aRectanglebreaks. Fix: don’t inherit, or model differently (two distinct types). - I — Interface Segregation: several small, specific interfaces rather than one big one. Violation:
Machinewithprint(),scan(),fax()— the entry-level printer must implementfax()by throwing. Fix: separatePrinter,Scanner,Fax; each class implements what it can actually do. - D — Dependency Inversion: depend on abstractions, not concrete implementations. Violation:
OrderServicedoing a hardcodednew MySqlOrderRepository()— impossible to test without MySQL. Fix:OrderServicereceives anOrderRepository(interface) through its constructor.
How it works
The common thread of all five principles: isolate what changes from what doesn’t, and point dependencies toward the stable part. That’s exactly what hexagonal architecture (ports & adapters) formalizes: the business domain at the center, with zero technical dependencies; around it, interchangeable adapters that talk to the outside world.
┌──────────────────────────────────┐
│ ADAPTERS │
│ HTTP (Express) CLI Tests │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───── ports (interfaces) ────┐ │
│ │ │ │
│ │ BUSINESS DOMAIN │ │
│ │ (rules, entities, use │ │
│ │ cases — zero framework) │ │
│ │ │ │
│ └───── ports (interfaces) ────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Postgres Stripe SMTP │
└──────────────────────────────────┘
Rule: the arrows point toward the center.
The domain defines ports (interfaces: OrderRepository, PaymentGateway); the infrastructure provides adapters that implement them (PostgresOrderRepository, StripeGateway). Switching databases or testing in memory = writing an adapter, without touching the business logic. It’s the D principle applied at application scale.
Dependency injection is the concrete mechanism: instead of the class building its dependencies, you provide them from the outside (usually via the constructor). A “DI container” (Spring, NestJS) automates the wiring, but the principle stands without any framework.
// Port: the domain declares what it needs, nothing more
interface OrderRepository {
save(order: Order): Promise<void>;
}
// Business use case: no idea Postgres, Stripe or Express exist
class PlaceOrder {
// the dependency is INJECTED: never `new PostgresRepo()` here
constructor(private readonly repo: OrderRepository) {}
async execute(order: Order): Promise<void> {
if (order.items.length === 0) throw new Error("Empty order");
await this.repo.save(order); // we talk to the port, not the adapter
}
}
// Composition root: the ONLY place that knows the concrete types
const placeOrder = new PlaceOrder(new PostgresOrderRepository(pool));
// In tests: a fake in-memory repo, zero database
const testable = new PlaceOrder(new InMemoryOrderRepository());
💡 The connection to make — DI (injection, the mechanism) implements the D of SOLID (inversion, the principle). Naming both and telling them apart is exactly the level expected from an internship candidate.
Key concepts to master
The GoF patterns you actually meet (nobody cares about all 23 by heart):
| Pattern | Intent | Concrete example |
|---|---|---|
| Factory | Centralize object creation | createLogger(env) → console in dev, JSON in prod |
| Strategy | Make an algorithm interchangeable | Shipping cost: standard / express / pickup |
| Observer | Notify subscribers of an event | addEventListener, signals, Node event emitters |
| Adapter | Reconcile two interfaces | Wrapping Stripe behind your PaymentGateway port |
| Singleton | One global instance | DB connection pool — handle with suspicion |
| Dependency injection | Provide dependencies from outside | NestJS/Spring constructors, example above |
Why be wary of the singleton: it’s global state in disguise. It couples every piece of code that calls it, makes tests interdependent (state leaks from one test to the next) and hides dependencies (nothing in the signature says the class uses it). The legitimate need (a single pool instance) is better solved by creating the object once at startup and injecting it — single instance, no global access.
And the essential counterweight: YAGNI (You Aren’t Gonna Need It). An abstraction is paid for upfront (indirection, files, mental load) against a hypothetical benefit. The good heuristic: abstract at the second or third real occurrence, not the first hunch — a premature abstraction that turns out wrong costs more than temporary duplication, because you have to unwind it everywhere.
⚠️ Over-engineering — an interface with a single implementation “just in case”, a factory for one product, five layers for a CRUD: that’s cargo-cult SOLID. In an interview, saying “I only abstract at the second real implementation” scores more points than reciting the 23 GoF patterns. Senior interviewers have all been paged at 3am by an unreadable “clean” architecture.
In an interview
“Explain SOLID with an example.” — Walk through the acronym in one sentence each, then go deep on ONE principle with violation + fix. Most compelling: S (the do-everything class → split by reason to change) or D (the hardcoded new → injection through an interface, then segue into testability).
“What is dependency injection, and why?” — Providing dependencies from the outside (constructor) instead of building them inside. Three benefits: testability (inject a fake), decoupling (depend on an interface), flexibility (swap implementations without touching the class). Bonus: the DI container is just automation, the principle exists without it.
“What design pattern have you used recently?” — Prepare a true story. Strategy is the easiest to tell: “three ways of computing X, a switch that kept growing, I replaced it with an interface and three implementations — adding a mode = adding a class”. Concrete beats catalog.
“Why is the singleton called an anti-pattern?” — Hidden global state: tight coupling, interdependent tests, dependencies invisible in signatures. The alternative: create a single instance at startup and inject it — same guarantee, none of the drawbacks.
“What is hexagonal architecture?” — Business domain at the center with no technical dependency; it defines ports (interfaces); infrastructure provides adapters (DB, HTTP, APIs). Dependencies point toward the center. Concrete benefit: test the business logic without a DB, swap infrastructure without touching the rules.
Pitfalls & misconceptions
- “More patterns = better” — no: a pattern is a named solution to a recurring problem. Without the problem, the pattern is noise. The simplest code that works wins.
- “SOLID means interfaces everywhere” — no: an interface is justified when several implementations exist (or will very soon), or when you need substitution in tests. A single-implementation interface is free indirection.
- “Inheritance is OOP so it’s good” — inheritance is the strongest coupling there is; composition is almost always preferable (composition over inheritance). Liskov is precisely the principle you violate by inheriting too eagerly.
- Confusing the pattern with the library —
addEventListenerIS the observer pattern; React hooks resemble strategy/observer. Naming the patterns inside tools you already use impresses more than theoretical UML. - Applying hexagonal to a 500-line CRUD — clean architecture has an entry cost; on a small project, a simple layered split (routes / services / repositories) is more than enough.
Going further
- Refactoring.Guru — Design Patterns: the best illustrated catalog, free
- The Clean Architecture — Robert C. Martin: the founding article
- Hexagonal Architecture — Alistair Cockburn: ports & adapters at the source
- Concrete exercise: take one of your projects, find a growing
switchor a hardcodednewinside a service, and refactor it into strategy or injection — that’s the story to tell in the interview