• Contact us
  • Documentation
  • Login
Watch a demoFree trial
Blog
Blog
BlogProduitÉtudes de casNouvellesPerspectives
Blog

Comprendre les principes SOLID dans la conception de logiciels

PHPDevOpsdette technique
19 décembre 2024
Partager
Cette page a été rédigée en anglais par nos experts, puis traduite par une IA pour vous y donner accès rapidement! Pour la version originale, c’est par ici.

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

Explorons chaque principe à l'aide d'exemples concrets pour 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 en développement logiciel ?

Écrire du code maintenable est un défi. Les principes SOLID te fournissent des conseils pratiques pour écrire un meilleur code. Robert C. Martin a créé ces principes pour résoudre des problèmes courants tels que le code rigide, les bugs en cascade et les systèmes complexes.

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

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

Le principe de responsabilité unique permet de garantir la maintenabilité de ton code. Chaque classe effectue une tâche spécifique, ni plus, ni moins.

Pense-y de cette façon : sépare l'authentification des utilisateurs de la journalisation ou des notifications. Lorsqu'une classe tente d'effectuer plusieurs tâches, les tests et les mises à jour deviennent plus complexes.

Relations entre les classes SRP

Le diagramme utilise la composition pour montrer comment UserRegistration interagit avec d'autres classes. Chaque flèche en losange creuse 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 du code permet de créer des applications plus claires et plus faciles à maintenir :

// This example demonstrates the Single Responsibility Principle (SRP)
// Each class has one specific job, making the code more maintainable and testable

// UserDataFormatter: Responsible for converting user data into a consistent format
class UserDataFormatter {
    public function formatUserData($user) {
        // Extracts and formats essential user information
        return [
            'name' => $user->getFullName(),    // Get user's full name
            'email' => $user->getEmail(),      // Get user's email
            'joined' => $user->getJoinDate()->format('Y-m-d')  // Format join date
        ];
    }
}

// UserDataValidator: Responsible for ensuring data meets requirements
class UserDataValidator {
    public function validateUserData($userData) {
        // Performs two validation checks:
        // 1. Verifies email is in valid format
        // 2. Ensures name field is not empty
        return filter_var($userData['email'], FILTER_VALIDATE_EMAIL) 
            && !empty($userData['name']);
    }
}

// UserDataPersistence: Responsible for database operations
class UserDataPersistence {
    private $database;

    // Initialize with database connection
    public function __construct($database) {
        $this->database = $database;
    }

    // Handles saving user data to the database
    public function saveUser($userData) {
        // Performs the actual database insert operation
        return $this->database->insert('users', $userData);
    }
}

// UserRegistration: Orchestrates the registration process
// Acts as a facade, coordinating between other specialized classes
class UserRegistration {
    private $formatter;
    private $validator;
    private $persistence;

    // Constructor injection of dependencies
    public function __construct(
        UserDataFormatter $formatter,
        UserDataValidator $validator,
        UserDataPersistence $persistence
    ) {
        $this->formatter = $formatter;
        $this->validator = $validator;
        $this->persistence = $persistence;
    }

    // Main registration method that coordinates the entire process
    public function registerUser($user) {
        // Step 1: Format the user data
        $userData = $this->formatter->formatUserData($user);

        // Step 2: Validate the formatted data
        if ($this->validator->validateUserData($userData)) {
            // Step 3: If validation passes, save to database
            return $this->persistence->saveUser($userData);
        }

        // If validation fails, throw an exception
        throw new ValidationException('Invalid user data');
    }
}

// EXAMPLE USAGE DEMONSTRATION
// Below shows how all components work together

// Create a mock database for demonstration
$database = new class {
    public function insert($table, $data) {
        // Simulates database insertion and returns success
        echo "Inserted into $table: " . json_encode($data) . "\\n";
        return true;
    }
};

try {
    // Step 1: Initialize all required components
    $formatter = new UserDataFormatter();
    $validator = new UserDataValidator();
    $persistence = new UserDataPersistence($database);

    // Step 2: Create the main UserRegistration service
    $userRegistration = new UserRegistration($formatter, $validator, $persistence);

    // Step 3: Create a mock user object for testing
    $user = new class {
        public function getFullName() {
            return "Jane Doe";
        }
        public function getEmail() {
            return "jane@example.com";
        }
        public function getJoinDate() {
            return new DateTime();
        }
    };

    // Step 4: Attempt to register the user
    $userRegistration->registerUser($user);
    echo "User successfully registered!";
} catch (ValidationException $e) {
    // Handle any validation errors that occur
    echo "Failed to register user: " . $e->getMessage();
}

