S — Responsabilité unique : le service de commande
Découper un service de commande e-commerce qui vérifie le stock, calcule la TVA, sauvegarde, facture et envoie l'e-mail en classes à responsabilité unique.
Objectif de la leçon
Découper un service de commande « couteau suisse » en classes qui n’ont chacune qu’une seule raison de changer.
La règle
Une classe ne doit avoir qu’une seule raison de changer. Autrement dit : un seul acteur métier doit pouvoir demander sa modification.
L’analogie : dans un entrepôt, le préparateur prépare le colis, la comptable édite la facture, le service client envoie l’e-mail. Si une seule personne faisait les trois, chaque changement de procédure la dérangerait.
Sans pattern : le service de commande qui fait tout
public class CommandeService {
private final JdbcTemplate jdbc; // dépendances techniques injectées
private final Mailer mailer; // (constructeur omis)
public void passerCommande(Client client, Panier panier) {
// 1. Stock
for (Article a : panier.articles()) {
int stock = jdbc.queryForObject(
"SELECT stock FROM produit WHERE sku = ?", Integer.class, a.sku());
if (stock < a.quantite()) {
throw new IllegalStateException("Rupture de stock : " + a.nom());
}
}
// 2. Calcul du total TTC
double totalTTC = panier.totalHT() * 1.20;
// 3. Sauvegarde
jdbc.update("INSERT INTO commande(client_id, total) VALUES (?, ?)",
client.id(), totalTTC);
// 4. Facture PDF
PdfDocument pdf = new PdfDocument();
pdf.addLine("Facture ShopFacile - total : " + totalTTC + " €");
pdf.save("/factures/" + client.id() + ".pdf");
// 5. E-mail
mailer.send(client.email(), "Merci pour votre commande de " + totalTTC + " €");
}
}
Le problème
Cette classe a cinq raisons de changer, demandées par cinq équipes différentes :
| Demande | Qui la fait | Impact |
|---|---|---|
| « On gère le stock réservé pendant 15 minutes » | Logistique | On modifie CommandeService |
| « Les livres passent en TVA à 5,5 % » | Comptabilité | On modifie CommandeService |
| « On passe de JDBC à JPA » | Architecture | On modifie CommandeService |
| « La facture doit afficher le n° de TVA » | Comptabilité | On modifie CommandeService |
| « On envoie aussi un SMS » | Marketing | On modifie CommandeService |
Résultat : conflits Git permanents, une régression sur l’e-mail quand on touche à la TVA, et impossible de tester le calcul du total sans base de données ni génération de PDF.
Avec pattern : une classe = un métier
Pattern utilisé : découpage par responsabilité (Service orchestrateur + composants métier + Repository + Notifier). C’est la base de toute architecture en couches.
// Logistique
public class VerificateurStock {
private final StockRepository stocks;
public VerificateurStock(StockRepository stocks) { this.stocks = stocks; }
public void verifier(Panier panier) {
for (Article a : panier.articles()) {
if (stocks.disponible(a.sku()) < a.quantite()) {
throw new RuptureDeStockException(a.nom());
}
}
}
}
// Comptabilité : le calcul
public class CalculateurTotal {
private static final double TVA = 0.20;
public double totalTTC(Panier panier) {
return panier.totalHT() * (1 + TVA);
}
}
// Comptabilité : le document
public class GenerateurFacture {
public Facture generer(Commande commande) { /* PDF */ return null; }
}
// Persistance
public interface CommandeRepository {
Commande sauvegarder(Commande commande);
}
// Communication client
public class NotificationClient {
public void confirmerCommande(Client client, Commande commande) { /* e-mail */ }
}
// Le chef d'orchestre : il coordonne, il ne calcule rien lui-même
public class CommandeService {
private final VerificateurStock stock;
private final CalculateurTotal calculateur;
private final CommandeRepository repository;
private final GenerateurFacture facturation;
private final NotificationClient notification;
public CommandeService(VerificateurStock stock, CalculateurTotal calculateur,
CommandeRepository repository, GenerateurFacture facturation,
NotificationClient notification) {
this.stock = stock;
this.calculateur = calculateur;
this.repository = repository;
this.facturation = facturation;
this.notification = notification;
}
public Commande passerCommande(Client client, Panier panier) {
stock.verifier(panier);
Commande commande = repository.sauvegarder(
new Commande(client, panier, calculateur.totalTTC(panier)));
facturation.generer(commande);
notification.confirmerCommande(client, commande);
return commande;
}
}
Le test du calcul devient trivial :
@Test
void total_ttc_applique_la_tva_a_20_pourcents() {
Panier panier = new Panier();
panier.ajouter(new Article("TSHIRT-01", "T-shirt", 25.0, 2));
assertEquals(60.0, new CalculateurTotal().totalTTC(panier), 0.001);
}
Le schéma : avant / après
Comment lire le schéma
- Avant : 5 équipes (à gauche) pointent toutes vers la même classe. Chaque flèche rouge est une raison de modifier
CommandeService, donc un risque de casser les 4 autres. - Après : chaque équipe a sa classe verte. Une demande de la compta ne touche que
CalculateurTotalouGenerateurFacture. CommandeService(en violet) ne fait plus qu’appeler les autres dans l’ordre : c’est un chef d’orchestre.- Légende : flèche pleine = « utilise » · flèche à triangle blanc = « implémente / hérite » · cadre pointillé jaune = interface · vert = conforme · rouge = problème.
Astuce d’expert
Un orchestrateur qui reçoit plus de 5 ou 6 dépendances au constructeur est un signal d’alarme : soit il porte encore de la logique métier, soit plusieurs étapes forment un sous-processus à extraire (par exemple FinalisationCommande qui regroupe facture et notification). Le nombre de paramètres du constructeur est la métrique SRP la plus simple à surveiller.
Ce qu’on gagne
| Critère | Sans pattern | Avec pattern |
|---|---|---|
| Raisons de changer par classe | 5 | 1 |
| Tester le calcul TTC | Base + PDF + serveur mail | Un test unitaire de 4 lignes |
| Réutilisation | Aucune | CalculateurTotal sert aussi au panier et au devis B2B ; NotificationClient à l’expédition |
| Travail en équipe | 5 équipes sur un fichier | Chaque équipe a son fichier |
| Ajouter un SMS | Modifier le cœur de la commande | Modifier seulement NotificationClient |
Bonnes pratiques
- Test rapide : décrivez la classe en une phrase. Si vous dites « et », elle en fait trop.
- Nommez les classes par ce qu’elles font :
CalculateurTotal, pasCommandeUtils. - Le service orchestrateur ne contient pas de règle métier : il appelle, dans l’ordre.
Erreurs fréquentes
- Confondre SRP et « une méthode par classe » : on découpe par acteur métier, pas par ligne.
- Créer des classes
HelperouManagerfourre-tout : c’est déplacer le problème.
Lab · 20 min
- Le marketing demande d’envoyer aussi un SMS de confirmation. Combien de classes modifiez-vous dans la version sans pattern ? Et avec ?
- La compta veut une TVA à 5,5 % pour les livres. Quelle classe change ? Écrivez son test.
- Bonus : extrayez la génération de facture dans un traitement asynchrone. Qu’est-ce qui change dans
CommandeService?
À retenir
- Une classe = une raison de changer = un acteur métier.
- Le service orchestre, les composants font le travail.
- SRP rend les 4 autres principes possibles.
Quiz rapide
- Combien de raisons de changer avait le
CommandeServiceinitial ? → 5. - Où se trouve la règle de TVA après refactoring ? → Dans
CalculateurTotal. - « Gère la commande et envoie la facture » : SRP respecté ? → Non, le « et » le trahit.
Suivez votre progression. Créez un compte gratuit pour cocher vos leçons et reprendre là où vous en étiez.
Créer un compte gratuit