Août 2026. Une bonne partie du code qui part en production n'a plus été tapée caractère par caractère. Les agents de développement sont passés de l'autocomplétion à l'écriture de fonctionnalités entières, et les équipes qui les utilisent produisent beaucoup plus de lignes qu'avant.
Entre accepter une suggestion qui compile et savoir si elle tiendra dans six mois, il y a un écart que la revue de code habituelle comble mal. Le code généré est plausible en surface : il tourne, il passe les tests qu'on lui a demandé d'écrire, il ressemble à du code écrit par quelqu'un de compétent.
Ce qui manque, c'est une grille de lecture qui répond à "est-ce que ça tient" plutôt qu'à "est-ce que ça marche". Sans elle, la revue valide ce qui fonctionne aujourd'hui et laisse passer ce qui coûtera cher dans un an.
Les cinq principes SOLID, formulés par Robert C. Martin au tournant des années 2000, sont cette grille. Ils n'ont jamais été aussi utiles que depuis qu'une machine écrit à ta place. On les reprend un par un, avec du code, et avec ce que chacun change quand c'est un agent qui produit.
Le problème : on ajoute beaucoup, on réorganise presque plus
GitClear et GitKraken ont analysé 623 millions de modifications de code réelles entre 2023 et 2026. Deux chiffres de ce rapport résument la période. Les blocs dupliqués sont passés de 40,3 à 73,0 pour un million de lignes modifiées, soit le niveau le plus élevé jamais mesuré. Dans le même temps, la part des modifications qui consistent à reprendre du code existant est tombée de 1,7 % à 0,46 %.
Le rapport mesure aussi une hausse de 47 % des constructions qui masquent les erreurs, du genre try / except Exception: pass. Ces trois signaux décrivent le même régime : on empile, on ne range plus.
Voici mon avis après avoir relu beaucoup de code produit par agent avec des groupes en formation : la vitesse de génération n'est pas le sujet. Ce qui coûte, c'est qu'un agent lit ton code avant d'écrire, et qu'il imite ce qu'il trouve. Une classe de 600 lignes lui apprend à en ajouter 40 de plus. Un switch de quinze branches lui apprend à en écrire une seizième. SOLID sert alors deux fois : comme grille de relecture, et comme forme que l'agent recopiera au tour suivant.
Single Responsibility Principle : une classe, une raison de changer
Le principe de responsabilité unique est le premier qu'on cite et le premier qu'on viole. Sa formulation est trompeusement simple : une classe ne doit avoir qu'une seule raison de changer. Robert Martin précise ensuite que cette raison renvoie à un acteur métier, pas à une fonction technique. Trois équipes qui peuvent demander de modifier la même classe pour des motifs sans rapport, cela fait trois responsabilités.
Regarde ce service de commande en TypeScript. Il valide, il persiste, il envoie un e-mail et il génère une facture.
Avant : quatre raisons de changer
class OrderService {
async place(order: Order): Promise<void> {
if (order.items.length === 0) throw new Error("Panier vide");
if (order.total < 0) throw new Error("Total négatif");
await this.db.query("INSERT INTO orders ...", order);
await this.mailer.send(order.email, "Merci pour ta commande");
await this.pdf.generateInvoice(order);
await this.analytics.track("order_placed", order.id);
}
}
La correction consiste à garder dans le cas d'usage ce qui décide, et à sortir ce qui réagit.
Après : une seule raison de changer
class PlaceOrder {
constructor(
private readonly orders: OrderRepository,
private readonly events: EventBus,
) {}
async execute(order: Order): Promise<OrderId> {
OrderValidator.assertValid(order);
const id = await this.orders.save(order);
this.events.publish(new OrderPlaced(id, order.email));
return id;
}
}
// L'e-mail, la facture et le suivi deviennent des abonnés à OrderPlaced.
// Ajouter un SMS de confirmation ne touche plus PlaceOrder.
Un indicateur simple : si tu peines à nommer une classe avec un nom précis et que tu es tenté par Manager, Handler ou Util, plusieurs responsabilités se sont déjà accumulées.
Avec un agent, SRP a un effet supplémentaire que peu de gens anticipent : il conditionne la qualité du contexte. Demande à Claude Code ou à Cursor d'ajouter une remise sur un fichier de 600 lignes, et il doit ingérer les 600 lignes pour comprendre où intervenir. Sur une classe de 40 lignes qui fait une chose, il lit peu, il modifie peu, et tu relis un diff que tu comprends en une minute. Petits fichiers, petits diffs, revue possible.
Open/Closed Principle : ouvert à l'extension, fermé à la modification
Le principe ouvert/fermé, formulé par Bertrand Meyer et repris par Martin, pose qu'un composant doit pouvoir être étendu sans être modifié. Ajouter une fonctionnalité ne devrait pas obliger à rouvrir du code qui marche.
Le cas d'école reste le calcul de prix avec une cascade de conditions. En PHP :
Avant : chaque nouvelle remise rouvre la méthode
final class PriceCalculator
{
public function total(Order $order): int
{
$total = $order->subtotal();
if ($order->couponType() === 'bienvenue') {
$total -= 500;
} elseif ($order->couponType() === 'blackfriday') {
$total = (int) round($total * 0.70);
} elseif ($order->couponType() === 'etudiant') {
$total = (int) round($total * 0.85);
}
return max(0, $total);
}
}
La solution passe par une abstraction et une collection d'implémentations. En Symfony, l'attribut #[AutoconfigureTag] suffit à ce que le conteneur injecte automatiquement toutes les remises déclarées.
Après : une remise = un fichier, zéro modification
#[AutoconfigureTag('app.discount')]
interface Discount
{
public function supports(Order $order): bool;
public function applyTo(int $amount): int;
}
final class PriceCalculator
{
/** @param iterable<Discount> $discounts */
public function __construct(private iterable $discounts) {}
public function total(Order $order): int
{
$total = $order->subtotal();
foreach ($this->discounts as $discount) {
if ($discount->supports($order)) {
$total = $discount->applyTo($total);
}
}
return max(0, $total);
}
}
C'est le pattern Stratégie, et c'est aussi là que l'écart entre les deux versions se voit le mieux avec un agent. Demande une remise saisonnière sur la première version : tu obtiens une branche elseif de plus, parce que c'est ce que le fichier lui montre. Demande la même chose sur la seconde : tu obtiens une classe SeasonalDiscount dans un nouveau fichier, sans toucher au calculateur.
La différence se lit en revue. Dans un cas tu dois relire une méthode que dix cas de test couvrent déjà, en cherchant si l'ordre des conditions a changé. Dans l'autre tu lis un fichier neuf de vingt lignes.
Liskov Substitution Principle : la substitution sans surprise
Formulé par Barbara Liskov en 1987, ce principe touche à l'héritage. Si B hérite de A, tout code qui utilise A doit pouvoir recevoir un B sans que le comportement global change.
L'exemple du carré et du rectangle est le plus cité, mais celui que tu croiseras en vrai ressemble plutôt à ceci, en Python :
Violation : le sous-type refuse une méthode du contrat
class Storage:
def put(self, key: str, data: bytes) -> None: ...
def delete(self, key: str) -> None: ...
class ArchiveStorage(Storage):
def delete(self, key: str) -> None:
raise NotImplementedError("Archive en écriture seule")
# N'importe quel code qui reçoit un Storage et appelle delete() casse.
def purge_expired(storage: Storage, keys: list[str]) -> None:
for key in keys:
storage.delete(key) # explose avec ArchiveStorage
La correction ne consiste pas à attraper l'exception dans purge_expired. Elle consiste à admettre que le contrat était faux : tous les stockages ne savent pas supprimer.
Correction : deux contrats au lieu d'un
class Storage(Protocol):
def put(self, key: str, data: bytes) -> None: ...
class DeletableStorage(Storage, Protocol):
def delete(self, key: str) -> None: ...
def purge_expired(storage: DeletableStorage, keys: list[str]) -> None:
for key in keys:
storage.delete(key) # ArchiveStorage ne passe même pas le typage
C'est le point où LSP rejoint la mesure du rapport GitClear sur les 47 % d'erreurs masquées en plus. Un agent à qui tu signales que purge_expired plante propose très souvent un try / except NotImplementedError: pass. Le test repasse au vert, la donnée n'est jamais supprimée, et personne ne s'en aperçoit avant l'audit.
Les signaux de violation restent les mêmes qu'il y a vingt ans : des isinstance ou des instanceof dans le code appelant, des méthodes redéfinies qui lèvent des exceptions inattendues, des préconditions renforcées dans les sous-classes. Quand les comportements divergent à ce point, la composition vaut mieux que l'héritage.
Interface Segregation Principle : des interfaces taillées pour leurs clients
ISP part d'un constat simple : une interface trop large force ses implémentations à définir des méthodes dont elles n'ont pas l'usage. Elles écrivent alors du code vide, ou pire, elles lèvent des exceptions, ce qui viole LSP au passage.
// Une interface fourre-tout : le contrôleur de login en utilise une méthode
interface UserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
findAll(): Promise<User[]>;
save(user: User): Promise<void>;
delete(id: string): Promise<void>;
exportCsv(): Promise<Buffer>;
countByCountry(): Promise<Record<string, number>>;
}
// Ce dont le login a besoin, et rien de plus
interface FindsUserByEmail {
findByEmail(email: string): Promise<User | null>;
}
class LoginController {
constructor(private readonly users: FindsUserByEmail) {}
}
Le gain est immédiat au test : un faux objet pour FindsUserByEmail tient en trois lignes, alors qu'un faux UserRepository complet en demande sept. Si tu travailles en TypeScript, cette façon de découper les contrats recoupe ce que raconte notre article sur les erreurs de typage qui révèlent un mauvais design.
ISP a pris une seconde vie en 2026, du côté des agents. Quand tu exposes des outils à un agent, via MCP ou via une définition maison, tu écris une interface. Un agent à qui tu présentes trente outils choisit moins bien qu'un agent à qui tu en présentes six, exactement comme une classe noyée sous une interface fourre-tout. La règle est la même : expose au client ce dont ce client a besoin. Si tu construis des outils pour un modèle, la formation Prompt Engineering et API LLM couvre cette partie conception.
Dependency Inversion Principle : dépendre des abstractions
DIP pose deux règles. Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau, les deux doivent dépendre d'abstractions. Et les abstractions ne doivent pas dépendre des détails, ce sont les détails qui dépendent des abstractions.
L'exemple qui parle le plus en 2026 n'est plus la base de données, c'est le fournisseur de modèle. Voici le code que produisent la plupart des premiers jets, y compris ceux écrits par un agent :
Avant : la logique métier connaît le fournisseur
from anthropic import Anthropic
class TicketSummarizer:
def __init__(self) -> None:
self._client = Anthropic() # instancié en dur
def summarize(self, ticket: str) -> str:
message = self._client.messages.create(
model="claude-opus-5",
max_tokens=400,
messages=[{"role": "user", "content": f"Résume ce ticket :\n{ticket}"}],
)
return "".join(b.text for b in message.content if b.type == "text")
Ce code marche. Il devient pénible le jour où tu veux tester la logique de tri des tickets sans appel réseau, comparer deux modèles sur le même jeu de données, ou basculer sur un modèle local pour les données sensibles. Trois besoins courants, un seul point de blocage : la dépendance concrète au fond du service.
Après : le service dépend d'un contrat
from typing import Protocol
class TextGenerator(Protocol):
def generate(self, prompt: str, max_tokens: int) -> str: ...
class TicketSummarizer:
def __init__(self, generator: TextGenerator) -> None:
self._generator = generator # injecté depuis l'extérieur
def summarize(self, ticket: str) -> str:
return self._generator.generate(f"Résume ce ticket :\n{ticket}", 400)
# Adaptateur : c'est le seul endroit du projet qui importe le SDK
class ClaudeGenerator:
def __init__(self, client: Anthropic, model: str = "claude-opus-5") -> None:
self._client, self._model = client, model
def generate(self, prompt: str, max_tokens: int) -> str:
message = self._client.messages.create(
model=self._model,
max_tokens=max_tokens,
messages=[{"role": "user", "content": prompt}],
)
return "".join(b.text for b in message.content if b.type == "text")
# En test : aucun réseau, aucune clé d'API, exécution instantanée
class FakeGenerator:
def generate(self, prompt: str, max_tokens: int) -> str:
return "résumé factice"
Le SDK n'est importé qu'à un seul endroit. Le jour où une version majeure change la signature de messages.create, tu modifies un fichier au lieu de trente. C'est le même raisonnement que celui du Repository pattern pour l'accès aux données, appliqué à un autre type d'infrastructure.
En Symfony, le conteneur de services est une concrétisation directe de DIP : tu déclares une interface dans le typage du constructeur et le framework résout l'implémentation. C'est aussi pour cette raison que Symfony et Laravel se ressemblent sur ce point, comme on le détaille dans notre comparatif Symfony ou Laravel.
Faire respecter SOLID par un agent
Depuis fin 2025, les outils d'assistance ont convergé sur une même convention : un fichier markdown à la racine du dépôt qui décrit les règles du projet. AGENTS.md est passé sous la garde de l'Agentic AI Foundation, une fondation Linux Foundation, et il est lu nativement par la plupart des agents du marché. Claude Code lit CLAUDE.md, GitHub Copilot lit .github/copilot-instructions.md, Cursor lit .cursor/rules/. Tu peux mettre les règles communes dans AGENTS.md et pointer dessus depuis les autres.
Ce qui fonctionne dans ce fichier, ce sont des règles vérifiables. "Respecte SOLID" ne produit rien. Voici le genre de formulation qui change le résultat :
# Conventions de conception
- Un cas d'usage par classe. Si une classe dépasse 5 dépendances
injectées, découpe-la avant d'ajouter du code.
- Interdiction d'ajouter une branche à un switch ou à une chaîne de if
qui distingue des types métier. Crée une implémentation de
l'interface existante à la place.
- Les services métier ne doivent jamais importer un SDK externe
(base de données, HTTP, fournisseur LLM). Passe par un adaptateur
dans src/Infrastructure/.
- Une méthode d'interface qu'une implémentation ne peut pas honorer
signifie que l'interface est trop large. Découpe-la, ne lève pas
NotImplementedError.
- Jamais de `except Exception: pass` ni de catch vide. Si une erreur
ne peut pas être traitée ici, laisse-la remonter.
Ces règles marchent parce qu'elles sont observables dans un diff. Un agent peut vérifier lui-même s'il vient d'ajouter un elseif, il ne peut pas vérifier s'il "respecte SOLID".
La seconde habitude à prendre, c'est la relecture en deux temps. Première passe : est-ce que ça fait ce que j'ai demandé. Seconde passe, sur le même diff : combien de raisons de changer cette classe a-t-elle maintenant, est-ce que j'ai ouvert un fichier existant alors qu'un nouveau fichier aurait suffi, est-ce qu'une exception vient d'être avalée quelque part. Cette seconde passe prend deux minutes et attrape la quasi-totalité de ce qui pourrit une base de code sur un an. C'est aussi le cœur de la méthode qu'on décrit dans vibe coding sans perdre le contrôle.
Les pièges, y compris ceux que SOLID crée lui-même
Appliqué sans discernement, SOLID produit des architectures sur-découpées où personne ne retrouve rien. Les erreurs que je vois le plus souvent :
- Créer une interface pour une seule implémentation. Attends la deuxième variante. Une interface avec un seul implémenteur ajoute un fichier et zéro souplesse.
- Demander à un agent d'appliquer SOLID sur toute une base de code. Tu obtiens un diff de 4 000 lignes que personne ne relira. Cible un module, vérifie, recommence.
- Confondre découpage et renommage. Sortir trois méthodes dans une classe
OrderHelperne crée pas une responsabilité, cela déplace le désordre. - Simuler tout ce qui bouge. Un test qui configure six doublures ne teste plus le comportement, il teste ta configuration. C'est souvent le signe que DIP a été appliqué à des dépendances qui n'en avaient pas besoin.
- Appliquer SOLID à un script jetable. Un prototype de validation, un script d'import ponctuel ou un notebook d'exploration n'ont rien à y gagner.
La bonne heuristique reste la même depuis vingt ans : n'anticipe pas l'abstraction. Écris l'implémentation simple, et extrais une interface quand la deuxième variante arrive. Les projets qui gagnent le plus à SOLID sont ceux qui durent, avec plusieurs personnes qui interviennent en parallèle et des spécifications qui bougent.
Tableau récapitulatif des cinq principes
| Principe | Ce qu'il prescrit | Signal de violation | Ce que fait l'agent si tu ne dis rien |
|---|---|---|---|
| SRP | Une classe, une raison de changer | Classe large, nommée Manager ou Helper | Il ajoute sa méthode dans la classe existante |
| OCP | Étendre sans modifier l'existant | Longues chaînes de conditions sur un type | Il rallonge la chaîne de conditions |
| LSP | Les sous-types sont substituables | instanceof, exceptions inattendues | Il attrape l'exception et la fait taire |
| ISP | Interfaces ciblées sur leur client | Méthodes vides ou non supportées | Il implémente des méthodes qui renvoient null |
| DIP | Dépendre des abstractions | Instanciation directe d'une classe concrète | Il importe le SDK dans la couche métier |
SOLID et les autres principes de conception
SOLID ne fonctionne pas en vase clos. Le principe DRY (Don't Repeat Yourself) complète SRP : deux responsabilités distinctes ne doivent pas non plus partager du code dupliqué. Et YAGNI (You Aren't Gonna Need It) tempère SOLID en rappelant qu'une abstraction créée trop tôt est de la complexité offerte. Ces principes sont replacés les uns par rapport aux autres dans notre panorama des 20 principes de code.
Les patterns du Gang of Four sont les outils qui mettent SOLID en œuvre. Strategy répond à OCP, Repository répond à DIP, Decorator permet d'étendre un comportement sans toucher aux classes de base. Ce sont des solutions connues à des problèmes récurrents, à choisir quand le besoin apparaît.
À plus grande échelle, SOLID prépare le terrain pour l'architecture hexagonale et le Domain-Driven Design, qui séparent strictement la logique métier de l'infrastructure. DIP en est le mécanisme central. Il se combine bien avec les 12 facteurs, qui traitent la même séparation côté déploiement et configuration.
Ce qu'il faut retenir
Les cinq principes sont des outils de diagnostic autant que de conception. Ils donnent un nom à des choses qu'on ressent avant de savoir les formuler : ce module est trop dur à tester, cette classe change trop souvent pour de mauvaises raisons, cet héritage produit des comportements étranges.
Ce qui a changé en 2026, c'est le volume de code qu'ils doivent filtrer. Un agent produit en une heure ce qu'une équipe produisait en une journée, et il produit dans le style de ce qu'il lit. La forme de ta base de code est devenue une instruction permanente donnée à la machine.
Si tu ne dois retenir qu'un geste : ouvre ton dépôt, cherche la classe la plus longue, et compte ses raisons de changer. Ce chiffre te dira où en est ton projet mieux que n'importe quel tableau de bord. Si tu encadres une équipe qui travaille avec ces outils, notre article sur former des développeurs quand l'IA est déjà dans la salle prolonge le sujet, et la formation PHP orienté objet travaille ces principes sur un projet complet.
Questions fréquentes sur les principes SOLID
SOLID a-t-il encore du sens si c'est une IA qui écrit le code ?
Davantage qu'avant. Un agent génère du code qui compile et qui passe les tests, ce qui rend le critère "est-ce que ça marche" beaucoup moins discriminant qu'à l'époque où tout était écrit à la main. SOLID fournit le second critère, celui de la tenue dans le temps. Il joue aussi un rôle en amont : l'agent lit ta base de code avant d'écrire et reproduit ses formes, donc une base bien structurée oriente ce qu'il produit sans que tu aies besoin de le répéter à chaque requête.
Comment faire appliquer SOLID par Claude Code, Cursor ou Copilot ?
Par un fichier de conventions à la racine du dépôt. AGENTS.md est le format le plus largement lu, placé sous la garde de l'Agentic AI Foundation, une fondation Linux Foundation ; Claude Code utilise CLAUDE.md, Copilot .github/copilot-instructions.md, Cursor .cursor/rules. Écris-y des règles vérifiables dans un diff plutôt que des intentions. "Ne rallonge pas une chaîne de conditions qui distingue des types métier, crée une implémentation de l'interface existante" fonctionne. "Respecte SOLID" ne produit rien d'observable.
Par quel principe commencer quand on débute ?
Par SRP, puis DIP. SRP donne le réflexe de découper avant que le fichier devienne illisible, et il ne demande aucune notion d'abstraction avancée. DIP vient ensuite parce qu'il débloque les tests : tant qu'un service instancie lui-même sa base de données ou son client HTTP, tu ne peux pas le tester en isolation. OCP et LSP se comprennent mieux une fois que ces deux-là sont acquis, et ISP arrive naturellement quand tu commences à écrire des interfaces.
SOLID s'applique-t-il en dehors de la programmation orientée objet ?
Les formulations d'origine supposent des classes et de l'héritage, mais plusieurs principes ont des équivalents ailleurs. SRP se traduit par des fonctions qui font une seule chose. DIP se traduit par des dépendances passées en paramètre plutôt qu'importées en dur, ce qui est le pain quotidien de la programmation fonctionnelle. ISP se traduit par des signatures de fonctions qui ne reçoivent que ce dont elles ont besoin. LSP est celui qui perd le plus de sens hors héritage.
Comment repérer une violation sans expertise avancée ?
Les tests sont le meilleur révélateur. Un test unitaire qui exige de configurer cinq doublures pour vérifier une seule méthode pointe vers un problème de DIP ou de SRP. Une classe difficile à nommer sans utiliser Manager ou Helper signale une accumulation de responsabilités. Une méthode héritée qui doit vérifier le type réel de l'objet avant d'agir indique une violation de LSP. Un code qui se teste difficilement en isolation viole presque toujours un principe SOLID au moins.
Faut-il connaître SOLID pour utiliser Symfony ou Laravel ?
Tu peux les utiliser sans, mais les connaître change ta façon de les lire. Symfony est architecturé autour de DIP avec son conteneur de services, autour d'OCP avec les EventListeners et les CompilerPass, autour d'ISP avec ses interfaces de contrats. Laravel s'appuie sur des mécanismes proches via ses Service Providers. Comprendre SOLID donne du sens à ces choix de conception et évite de les contourner sans le vouloir.