// Custom exception class for validation-specific errors
class ValidationException extends Exception {}


Chaque classe a une seule tâche bien définie. 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 ces éléments de manière claire. Cette structure facilite les tests et la maintenance.

Cette conception claire s’aligne sur le principe ouvert-fermé. Tu veux ajouter de nouvelles règles de formatage ou de nouveaux contrôles de validation ? Crée de nouvelles classes qui suivent le modèle existant. Ton code reste intact tandis que les fonctionnalités s’enrichissent.

O : Principe ouvert-fermé (OCP) : écris du code extensible et évolutif

Le principe ouvert-fermé (OCP) t'aide à ajouter des fonctionnalités sans modifier le code existant. Ta base de code reste stable à mesure qu'elle s'enrichit. Au lieu de modifier le code existant, étends les fonctionnalités à l'aide de nouvelles classes et interfaces.

L : Principe de substitution de Liskov (LSP) : éviter les ruptures de code en POO

Le principe de substitution de Liskov (LSP) signifie que les classes filles doivent fonctionner partout où leurs classes pères fonctionnent. Si ce n'est pas le cas, ton code va se casser de manière inattendue.

Voici une violation courante du LSP et comment y remédier :

// ❌ Violates LSP: This violates Liskov Substitution Principle because
// a subclass (Penguin) changes the expected behavior of its parent class (Bird)
class Bird {
    public function fly() {
        // Logic for flying
    }
}

class Penguin extends Bird {
    public function fly() {
        // This breaks the contract established by the parent class
        throw new Exception("Penguins can't fly!");
    }
}

// ✅ Refactored: Better design using composition over inheritance
// This approach separates flying capability from bird classification
interface Flyable {
    public function fly();
}

// Only birds that can actually fly implement the Flyable interface
class Sparrow implements Flyable {
    public function fly() {
        // Flying birds implement their specific flying behavior
    }
}

// Penguin doesn't implement Flyable, avoiding the forced inheritance problem
class Penguin {
    // Penguins have their own behaviors without being forced to implement fly()
}

I : Principe de ségrégation des interfaces (ISP) : conçoit des interfaces propres et ciblées

Le principe de séparation des interfaces t'aide à écrire un code plus propre en gardant les interfaces petites et ciblées. De cette façon, les classes n'implémentent que les méthodes qu'elles utilisent réellement.

Voyons comment diviser des interfaces trop lourdes en interfaces plus petites et ciblées.

// ❌ Violates ISP: Interface forces classes to implement methods they don't need
interface Animal {
    public function fly();    // Problem: Fish shouldn't need this
    public function swim();   // Problem: Birds shouldn't need this
}

class Fish implements Animal {
    public function swim() {
        // Swimming logic
    }
    
    public function fly() {
        // Forced to implement irrelevant method
        throw new Exception("Fish cannot fly");
    }
}

// ✅ Refactored: Following ISP with focused, specific interfaces
// Each class only implements the methods it actually needs
interface Flyable {
    public function fly();    // Interface for flying creatures
}

interface Swimmable {
    public function swim();   // Interface for swimming creatures
}

class Fish implements Swimmable {
    public function swim() {
        // Fish only implements what it can actually do
        // This follows ISP by not forcing unnecessary methods
    }
}

class Bird implements Flyable {
    public function fly() {
        // Birds only implement flying behavior
        // No unused methods are forced upon the class
    }
}

D : Principe d'inversion des dépendances (DIP) : améliore la flexibilité du code grâce aux abstractions

Le principe d'inversion des dépendances t'aide à écrire un meilleur code en utilisant des interfaces plutôt que des implémentations directes. Tes modules de haut niveau fonctionnent avec des abstractions et non avec des détails concrets. Cela te permet d'échanger des composants sans perturber ton système.

Voici un exemple concret :

// ❌ Violates DIP: High-level module depends on low-level module
class EmailNotifier {
    public function send($message) {
        // Logic for sending email
    }
}

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

// ✅ Refactored: Depend on abstractions, not concrete implementations
interface Notifier {
    public function send($message);
}

class EmailNotifier implements Notifier {
    public function send($message) {
        // Logic for sending email
    }
}

