Watch a demoFree trial
Blog

Compréhension des principes SOLID dans la conception de logiciels

PHPDevOpsdette technique
04 août 2025
Partager
Cet article est également disponible en Allemande et Anglais.

Les principes SOLID facilitent la maintenance et l'adaptation de votre code. Ces cinq pratiques de conception orientée objet vous aident à écrire un code plus propre et plus fiable.

Nous allons explorer chaque principe à l'aide d'exemples pratiques permettant de créer des applications faciles à maintenir, qu'il s'agisse de bases de code uniques ou de systèmes distribués.

Que sont les principes SOLID dans le développement de logiciels ?

L'écriture d'un code maintenable est un défi. Les principes SOLID vous donnent des lignes directrices pratiques pour écrire un meilleur code. Robert C. Martin a créé ces principes pour résoudre des problèmes courants tels que la rigidité du code, les bogues en cascade et les systèmes complexes.

Chaque principe SOLID cible un défi de codage spécifique :

S : Principe de responsabilité unique (SRP) : simplifier la maintenance du code

Le principe de responsabilité unique permet à votre code d'être maintenable. Chaque classe effectue une tâche spécifique, ni plus ni moins.

Pensez-y de la manière suivante : L'authentification de l'utilisateur doit être séparée de la journalisation ou des notifications. Lorsqu'une classe essaie de faire plusieurs choses, les tests et les mises à jour deviennent plus complexes.

Relations entre les classes SRP

Le diagramme utilise la composition pour montrer comment UserRegistration fonctionne avec d'autres classes. Chaque flèche de diamant creux signifie que UserRegistration fait référence à une classe mais ne contrôle pas son cycle de vie.

Légende du diagramme :

  • + signifie méthodes/propriétés publiques
  • - signifie méthodes/propriétés privées
  • Les flèches indiquent les dépendances entre les classes
  • Les encadrés indiquent les noms des classes et leurs membres

Voici comment la séparation des responsabilités en matière de code permet de créer des applications plus propres et plus faciles à maintenir :

// Cet exemple démontre le Principe de Responsabilité Unique (SRP)
// Chaque classe a un seul rôle spécifique, ce qui rend le code plus maintenable et testable

// UserDataFormatter : Responsable de la conversion des données utilisateur dans un format cohérent
class UserDataFormatter {
    public function formatUserData($user) {
        // Extrait et formate les informations essentielles de l'utilisateur
        return [
            'name' => $user->getFullName(),    // Obtient le nom complet de l'utilisateur
            'email' => $user->getEmail(),      // Obtient l'email de l'utilisateur
            'joined' => $user->getJoinDate()->format('Y-m-d')  // Formate la date d'adhésion
        ];
    }
}

// UserDataValidator : Responsable de s'assurer que les données répondent aux exigences
class UserDataValidator {
    public function validateUserData($userData) {
        // Effectue deux vérifications de validation :
        // 1. Vérifie que l'email est dans un format valide
        // 2. S'assure que le champ nom n'est pas vide
        return filter_var($userData['email'], FILTER_VALIDATE_EMAIL) 
            && !empty($userData['name']);
    }
}

// UserDataPersistence : Responsable des opérations de base de données
class UserDataPersistence {
    private $database;

    // Initialise avec la connexion à la base de données
    public function __construct($database) {
        $this->database = $database;
    }

    // Gère la sauvegarde des données utilisateur dans la base de données
    public function saveUser($userData) {
        // Effectue l'opération d'insertion réelle dans la base de données
        return $this->database->insert('users', $userData);
    }
}

// UserRegistration : Orchestre le processus d'inscription
// Agit comme une façade, coordonnant les autres classes spécialisées
class UserRegistration {
    private $formatter;
    private $validator;
    private $persistence;

    // Injection de dépendances via le constructeur
    public function __construct(
        UserDataFormatter $formatter,
        UserDataValidator $validator,
        UserDataPersistence $persistence
    ) {
        $this->formatter = $formatter;
        $this->validator = $validator;
        $this->persistence = $persistence;
    }

    // Méthode d'inscription principale qui coordonne l'ensemble du processus
    public function registerUser($user) {
        // Étape 1 : Formater les données utilisateur
        $userData = $this->formatter->formatUserData($user);

        // Étape 2 : Valider les données formatées
        if ($this->validator->validateUserData($userData)) {
            // Étape 3 : Si la validation réussit, sauvegarder dans la base de données
            return $this->persistence->saveUser($userData);
        }

        // Si la validation échoue, lancer une exception
        throw new ValidationException('Données utilisateur invalides');
    }
}

