SOLID
Acronyme de cinq principes de conception orientée objet (Robert C. Martin) pour un code plus facile à faire évoluer.
SRP — Responsabilité unique
Un module ne doit avoir qu’une seule raison de changer.
OCP — Ouvert/fermé
Un module est ouvert à l’extension mais fermé à la modification : on ajoute du code plutôt que de modifier l’existant.
LSP — Substitution de Liskov
Un sous-type doit pouvoir remplacer son type parent sans casser le comportement attendu.
ISP — Ségrégation des interfaces
Mieux vaut plusieurs petites interfaces spécifiques qu’une grosse interface générale.
DIP — Inversion des dépendances
Le code métier dépend d’abstractions, pas des détails techniques (base, API, framework).
Injection de dépendances
Technique qui fournit à un objet ses dépendances de l’extérieur, au lieu de les créer lui-même.
Duck typing
En JavaScript, un objet est accepté s’il possède les méthodes attendues, sans interface déclarée.
Composition
Assembler des comportements en combinant des objets, plutôt qu’en multipliant les niveaux d’héritage.
Sur-découpage
Piège qui consiste à créer trop d’abstractions pour un besoin simple, au détriment de la lisibilité.
Appliquer SOLID en JavaScript, c’est utiliser cinq principes de conception nés en programmation orientée objet pour écrire du code JS plus simple à tester et à faire évoluer. JavaScript n’a ni interfaces ni classes abstraites au sens de Java, mais les idées restent valables : une responsabilité par module, des extensions sans modification, des contrats respectés, des dépendances injectées.
Voici les cinq principes, chacun avec un exemple avant/après en JavaScript moderne, puis ce qui change par rapport à un langage typé.
S — Responsabilité unique (SRP)
Un module ne doit avoir qu’une seule raison de changer. Le symptôme classique : une fonction qui valide, calcule, enregistre et envoie un email.
// Avant : quatre raisons de changer
async function placeOrder(order) {
if (!order.items.length) throw new Error('Commande vide');
const total = order.items.reduce((sum, i) => sum + i.price * i.qty, 0);
await db.insert('orders', { ...order, total });
await mailer.send(order.email, `Merci ! Total : ${total} €`);
}
// Après : chaque fonction a un seul rôle
const validateOrder = (order) => {
if (!order.items.length) throw new Error('Commande vide');
};
const computeTotal = (items) => items.reduce((sum, i) => sum + i.price * i.qty, 0);
async function placeOrder(order, { saveOrder, notifyCustomer }) {
validateOrder(order);
const total = computeTotal(order.items);
await saveOrder({ ...order, total });
await notifyCustomer(order.email, total);
}
computeTotal se teste désormais sans base de données ni serveur mail.
O — Ouvert/fermé (OCP)
Le code doit être ouvert à l’extension et fermé à la modification. En JavaScript, une table d’objets ou de fonctions remplace avantageusement le switch qui grossit à chaque nouveau cas.
Pour un cas Angular complet, voir le pattern Strategy appliqué aux formulaires dynamiques : ajouter un profil utilisateur revient à ajouter une classe, sans modifier le code existant.
// Avant : chaque nouveau mode de livraison modifie la fonction
function shippingCost(mode, weight) {
switch (mode) {
case 'standard': return 4.9;
case 'express': return 9.9 + weight * 0.5;
default: throw new Error(`Mode inconnu : ${mode}`);
}
}
// Après : on ajoute une stratégie sans toucher au reste
const shippingStrategies = {
standard: () => 4.9,
express: (weight) => 9.9 + weight * 0.5,
relay: () => 3.5, // nouveau mode : une ligne ajoutée
};
function shippingCost(mode, weight) {
const strategy = shippingStrategies[mode];
if (!strategy) throw new Error(`Mode inconnu : ${mode}`);
return strategy(weight);
}
C’est le pattern Strategy, sans classe : une simple fonction par stratégie.
L — Substitution de Liskov (LSP)
Un objet doit pouvoir remplacer celui dont il hérite sans casser le code qui l’utilise. Sans compilateur pour vérifier les contrats, la violation passe souvent inaperçue jusqu’à la production.
// Violation : un produit numérique « hérite » d’un produit physique
class Product {
shippingWeight() { return this.weight; }
}
class Ebook extends Product {
shippingWeight() { throw new Error('Un e-book ne se livre pas'); }
}
// Le panier plante dès qu’il contient un e-book
const totalWeight = (cart) => cart.reduce((w, p) => w + p.shippingWeight(), 0);
// Correction : ne promettre que ce qui est vrai pour tous
class Product { constructor(name, price) { this.name = name; this.price = price; } }
class PhysicalProduct extends Product {
constructor(name, price, weight) { super(name, price); this.weight = weight; }
}
class Ebook extends Product {}
const totalWeight = (cart) =>
cart.filter((p) => p instanceof PhysicalProduct).reduce((w, p) => w + p.weight, 0);
I — Ségrégation des interfaces (ISP)
Aucun client ne doit dépendre de méthodes qu’il n’utilise pas. En JavaScript, l’« interface » est la forme de l’objet attendu : gardez-la petite.
// Avant : chaque moyen de paiement doit tout implémenter
const paypal = {
charge(amount) { /* ... */ },
refund(amount) { /* ... */ },
scheduleRecurring() { throw new Error('Non supporté'); },
};
// Après : des capacités séparées, combinées selon le besoin
const canCharge = (impl) => ({ charge: impl.charge });
const canRefund = (impl) => ({ refund: impl.refund });
const paypal = { ...canCharge(paypalApi), ...canRefund(paypalApi) };
const subscriptionProvider = {
...canCharge(stripeApi),
scheduleRecurring: stripeApi.scheduleRecurring,
};
// Le code appelant ne demande que ce dont il a besoin
async function checkout(payment, amount) {
return payment.charge(amount);
}
D — Inversion des dépendances (DIP)
Le code métier doit dépendre d’abstractions, pas de détails techniques. En JavaScript, le plus simple est d’injecter les dépendances en paramètre plutôt que de les importer en dur.
Côté backend, l’inversion des dépendances est le cœur de l’architecture hexagonale avec Spring Boot : le domaine définit ses ports, l’infrastructure les implémente.
// Avant : impossible à tester sans vraie base
import { pool } from './db.js';
export async function getInvoice(id) {
const { rows } = await pool.query('SELECT * FROM invoices WHERE id = $1', [id]);
return rows[0];
}
// Après : la dépendance est fournie de l’extérieur
export const makeInvoiceService = ({ findInvoiceById }) => ({
async getInvoice(id) {
const invoice = await findInvoiceById(id);
if (!invoice) throw new Error(`Facture ${id} introuvable`);
return invoice;
},
});
// En test
const service = makeInvoiceService({ findInvoiceById: async () => null });
Ce qui change en JavaScript par rapport à Java
- Pas d’interfaces natives : le contrat est implicite (duck typing). TypeScript ou JSDoc rendent ces contrats vérifiables.
- Les fonctions suffisent souvent : une fonction injectée remplace une interface à une méthode.
- Les modules ES jouent le rôle de frontières : n’exportez que ce qui fait partie du contrat.
- Composition plutôt qu’héritage : la plupart des violations de Liskov disparaissent quand on combine des objets au lieu d’empiler des classes.
Les pièges à éviter
SOLID n’est pas une liste à cocher. Découper une fonction de dix lignes en cinq modules ou créer une couche d’abstraction pour une seule implémentation rend le code plus difficile à lire, pas plus propre. Appliquez un principe quand le problème qu’il résout apparaît : une fonction qui change pour plusieurs raisons, un switch qui grossit, un test impossible à écrire.
Pour aller plus loin
- La formation Principes SOLID en pratique applique les cinq principes à une boutique e-commerce en Java, avec code avant/après et labs. Les premières leçons sont gratuites.
- Les leçons sur la responsabilité unique et le principe ouvert/fermé détaillent les deux premiers principes.
- Si vous codez avec un assistant IA, la leçon Socle qualité montre comment imposer SOLID au code généré.