class SMSNotifier implements Notifier {
    public function send($message) {
        // Logic for sending SMS
    }
}

class Notification {
    private $notifier;
    
    public function __construct(Notifier $notifier) {
        $this->notifier = $notifier;
    }
    
    public function notify($message) {
        $this->notifier->send($message);
    }
}
// This design allows swapping EmailNotifier with SMSNotifier without modifying Notification

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

Voici une présentation claire de ce que fait chaque principe :

PrincipeRésuméAvantage principal
Responsabilité unique (SRP)Une classe ne doit avoir qu'une seule raison d'être modifiéeRéduit la complexité de la maintenance
Principe d'ouverture-fermeture (OCP)Les entités logicielles doivent être ouvertes à l'extension mais fermées à la modificationPermet l'ajout de fonctionnalités évolutives
Substitution de Liskov (LSP)Les classes dérivées doivent être substituables à leurs classes de baseAssure la cohérence comportementale
Séparation des interfaces (ISP)Les clients ne devraient pas être obligés de dépendre d'interfaces qu'ils n'utilisent pasRéduit au minimum le couplage entre les composants
Inversion des dépendances (DIP)Dépendre d'abstractions, pas d'implémentations concrètesAméliore la flexibilité du système


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

L'inversion des dépendances 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 d'interface permet de garder 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 logicielle maintenable et évolutive

Écrire du code maintenable est un défi. Les solutions de fortune mises en place pour respecter les délais créent une dette technique difficile à corriger par la suite. Les principes SOLID offrent une solution.

Ces principes aident les équipes à créer 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 favorise un meilleur travail d'équipe. Lorsque chaque composant a un objectif spécifique, les tests et le débogage deviennent simples.

SOLID favorise la croissance. Grâce au principe d'ouverture-fermeture, tu peux étendre les fonctionnalités sans modifier le code existant, ce qui est essentiel pour les projets en évolution.

À mesure que les exigences évoluent, les principes SOLID garantissent que ton code peut s'adapter. Ajoute des fonctionnalités ou modifie des fonctionnalités sans refactorisation majeure.

Si les raccourcis peuvent sembler efficaces à court terme, ils créent des problèmes à long terme : un développement plus lent, davantage de bugs et une frustration des développeurs. Les principes SOLID mènent à un code plus propre et plus facile à maintenir.

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

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

Principe de responsabilité unique (SRP)

Chaque classe ne doit faire qu'une seule chose.

Prenons l’exemple d’une classe Report qui formate les données et les écrit dans une base de données. Cela pose des problèmes : lorsque tu modifies la mise en forme ou que tu changes de base de données, tu risques de casser ces deux fonctionnalités.

La solution ? Divise-la en ReportFormatter et ReportWriter. Chaque classe a un objectif clair. Tu veux modifier la mise en forme du rapport ? Mets à jour ReportFormatter. Tu dois changer de base de données ? Modifie ReportWriter.

Cette séparation simplifie également les tests, car tu peux vérifier le formatage et les opérations de base de données séparément.

Voici un exemple de code :

// ❌ Violates SRP: One class handling multiple responsibilities
class Report {
    public function formatData($data) {
        // Format data as JSON
        // This violates SRP by mixing formatting and storage concerns
    }
    
    public function writeToDatabase($data) {
        // Write data to database
        // This creates tight coupling between data formatting and storage
    }
}

// ✅ Refactored: Following SRP with clear separation of concerns
class ReportFormatter {
    public function formatData($data) {
        // Format data as JSON
        // This class has a single responsibility: data formatting
    }
}

class ReportWriter {
    public function writeToDatabase($data) {
        // Write data to database
        // This class has a single responsibility: data persistence
        // Decoupling allows for easier testing and maintenance
    }
}

 

  • Point clé : le SRP simplifie le code en donnant à chaque composant un objectif unique et clair. Cela réduit les coûts de maintenance et facilite les modifications.

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

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

Surcharge d'interface

Ne crée pas trop de petites interfaces. Regroupe les fonctions apparentées de manière logique. Par exemple, un système d'autorisations fonctionne mieux avec une seule interface UserPermissions plutôt qu'avec des interfaces distinctes pour la lecture, l'écriture et la gestion des rôles.

Mauvaise utilisation des classes d'aide