// DÉMONSTRATION D'EXEMPLE D'UTILISATION
// Ci-dessous montre comment tous les composants fonctionnent ensemble

// Créer une fausse base de données pour la démonstration
$database = new class {
    public function insert($table, $data) {
        // Simule l'insertion dans la base de données et retourne le succès
        echo "Inséré dans $table: " . json_encode($data) . "\\n";
        return true;
    }
};

try {
    // Étape 1 : Initialiser tous les composants requis
    $formatter = new UserDataFormatter();
    $validator = new UserDataValidator();
    $persistence = new UserDataPersistence($database);

    // Étape 2 : Créer le service principal UserRegistration
    $userRegistration = new UserRegistration($formatter, $validator, $persistence);

    // Étape 3 : Créer un faux objet utilisateur pour le test
    $user = new class {
        public function getFullName() {
            return "Jane Doe";
        }
        public function getEmail() {
            return "jane@example.com";
        }
        public function getJoinDate() {
            return new DateTime();
        }
    };

    // Étape 4 : Tenter d'enregistrer l'utilisateur
    $userRegistration->registerUser($user);
    echo "Utilisateur enregistré avec succès !";
} catch (ValidationException $e) {
    // Gérer toutes les erreurs de validation qui se produisent
    echo "Échec de l'enregistrement de l'utilisateur : " . $e->getMessage();
}

// Classe d'exception personnalisée pour les erreurs spécifiques à la validation
class ValidationException extends Exception {}


Chaque classe a une tâche unique et ciblée. Le formateur formate les données. Le validateur vérifie les données. La couche de persistance enregistre les données. La classe d'enregistrement coordonne proprement ces éléments. Cette structure facilite les tests et la maintenance.

Cette conception propre s'aligne sur le principe "ouvert-fermé". Vous souhaitez ajouter de nouvelles règles de formatage ou de nouveaux contrôles de validation ? Créez de nouvelles classes qui suivent le modèle existant. Votre code reste intact tandis que les fonctionnalités se développent.

O : Principe ouvert-fermé (OCP) : écrire un code extensible et évolutif

Le principe ouvert-fermé (OCP) vous permet d'ajouter des fonctionnalités sans modifier le code existant. Votre base de code reste stable tout en se développant. Au lieu de modifier le code existant, étendez les fonctionnalités grâce à de nouvelles classes et interfaces.

L : Principe de substitution de Liskov (PSL) : éviter les ruptures de code dans la POO

Le principe de substitution de Liskov (LSP) signifie que les classes enfants doivent fonctionner partout où leurs classes parents fonctionnent. Si ce n'est pas le cas, votre code se brisera de manière inattendue.

Voici une violation courante du principe de substitution de Liskov et la façon de la corriger :

// ❌ Viole le LSP : Cela viole le Principe de Substitution de Liskov car
// une sous-classe (Penguin) modifie le comportement attendu de sa classe parente (Bird)
class Bird {
    public function fly() {
        // Logique pour voler
    }
}

class Penguin extends Bird {
    public function fly() {
        // Cela rompt le contrat établi par la classe parente
        throw new Exception("Les pingouins ne peuvent pas voler !");
    }
}

// ✅ Refactorisé : Meilleure conception utilisant la composition plutôt que l'héritage
// Cette approche sépare la capacité de voler de la classification des oiseaux
interface Flyable {
    public function fly();
}

// Seuls les oiseaux qui peuvent réellement voler implémentent l'interface Flyable
class Sparrow implements Flyable {
    public function fly() {
        // Les oiseaux qui volent implémentent leur comportement de vol spécifique
    }
}

// Penguin n'implémente pas Flyable, évitant le problème d'héritage forcé
class Penguin {
    // Les pingouins ont leurs propres comportements sans être forcés d'implémenter fly()
}

I : Principe de séparation des interfaces (ISP) : concevoir des interfaces propres et ciblées

Le principe de ségrégation des interfaces vous aide à écrire un code plus propre en gardant les interfaces petites et ciblées. De cette manière, les classes n'implémentent que les méthodes qu'elles utilisent réellement.

Voyons comment diviser les interfaces surchargées en interfaces plus petites et plus ciblées.

// ❌ Viole l'ISP : L'interface force les classes à implémenter des méthodes dont elles n'ont pas besoin
interface Animal {
    public function fly();    // Problème : Le poisson ne devrait pas en avoir besoin
    public function swim();   // Problème : Les oiseaux ne devraient pas en avoir besoin
}

