Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1219 minIntermédiaire

Les principes SOLID expliqués avec des exemples

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • énoncer les cinq principes SOLID avec un exemple ;
  • repérer le principe violé dans un code ;
  • découper une classe qui a trop de responsabilités ;
  • expliquer couplage et cohésion.

3 exercices corrigés1 schéma

SOLID est l'acronyme de cinq principes de conception orientée objet : responsabilité unique (Single responsibility), ouvert/fermé (Open/closed), substitution de Liskov (Liskov substitution), ségrégation des interfaces (Interface segregation) et inversion des dépendances (Dependency inversion). Rassemblés par Robert C. Martin au début des années 2000 (l'acronyme est de Michael Feathers), ils servent à écrire des classes faciles à comprendre, à tester et à faire évoluer. Ce chapitre explique chaque principe avec un exemple de GestParc, avant et après, puis les notions de couplage et de cohésion qui les relient.

Pourquoi des principes de conception

Un programme qui marche n'est que la moitié du travail. L'autre moitié, c'est qu'il continue de marcher quand on le modifie : nouvelle règle de gestion, nouveau format d'export, passage d'une application console à WinForms ou à une API. À l'E6, la commission te demande précisément de faire évoluer ton application en une heure (les évolutions types de l'E6 SLAM) : une conception propre fait la différence entre une modification de dix lignes et une réécriture paniquée.

Les principes SOLID ne sont pas des règles de syntaxe. Ce sont des critères pour juger une conception : quand une classe devient difficile à modifier, l'un d'eux explique presque toujours pourquoi.

S : le principe de responsabilité unique

Une classe ne doit avoir qu'une seule raison de changer.

Voici une classe qui fait tout :

csharp
class RapportTickets
{
    public void Generer(List<Ticket> tickets, string chemin)
    {
        // 1. Calculer les statistiques par service
        // 2. Mettre en forme en CSV, avec les en-têtes
        // 3. Écrire le fichier sur le disque
        // 4. Envoyer le fichier par courriel au responsable
    }
}

Elle a quatre raisons de changer : une nouvelle statistique, un nouveau format (PDF), un nouvel emplacement de stockage, un nouveau serveur de messagerie. Chaque modification risque de casser les trois autres fonctions, et rien ne se teste séparément. On découpe selon les responsabilités :

csharp
record LigneStat(string Service, int Nombre, int Minutes);

class StatistiquesTickets
{
    public IReadOnlyList<LigneStat> ParService(IEnumerable<Ticket> tickets) =>
        tickets.GroupBy(t => t.Service)
               .Select(g => new LigneStat(g.Key, g.Count(), g.Sum(t => t.DureeMin)))
               .OrderBy(l => l.Service)
               .ToList();
}

interface IFormatRapport
{
    string Formater(IReadOnlyList<LigneStat> lignes);
}

class FormatCsv : IFormatRapport
{
    public string Formater(IReadOnlyList<LigneStat> lignes) =>
        "service;nombre;minutes\n" + string.Concat(lignes.Select(l => $"{l.Service};{l.Nombre};{l.Minutes}\n"));
}

class FormatTexte : IFormatRapport
{
    public string Formater(IReadOnlyList<LigneStat> lignes)
    {
        var sb = new System.Text.StringBuilder();
        foreach (LigneStat l in lignes)
        {
            sb.AppendLine($"{l.Service} : {l.Nombre} ticket(s), {l.Minutes} min");
        }
        return sb.ToString();
    }
}

Avec la liste tickets du chapitre 6 :

csharp
var lignes = new StatistiquesTickets().ParService(tickets);
Console.Write(new FormatTexte().Formater(lignes));
Console.Write(new FormatCsv().Formater(lignes));
text
Commercial : 2 ticket(s), 70 min
Comptabilité : 2 ticket(s), 65 min
Direction : 1 ticket(s), 0 min
Juridique : 1 ticket(s), 0 min
Logistique : 1 ticket(s), 90 min
service;nombre;minutes
Commercial;2;70
Comptabilité;2;65
Direction;1;0
Juridique;1;0
Logistique;1;90