Évite de regrouper plusieurs tâches dans des classes « d'aide ». Décompose-les plutôt en composants spécialisés. Exemple : remplace une classe DataProcessorHelper par des classes distinctes DataValidator, DataFormatter et Logger.

Abstractions inutiles

Ne crée des interfaces que lorsqu’elles apportent une valeur ajoutée. Une simple classe StringFormatter n’a pas besoin d’interface : cela ajoute de la complexité sans apporter d’avantage.

Garde l'équilibre

N'abuse pas d'un seul principe. Une séparation excessive crée une complexité inutile. Une séparation insuffisante rend le code rigide. Laisse les principes SOLID guider tes décisions de conception sans les contraindre.

Suivre ces directives aide à créer un code maintenable et évolutif.

Pourquoi les principes SOLID sont-ils importants pour une conception logicielle propre et modulaire

Écrire du code maintenable est essentiel. Les principes SOLID facilitent la modification et l'évolutivité de ton code. Voici comment :

Ton code doit s'adapter à mesure que ton projet évolue. Grâce aux principes d'ouverture-fermeture et d'inversion des dépendances, tu peux ajouter des fonctionnalités sans perturber celles qui existent déjà.

Par exemple : imaginons que tu développes un site de commerce électronique qui gère le calcul des taxes. En utilisant ces principes, tu peux ajouter de nouvelles règles fiscales pour différents pays sans modifier le code central de ton processus de paiement.

Voyons cela en pratique :

// Example demonstrating violation of Open-Closed Principle (OCP)
// ❌ Bad Practice: This implementation requires modifying existing code to add new shapes
class Shape {
    // This method uses conditional logic that must be modified for each new shape type
    // Breaking OCP as the class must be modified to extend functionality
    public function calculateArea($type, $dimensions) {
        if ($type === 'circle') {
            return pi() * pow($dimensions['radius'], 2);
        } elseif ($type === 'square') {
            return pow($dimensions['side'], 2);
        }
    }
}

// Example demonstrating proper implementation following OCP
// ✅ Good Practice: Using polymorphism to allow extension without modification

// Abstract base class defines the interface that all shapes must implement
// This creates a contract that concrete classes must fulfill
abstract class Shape {
    // Abstract method forces all child classes to implement their own area calculation
    abstract public function calculateArea();
}

// Concrete implementation for Circle
// Each shape is responsible for its own area calculation
class Circle extends Shape {
    private $radius;
    
    // Constructor initializes the circle with its radius
    public function __construct($radius) {
        $this->radius = $radius;
    }
    
    // Specific implementation for calculating circle area
    // π * r²
    public function calculateArea() {
        return pi() * pow($this->radius, 2);
    }
}

// Concrete implementation for Square
// New shapes can be added by creating new classes without modifying existing code
class Square extends Shape {
    private $side;
    
    // Constructor initializes the square with its side length
    public function __construct($side) {
        $this->side = $side;
    }
    
    // Specific implementation for calculating square area
    // side * side
    public function calculateArea() {
        return pow($this->side, 2);
    }
}
// To add a new shape, simply create a new class that extends Shape
// This follows OCP as existing code remains unchanged
  • Modularité de conception - SOLID crée des composants réutilisables. Un module d'authentification bien structuré fonctionne dans plusieurs projets sans modification.
  • Orientation é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 composants petits et ciblés se déploient et s'adaptent facilement dans les environnements cloud et les microservices.

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

Upsun te fournit les outils dont tu as besoin pour écrire un meilleur code, que tu développes des monolithes ou des microservices.

Tests modulaires

Évolue sans friction

  • Concentre-toi sur le code pendant que l'infrastructure s'adapte automatiquement
  • Utilise des processus Git qui prennent en charge des modifications propres et extensibles

Structure du code

  • Profil et décompose efficacement le code monolithique

Dépendances

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

Mettre en pratique les principes SOLID

Voici comment appliquer efficacement les principes SOLID :

  1. Décompose les grandes classes en composants ciblés : chaque composant doit effectuer une tâche spécifique. Cela facilite les tests et la maintenance.
  2. Apporte des améliorations progressives : refactorise ton code petit à petit. Évite les réécritures complètes.
  3. Teste en continu : exécute des tests après chaque modification pour détecter les problèmes dès leur apparition.

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

 

Restez informé

Abonnez-vous à notre newsletter mensuelle pour les dernières mises à jour et nouvelles.

Votre meilleur travail
est à l'horizon

Essai gratuit