class Fish implements Animal {
    public function swim() {
        // Logique de nage
    }
    
    public function fly() {
        // Forcé d'implémenter une méthode non pertinente
        throw new Exception("Les poissons ne peuvent pas voler");
    }
}

// ✅ Refactorisé : Suivant l'ISP avec des interfaces ciblées et spécifiques
// Chaque classe n'implémente que les méthodes dont elle a réellement besoin
interface Flyable {
    public function fly();    // Interface pour les créatures volantes
}

interface Swimmable {
    public function swim();   // Interface pour les créatures nageuses
}

class Fish implements Swimmable {
    public function swim() {
        // Fish n'implémente que ce qu'il peut réellement faire
        // Ceci suit l'ISP en n'imposant pas de méthodes inutiles
    }
}

class Bird implements Flyable {
    public function fly() {
        // Bird n'implémente que le comportement de vol
        // Aucune méthode inutilisée n'est imposée à la classe
    }
}

D : Principe d'inversion de dépendance (DIP) : améliorer la flexibilité du code grâce aux abstractions

Le principe d'inversion de la dépendance vous aide à écrire un meilleur code en utilisant des interfaces au lieu d'implémentations directes. Vos modules de haut niveau travaillent avec des abstractions et non des spécificités. Cela vous permet d'échanger des composants sans casser votre système.

Voici un exemple pratique :

// ❌ Viole le DIP : Le module de haut niveau dépend du module de bas niveau
class EmailNotifier {
    public function send($message) {
        // Logique d'envoi d'e-mail
    }
}

class Notification {
    private $emailNotifier;
    
    public function __construct() {
        $this->emailNotifier = new EmailNotifier();
    }
    
    public function notify($message) {
        $this->emailNotifier->send($message);
    }
}

// ✅ Refactorisé : Dépendre des abstractions, pas des implémentations concrètes
interface Notifier {
    public function send($message);
}

class EmailNotifier implements Notifier {
    public function send($message) {
        // Logique d'envoi d'e-mail
    }
}

class SMSNotifier implements Notifier {
    public function send($message) {
        // Logique d'envoi de SMS
    }
}

class Notification {
    private $notifier;
    
    public function __construct(Notifier $notifier) {
        $this->notifier = $notifier;
    }
    
    public function notify($message) {
        $this->notifier->send($message);
    }
}
// Cette conception permet d'échanger EmailNotifier avec SMSNotifier sans modifier Notification

Les principes SOLID sont des outils simples qui fonctionnent ensemble. Lorsqu'ils sont combinés correctement, ils vous aident à écrire un code propre, plus facile à maintenir et à déboguer.

Voici une décomposition claire de ce que fait chaque principe :

PrincipeRésuméAvantage principal
Responsabilité unique (SRP)Une classe ne doit avoir qu'une seule raison de changerRéduit la complexité de la maintenance
Ouvert-fermé (OCP)Les entités logicielles doivent être ouvertes à l'extension mais fermées à la modification.Permet l'ajout de fonctionnalités évolutives
Substitution de Liskov (LSP)Les classes dérivées peuvent être substituées à leurs classes de baseAssure la cohérence du comportement
Séparation des interfaces (ISP)Les clients ne doivent pas être contraints de dépendre d'interfaces qu'ils n'utilisent pas.Minimise le couplage entre les composants
Inversion de dépendance (DIP)Dépendre d'abstractions et non d'implémentations concrètesAméliore la flexibilité du système


La ségrégation des interfaces et l'inversion de dépendance permettent de construire des systèmes adaptables. La ségrégation des interfaces permet à chaque microservice de n'exposer que les fonctions essentielles. L'inversion de dépendance permet d'échanger des composants sans modifier le code de base.

L'inversion de dépendance fonctionne particulièrement bien pour les systèmes de plugins. L'utilisation d'interfaces au lieu de dépendances codées en dur rend les composants interchangeables, ce qui facilite l'intégration de nouveaux outils.

Par exemple, un CMS peut basculer entre SQL, NoSQL ou des fichiers pour le stockage. La ségrégation des interfaces permet de maintenir les connexions propres et minimales. Ensemble, ces principes créent des systèmes qui s'adaptent aux nouvelles exigences.

Comment les principes SOLID rendent la conception de logiciels maintenable et évolutive

L'écriture d'un code maintenable est un défi. Les corrections rapides effectuées pendant les délais créent une dette technique qu'il est difficile de réparer par la suite. Les principes SOLID offrent une solution.

Ces principes aident les équipes à élaborer un code stable et flexible, plus facile à comprendre et à modifier. Résultat : moins de bugs et des cycles de développement plus rapides.

