Formation SOLID en pratique

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.

Leçon gratuite

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 :

DemandeQui la faitImpact
« On gère le stock réservé pendant 15 minutes »LogistiqueOn modifie CommandeService
« Les livres passent en TVA à 5,5 % »ComptabilitéOn modifie CommandeService
« On passe de JDBC à JPA »ArchitectureOn modifie CommandeService
« La facture doit afficher le n° de TVA »ComptabilitéOn modifie CommandeService
« On envoie aussi un SMS »MarketingOn 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

CommandeService5 raisons de changer• vérifier le stock• calculer la TVA• requête SQL• facture PDF• envoyer l’e-mailLogistiqueComptabilitéArchitectureComptabilitéMarketing
Avant Une seule classe, cinq équipes qui la modifient
CommandeServiceorchestre, ne calcule rienVerificateurStockdemandé par : LogistiqueCalculateurTotaldemandé par : ComptabilitéCommandeRepositorydemandé par : ArchitectureGenerateurFacturedemandé par : ComptabilitéNotificationClientdemandé par : Marketing
Après Une classe par métier, un orchestre qui coordonne

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 CalculateurTotal ou GenerateurFacture.
  • 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èreSans patternAvec pattern
Raisons de changer par classe51
Tester le calcul TTCBase + PDF + serveur mailUn test unitaire de 4 lignes
RéutilisationAucuneCalculateurTotal sert aussi au panier et au devis B2B ; NotificationClient à l’expédition
Travail en équipe5 équipes sur un fichierChaque équipe a son fichier
Ajouter un SMSModifier le cœur de la commandeModifier 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, pas CommandeUtils.
  • 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 Helper ou Manager fourre-tout : c’est déplacer le problème.

Lab · 20 min

  1. Le marketing demande d’envoyer aussi un SMS de confirmation. Combien de classes modifiez-vous dans la version sans pattern ? Et avec ?
  2. La compta veut une TVA à 5,5 % pour les livres. Quelle classe change ? Écrivez son test.
  3. 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

  1. Combien de raisons de changer avait le CommandeService initial ? → 5.
  2. Où se trouve la règle de TVA après refactoring ? → Dans CalculateurTotal.
  3. « 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