Le calcul se teste sans fichier ni courriel ; le format change sans toucher au calcul. Un indice de violation : une classe dont le nom contient « Gestion », « Manager » ou « Utils » et qui grossit à chaque évolution.

O : le principe ouvert/fermé

Une classe doit être ouverte à l'extension, mais fermée à la modification.

On doit pouvoir ajouter un comportement sans modifier le code existant qui fonctionne. Au chapitre 8, le switch sur le type de matériel violait ce principe : chaque nouveau type obligeait à le modifier. La version polymorphe le respecte : un nouveau type, c'est une nouvelle classe.

L'exemple de responsabilité unique le respecte aussi : un export PDF s'ajoute en écrivant une classe FormatPdf : IFormatRapport, sans toucher à StatistiquesTickets ni aux formats existants. De même, l'échéance d'un ticket se calcule par une interface ICalculEcheance : un nouveau contrat de service, c'est une nouvelle classe (chapitre 13).

Les outils de l'ouverture/fermeture sont ceux des chapitres précédents : polymorphisme, classes abstraites, interfaces.

L : le principe de substitution de Liskov

Un objet d'une classe fille doit pouvoir remplacer un objet de la classe mère sans que le programme se comporte mal.

Formulé par Barbara Liskov en 1987, il précise ce que veut dire « est un ». Une classe fille ne doit ni refuser ce que la mère accepte, ni promettre moins qu'elle. Contre-exemple : pour réutiliser du code, une imprimante hérite de Poste.

csharp
class PosteV1(string num)
{
    public string NumInventaire { get; } = num;
    public virtual void Installer(string logiciel) => Console.WriteLine($"{logiciel} installé sur {NumInventaire}");
}

class ImprimanteV1(string num) : PosteV1(num)
{
    public override void Installer(string logiciel) =>
        throw new NotSupportedException("on n'installe pas de logiciel sur une imprimante");
}

Tout code qui installe un logiciel sur une liste de PosteV1 plante dès qu'une imprimante s'y trouve : il faudrait tester le type avant chaque appel, ce qui détruit le polymorphisme. La fille ne respecte pas le contrat de la mère.

La correction : l'imprimante n'est pas un poste. Elle hérite de Materiel, et seule la capacité d'installation est partagée par une interface que seuls les postes implémentent.

csharp
interface IInstallable
{
    void Installer(string logiciel);
}

class PosteFixe(string num) : IInstallable
{
    public string NumInventaire { get; } = num;
    public void Installer(string logiciel) => Console.WriteLine($"{logiciel} installé sur {NumInventaire}");
}

class Portable(string num) : PosteFixe(num);

L'exemple classique des manuels est le carré qui hérite du rectangle : si on change la largeur d'un carré, sa hauteur change aussi, ce qu'aucun code écrit pour un rectangle n'attend. Mathématiquement, un carré est un rectangle ; en programmation, un carré modifiable n'en est pas un.

I : le principe de ségrégation des interfaces

Aucune classe ne doit dépendre de méthodes qu'elle n'utilise pas.

Une interface trop large oblige ses implémentations à écrire des méthodes absurdes :

csharp
interface IMateriel
{
    decimal ValeurNette(DateOnly date);
    void Installer(string logiciel);
    void Imprimer(string document);
    int NiveauEncre();
}

Un poste doit implémenter Imprimer et NiveauEncre, une imprimante Installer : on retombe sur les NotSupportedException du principe précédent. On découpe en interfaces centrées sur un usage, que chaque classe combine selon ses capacités :

csharp
interface IAmortissable { decimal ValeurNette(DateOnly date); }
interface IInstallable  { void Installer(string logiciel); }
interface IImprimante   { void Imprimer(string document); int NiveauEncre(); }

// class Poste : Materiel, IAmortissable, IInstallable
// class Imprimante : Materiel, IAmortissable, IImprimante

Le code qui calcule la valeur du parc ne dépend que d'IAmortissable ; il ne voit rien de l'impression.

D : le principe d'inversion des dépendances

Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre d'abstractions.

Le haut niveau, ce sont les règles métier : ouvrir un ticket, prévenir l'astreinte s'il est critique. Le bas niveau, ce sont les détails techniques : MySQL, l'envoi de courriels, l'horloge du système. Si TicketService crée lui-même un TicketRepositoryMySql et un EnvoiSmtp, la règle métier est soudée à ces techniques : impossible de la tester sans base ni serveur de messagerie, impossible de changer de base sans la modifier.