Une structure de code claire permet un meilleur travail d'équipe. Lorsque chaque composant a un objectif spécifique, les tests et le débogage deviennent plus simples.

La norme SOLID favorise la croissance. Grâce au principe d'ouverture et de fermeture, vous pouvez étendre les fonctionnalités sans modifier le code existant, ce qui est essentiel pour les projets évolutifs.

Au fur et à mesure que les besoins évoluent, les principes SOLID garantissent que votre code peut s'adapter. Ajoutez des fonctionnalités ou modifiez des fonctionnalités sans refactorisation majeure.

Si les raccourcis peuvent sembler efficaces dans l'immédiat, ils créent des problèmes à long terme : un développement plus lent, davantage de bugs et la frustration des développeurs. Les principes SOLID permettent d'obtenir un code plus propre et plus facile à maintenir.

Robert C. Martin le dit clairement : "Une bonne architecture rend le système facile à comprendre, facile à développer, facile à maintenir et facile à déployer. L'objectif ultime est de minimiser le coût de la durée de vie du système et de maximiser la productivité des programmeurs". Les principes SOLID rendent cela possible.

3. Décomposition des principes SOLID (avec exemples)

Principe de la responsabilité unique (SRP)

Chaque classe ne doit faire qu'une seule chose.

Prenons l'exemple d'une classe de rapport qui formate les données et les écrit dans une base de données. Cela pose des problèmes : lorsque vous mettez à jour le formatage ou que vous changez de base de données, vous risquez de casser les deux fonctionnalités.

La solution ? Diviser la classe en ReportFormatter et ReportWriter. Chaque classe a un objectif précis. Vous souhaitez modifier la mise en forme d'un rapport ? Mettez à jour ReportFormatter. Besoin de changer de base de données ? Modifiez ReportWriter.

Cette séparation simplifie également les tests, puisque vous pouvez vérifier séparément les opérations de formatage et de base de données.

Voici un exemple de code :

// ❌ Viole le SRP : Une classe gérant plusieurs responsabilités
class Report {
    public function formatData($data) {
        // Formater les données en JSON
        // Cela viole le SRP en mélangeant les préoccupations de formatage et de stockage
    }
    
    public function writeToDatabase($data) {
        // Écrire les données dans la base de données
        // Cela crée un couplage fort entre le formatage des données et le stockage
    }
}

// ✅ Refactorisé : Suivant le SRP avec une séparation claire des préoccupations
class ReportFormatter {
    public function formatData($data) {
        // Formater les données en JSON
        // Cette classe a une seule responsabilité : le formatage des données
    }
}

class ReportWriter {
    public function writeToDatabase($data) {
        // Écrire les données dans la base de données
        // Cette classe a une seule responsabilité : la persistance des données
        // Le découplage permet un test et une maintenance plus faciles
    }
}

 

  • Principale leçon à retenir : L'ASR simplifie le code en donnant à chaque composant un objectif clair et unique. Cela réduit les coûts de maintenance et facilite les changements.

Erreurs courantes à éviter lors de l'application des principes SOLID

Même les développeurs expérimentés peuvent avoir du mal à appliquer les principes SOLID. Voici les erreurs les plus courantes à éviter :

Surcharge d'interfaces

Ne créez pas trop de petites interfaces. Regroupez les fonctions connexes de manière logique. Par exemple, un système de permissions fonctionne mieux avec une seule interface UserPermissions qu'avec des interfaces séparées pour la lecture, l'écriture et la gestion des rôles.

Mauvaise utilisation des classes d'aide

Évitez de déverser de multiples tâches dans des classes d'aide (helper classes). Décomposez-les plutôt en composants ciblés. Exemple : Remplacer un DataProcessorHelper par des classes DataValidator, DataFormatter et Logger distinctes.

Abstractions inutiles

Ne créez des interfaces que lorsqu'elles apportent une valeur ajoutée. Une simple classe StringFormatter n'a pas besoin d'interface, elle ajoute de la complexité sans aucun avantage.

Garder l'équilibre

N'abusez pas d'un seul principe. Trop de séparation crée une complexité inutile. Une séparation insuffisante rend le code rigide. Laissez les principes SOLID guider vos décisions de conception sans les contraindre.

Le respect de ces principes permet de créer un code maintenable et évolutif.

Pourquoi les principes SOLID sont-ils importants pour la conception de logiciels propres et modulaires ?

Il est essentiel d'écrire un code facile à maintenir. Les principes SOLID facilitent la modification et l'évolution de votre code. Voici comment :

Votre code doit s'adapter à la croissance de votre projet. Grâce aux principes d'ouverture et d'inversion de dépendance, vous pouvez ajouter des fonctionnalités sans casser les fonctionnalités existantes.

En voici un exemple : Supposons que vous construisiez un site de commerce électronique qui gère le calcul des taxes. Grâce à ces principes, vous pouvez ajouter de nouvelles règles fiscales pour différents pays sans modifier le code de base de la caisse.

Voyons cela en pratique :

// Exemple démontrant la violation du Principe Ouvert-Fermé (OCP)
// ❌ Mauvaise pratique : Cette implémentation nécessite de modifier le code existant pour ajouter de nouvelles formes
class Shape {
    // Cette méthode utilise une logique conditionnelle qui doit être modifiée pour chaque nouveau type de forme
    // Ce qui viole l'OCP car la classe doit être modifiée pour étendre les fonctionnalités
    public function calculateArea($type, $dimensions) {
        if ($type === 'circle') {
            return pi() * pow($dimensions['radius'], 2);
        } elseif ($type === 'square') {
            return pow($dimensions['side'], 2);
        }
    }
}

// Exemple démontrant une implémentation correcte suivant l'OCP
// ✅ Bonne pratique : Utilisation du polymorphisme pour permettre l'extension sans modification

// Classe de base abstraite définissant l'interface que toutes les formes doivent implémenter
// Cela crée un contrat que les classes concrètes doivent respecter
abstract class Shape {
    // La méthode abstraite force toutes les classes enfants à implémenter leur propre calcul de surface
    abstract public function calculateArea();
}

// Implémentation concrète pour Circle
// Chaque forme est responsable de son propre calcul de surface
class Circle extends Shape {
    private $radius;
    
    // Le constructeur initialise le cercle avec son rayon
    public function __construct($radius) {
        $this->radius = $radius;
    }
    
    // Implémentation spécifique pour le calcul de la surface du cercle
    // π * r²
    public function calculateArea() {
        return pi() * pow($this->radius, 2);
    }
}

// Implémentation concrète pour Square
// De nouvelles formes peuvent être ajoutées en créant de nouvelles classes sans modifier le code existant
class Square extends Shape {
    private $side;
    
    // Le constructeur initialise le carré avec la longueur de son côté
    public function __construct($side) {
        $this->side = $side;
    }
    
    // Implémentation spécifique pour le calcul de la surface du carré
    // côté * côté
    public function calculateArea() {
        return pow($this->side, 2);
    }
}
// Pour ajouter une nouvelle forme, il suffit de créer une nouvelle classe qui étend Shape
// Cela suit l'OCP car le code existant reste inchangé
  • Modularité par conception: SOLID crée des composants réutilisables. Un module d'authentification bien structuré peut être utilisé dans plusieurs projets sans modification.
  • L'attention portée à l'équipe: Des limites claires aident les équipes à travailler de manière indépendante. Les nouveaux développeurs commencent à contribuer plus rapidement grâce à des composants ciblés et à usage unique.
  • Architecture prête pour le cloud: Les petits composants ciblés se déploient et s'adaptent facilement aux environnements cloud et aux microservices.

Comment utiliser les principes SOLID pour les microservices et les systèmes prêts pour le cloud ?

Upsun fournit les outils dont vous avez besoin pour écrire un meilleur code, que vous construisiez des monolithes ou des microservices.

Test modulaire

Évoluer sans contrainte

  • Concentrez-vous sur le code pendant que l'infrastructure évolue automatiquement
  • Utiliser des flux de travail Git qui supportent des changements propres et extensibles

Structure du code

  • Profilez et décomposez efficacement le code monolithique

Dépendances

  • Garder les implémentations et les abstractions synchronisées grâce au contrôle de version

Mettre en œuvre les principes de SOLID

Voici comment appliquer efficacement les principes SOLID :

  1. Diviser les grandes classes en composants ciblés : Chaque composant doit effectuer une tâche spécifique. Cela permet d'améliorer les tests et la maintenance. 
  2. Apporter des améliorations incrémentales : Modifiez votre code progressivement. Évitez les réécritures complètes.
  3. Testez en permanence : Exécutez des tests après chaque modification afin de détecter rapidement les problèmes.

Le respect de ces pratiques permet d'obtenir un code plus facile à maintenir et à adapter, ce qui est essentiel pour les projets en pleine croissance.

 

Votre meilleur travail
est à l'horizon

Essai gratuit
Discord
© 2025 Platform.sh. All rights reserved.