csharp
// Avant : le métier dépend directement de la technique
class TicketService
{
    private readonly TicketRepositoryMySql depot = new TicketRepositoryMySql("server=…");
    private readonly EnvoiSmtp courriel = new EnvoiSmtp("smtp.sorbier.example");
    // …
}

L'inversion consiste à faire dépendre le métier d'interfaces qu'il définit selon ses besoins, et à laisser les classes techniques les implémenter. La flèche de dépendance, qui allait du métier vers la technique, s'inverse : c'est la technique qui dépend du contrat fixé par le métier.

csharp
// Après : le métier dépend d'abstractions, reçues de l'extérieur
public class TicketService
{
    private readonly ITicketRepository depot;
    private readonly INotificateur notificateur;

    public TicketService(ITicketRepository depot, INotificateur notificateur)
    {
        this.depot = depot;
        this.notificateur = notificateur;
    }
}
Avant : le métier dépend de la techniqueTicketServiceTicketRepositoryMySqlEnvoiSmtpnew dans le code métierAprès : la technique dépend du contrat du métierTicketService«interface»ITicketRepository«interface»INotificateurcouche métiercouche techniqueTicketRepositoryMySqlTicketRepositoryMemoireNotificateurCourrielNotificateurConsole
Après l'inversion, la technique dépend du contrat fixé par le métier

Qui fournit alors les objets concrets ? Le code de démarrage de l'application, par injection de dépendances : c'est le sujet du chapitre 14. En test, on fournit une fausse implémentation en mémoire (chapitre 15).

Couplage et cohésion

Les cinq principes poursuivent deux objectifs, que tu peux citer à l'oral :

NotionDéfinitionObjectif
Couplagele degré de dépendance entre classes : combien une classe doit connaître des autresfaible : modifier une classe ne casse pas les autres
Cohésionle degré d'unité d'une classe : ses membres servent-ils un même but ?forte : une classe fait une chose, entièrement

La responsabilité unique augmente la cohésion ; l'inversion des dépendances et la ségrégation des interfaces diminuent le couplage ; ouvert/fermé et Liskov rendent l'extension sûre.

D'autres principes à connaître

PrincipeIdéeExemple
DRY (Don't Repeat Yourself)chaque règle est écrite à un seul endroitla validation d'un numéro d'inventaire dans un record, pas dans chaque formulaire
KISS (Keep It Simple, Stupid)la solution la plus simple qui répond au besoinune méthode de dix lignes plutôt qu'un patron de conception superflu
YAGNI (You Aren't Gonna Need It)ne pas coder ce dont on n'a pas besoin aujourd'huipas d'export PDF « au cas où »
Loi de Déméterne parle qu'à tes voisins immédiatsticket.NomDuDemandeur() plutôt que ticket.Demandeur.Service.Responsable.Nom dispersé partout
Composition plutôt qu'héritageassembler des objets plutôt que d'empiler des niveaux d'héritagechapitre 7

SOLID sans dogmatisme

Appliquer SOLID à la lettre sur un programme de cent lignes produit dix interfaces et autant de fichiers pour rien : c'est l'inverse de KISS. Les principes servent quand le code grandit et change. Une bonne pratique : écris d'abord simplement ; quand une modification te fait souffrir (le même switch à changer trois fois, une classe impossible à tester), identifie le principe violé et corrige à ce moment-là. À l'oral de l'E6, savoir dire « j'ai extrait cette interface pour pouvoir tester le service sans la base » vaut mieux que réciter les cinq définitions.

Exercices

Exercice 1 sur 3 Quel principe est violé ? Réussi À revoir

Pour chaque extrait, nomme le principe SOLID violé.

  1. Une classe Facture calcule ses totaux, se sauvegarde dans MySQL et se met en page en PDF.
  2. class LogicielLibre : Logiciel redéfinit Acheter(int nbLicences) en levant NotSupportedException.
  3. class Rapport { private readonly ExportExcel export = new ExportExcel(); … }
  4. Une interface IUtilisateur avec SeConnecter(), ValiderNoteDeFrais(), GererLesDroits() et ConsulterPlanning(), implémentée par les visiteurs, les comptables et les administrateurs.
  5. Pour ajouter un type de contrat, il faut ajouter un case dans quatre switch répartis dans l'application.
Voir la réponseMasquer la réponse de l'exercice 1
  1. S, responsabilité unique : trois raisons de changer (calcul, stockage, présentation).
  2. L, substitution de Liskov : un LogicielLibre ne peut pas remplacer un Logiciel dans le code qui achète des licences.
  3. D, inversion des dépendances : Rapport crée lui-même une classe technique concrète ; il devrait recevoir une abstraction (IExport).
  4. I, ségrégation des interfaces : un visiteur doit implémenter GererLesDroits(), qu'il n'utilise pas.
  5. O, ouvert/fermé : chaque extension oblige à modifier du code existant ; le polymorphisme (une classe par type de contrat) règle le problème.

Tu avais trouvé ?

Exercice 2 sur 3 Découper une classe Réussi À revoir

Cette classe viole la responsabilité unique. Propose un découpage : noms des classes ou interfaces, et rôle de chacune.

csharp
class GestionImport
{
    public void Importer(string chemin)
    {
        // lit le fichier CSV ligne par ligne
        // vérifie chaque ligne : numéro au format PC-0000, prix positif
        // crée les objets Poste
        // les enregistre dans la base MySQL
        // écrit un compte rendu dans un fichier journal
    }
}
Voir la réponseMasquer la réponse de l'exercice 2

Un découpage possible :

Classe ou interfaceResponsabilité
LecteurCsvlire le fichier et produire des lignes brutes (string[] ou un record LigneImport)
NumeroInventaire (record) et le constructeur de Postevalider les données : le format du numéro, le prix positif
MaterielFactorycréer le bon objet à partir d'une ligne (chapitre 13)
IPosteRepository (implémentée par PosteRepositoryMySql)enregistrer les postes
IJournalécrire le compte rendu
ImportServiceorchestrer : lire, créer, enregistrer, journaliser, en recevant les objets précédents par son constructeur

ImportService garde une seule raison de changer : l'enchaînement des étapes. Chaque étape se teste seule, et passer de MySQL à une API, ou du fichier journal à la console, ne touche qu'une classe.

Tu avais trouvé ?

Exercice 3 sur 3 Inverser une dépendance Réussi À revoir

Réécris cette classe pour qu'elle respecte l'inversion des dépendances, et qu'un test puisse vérifier le message journalisé à une heure fixée.

csharp
class ExportCompta
{
    public void Exporter(IEnumerable<Facture> factures)
    {
        decimal total = factures.Sum(f => f.Montant);
        File.AppendAllText("export.log", $"{DateTime.Now:dd/MM/yyyy HH:mm} export de {factures.Count()} factures, {total:N2} €\n");
    }
}

Avec deux factures de 1 200 € et 349,90 € exportées le 2 novembre 2026 à 18 h 30, quel message est journalisé ?

Voir la réponseMasquer la réponse de l'exercice 3

Deux dépendances techniques sont cachées : le fichier (File) et l'horloge (DateTime.Now). On les remplace par deux interfaces, reçues par le constructeur :

csharp
interface IJournal { void Ecrire(string message); }
interface IHorloge { DateTime Maintenant { get; } }

class ExportCompta(IJournal journal, IHorloge horloge)
{
    public void Exporter(IEnumerable<Facture> factures)
    {
        decimal total = factures.Sum(f => f.Montant);
        journal.Ecrire($"{horloge.Maintenant:dd/MM/yyyy HH:mm} export de {factures.Count()} factures, {total:N2} €");
    }
}

En production, on injecte un JournalFichier et une horloge système ; en test, un JournalMemoire qui garde les lignes dans une liste et une HorlogeFixe. Le message journalisé est « 02/11/2026 18:30 export de 2 factures, 1 549,90 € ». .NET fournit déjà une abstraction de l'horloge, la classe TimeProvider, utilisée au chapitre 14.

Tu avais trouvé ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Design patterns : Repository, Stratégie, Fabrique, SingletonRevoir le sommaire du cours