🛡️ Les Principes SOLID – Correction par l’Exemple

Voici une classe Java Animal que l’on va corriger avec les principe de SOLID :

public class Animal {
    private String nom;
    private int ageHumain; // L'âge de l'animal en années humaines
    private String espece; // Utilisé pour le calcul et les comportements

    public Animal(String nom, int ageHumain, String espece) {
        this.nom = nom;
        this.ageHumain = ageHumain;
        this.espece = espece;
    }

    public String getNom() {
        return nom;
    }

    public int getAgeHumain() {
        return ageHumain;
    }

    public String getEspece() {
        return espece;
    }

    /**
     * Calcule l'âge d'un animal en "années de chien".
     */
    public int calculerAgeEnAnneeDeChien() {
        if (this.espece.equalsIgnoreCase("chien")) {
            return ageHumain * 7;
        } else {
            System.out.println("ATTENTION: Calcul non pertinent pour l'espèce " + this.espece);
            return ageHumain;
        }
    }

    public void voler() {
        if (this.espece.equalsIgnoreCase("oiseau")) {
            System.out.println(nom + " prend son envol. Vol en cours !");
        } else {
            // Problème si on a un Pingouin (oiseau qui ne vole pas) : 
            System.out.println(nom + " essaie de voler... Échec.");
        }
    }

    public void nager() {
        if (this.espece.equalsIgnoreCase("poisson") || this.espece.equalsIgnoreCase("pingouin")) {
            System.out.println(nom + " nage dans l'eau avec aisance.");
        } else {
            System.out.println(nom + " essaie de nager, c'est difficile.");
        }
    }

    public void enregistrerDansBaseDeDonnees() {
        // ... code pour se connecter à une DB MySQL et sauver l'objet.
    }

}

4.1. S : Single Responsibility Principle (SRP) – Responsabilité Unique

Le SRP stipule qu’une classe ne doit avoir qu’une seule raison de changer. Or la classe Animal a plusieurs dépendances et responsabilités (état, logique métier, persistance).

@startuml
!theme sunlust
class Animal {
  - nom: String
  - ageHumain: int
  + calculerAgeEnAnneeDeChien()
  + voler()
  + nager()
  + enregistrerDansBaseDeDonnees()
}
note left of Animal::enregistrerDansBaseDeDonnees
  Dépendance à la BD
end note
note right of Animal::calculerAgeEnAnneeDeChien
  Logique de calcul
end note
@enduml

4.1.1. 💥 Violation du SRP (Classe Animal Maladroite)

La classe initiale Animal avait plusieurs raisons de changer :

  1. Si la logique métier (calcul de l’âge du chien) change.

  2. Si la gestion des propriétés de base change.

  3. Si la méthode de persistence des données change (où on enregistre l’animal).

// CODE MALADROIT AVANT CORRECTION
public class Animal {
    // ... Propriétés (gestion de l'état)

    // Logique métier pour l'âge (Responsabilité 1)
    public int calculerAgeEnAnneeDeChien() { /* ... */ } 

    // Comportements (Responsabilité 2, à revoir avec LSP/ISP)
    public void voler() { /* ... */ } 

    // Sauvegarde des données (Responsabilité 3 - supposée)
    public void enregistrerDansBaseDeDonnees() { /* ... */ } 
}

4.1.2. ✅ Correction du SRP

Nous séparons les responsabilités en classes distinctes.

@startuml
!theme sunlust
class Animal {
  - nom: String
  - ageHumain: int
}
class CalculateurAge {
  + calculerAgeEnAnneeDeChien(animal: Animal): int
}
class AnimalRepository {
  + enregistrer(animal: Animal)
}

CalculateurAge .> Animal : utilise
AnimalRepository .> Animal : gère
@enduml
  1. Animal : Gère uniquement les propriétés de base.

  2. CalculateurAge : Gère uniquement la logique métier.

// 1. La classe Animal ne gère que les propriétés (son état)
public class Animal {
    private String nom;
    private int ageHumain;

    public Animal(String nom, int ageHumain) {
        this.nom = nom;
        this.ageHumain = ageHumain;
    }
    // Getters et Setters...
}

// 2. Le Calculateur gère la logique métier (sa propre raison de changer)
public class CalculateurAge {
    /**
     * Une classe statique pour l'instant (mais le SRP est respecté)
     */
    public static int calculerAgeEnAnneeDeChien(Animal animal) {
        // La logique est ici, elle peut changer sans affecter la classe Animal
        return animal.getAgeHumain() * 7;
    }
}

4.2. O : Open/Closed Principle (OCP) – Ouvert/Fermé

Le OCP stipule qu’une entité logicielle doit être ouverte à l’extension mais fermée à la modification.

4.2.1. 💥 Violation de l’OCP

Si nous voulons ajouter une nouvelle espèce (Chat) avec une nouvelle règle de calcul d’âge, nous devrions modifier le CalculateurAge existant.

@startuml
!theme sunlust
class CalculateurAge {
  + calculerAge(animal: Animal, espece: String): int
}
class Animal {
  - espece: String
}
note right of CalculateurAge::calculerAge
  if (espece == "Chien") { ... }
  // Nouvelle condition pour "Chat" oblige à modifier
end note

CalculateurAge .> Animal
@enduml
// VIOLATION : Si on ajoute un Chat, on doit modifier cette méthode !
public class CalculateurAge {
    public static int calculerAge(Animal animal, String espece) {
        if (espece.equals("Chien")) {
            return animal.getAgeHumain() * 7;
        } else if (espece.equals("Chat")) { // 🛑 MODIFICATION NÉCESSAIRE
            return animal.getAgeHumain() * 4;
        }
        return animal.getAgeHumain();
    }
}

4.2.2. ✅ Correction de l’OCP

Nous utilisons le Polymorphisme et l’Héritage. L’ajout d’une nouvelle espèce se fait par extension (création d’une nouvelle classe), et non par modification du code existant.

  1. Créer une méthode abstraite ou une interface pour le calcul.

  2. Chaque classe fille implémente son propre calcul.

@startuml
!theme sunlust
abstract class Animal {
  + calculerAgeEquiv() : int <<abstract>>
}
class Chien {
  + calculerAgeEquiv() : int
}
class Chat {
  + calculerAgeEquiv() : int
}

Animal <|-- Chien
Animal <|-- Chat
@enduml
// 1. Classe de base ouverte à l'extension
public abstract class Animal {
    private String nom;
    private int ageHumain;

    public abstract int calculerAgeEquiv(); // Méthode abstraite, ouverte à l'extension

    // Getters/Setters...
}

// 2. Les classes concrètes héritent et étendent (fermé à la modification pour la classe Animal)
public class Chien extends Animal {
    @Override
    public int calculerAgeEquiv() {
        return getAgeHumain() * 7; // Logique propre au Chien
    }
}

public class Chat extends Animal {
    @Override
    public int calculerAgeEquiv() {
        return getAgeHumain() * 4; // Logique propre au Chat
    }
}

// L'application peut maintenant utiliser le polymorphisme sans modification
public void afficherAge(Animal animal) {
    // Le code utilise la méthode abstraite, il est fermé à la modification
    System.out.println(animal.getNom() + " : " + animal.calculerAgeEquiv() + " ans."); 
}

4.3. L : Liskov Substitution Principle (LSP) – Substitution de Liskov

Le LSP stipule que les objets d’un programme doivent pouvoir être remplacés par des instances de leurs sous-types sans altérer le bon fonctionnement.

4.3.1. 💥 Violation du LSP

Ceci se produit souvent lorsque l’héritage est utilisé pour des raisons autres que la substitution.

Problème : Un Pingouin est biologiquement un Oiseau, mais il ne vole pas. Si on crée une hiérarchie Animal -> Oiseau -> Pingouin, et que la classe Oiseau a une méthode voler(), le Pingouin brise le contrat.

@startuml
!theme sunlust
interface IAnimalActions {
  + voler()
  + nager()
  + marcher()
}
class Pingouin {
  + voler() { throw }
  + nager()
  + marcher()
}

IAnimalActions <|.. Pingouin
note bottom of Pingouin
  Violation LSP : voler() lève une erreur.
  Violation ISP : forcé d'implémenter voler().
end note
@enduml
public class Oiseau extends Animal {
    public void voler() {
        System.out.println("L'oiseau vole !");
    }
}

public class Pingouin extends Oiseau {
    @Override
    public void voler() {
        // 🛑 Violation : Le comportement est altéré, la substitution échoue !
        throw new UnsupportedOperationException("Les pingouins ne volent pas !");
    }
}
// Code client : on s'attend à ce que tout Oiseau puisse voler, mais le Pingouin génère une erreur.
// for (Oiseau o : listeOiseaux) { o.voler(); } // Le programme s'arrête !

4.3.2. ✅ Correction du LSP

Nous corrigeons la hiérarchie en nous basant sur les capacités et non sur la classification biologique. On utilise l’Interface Segregation Principle (ISP) de manière précoce.

  1. Créer une interface IVolant et une interface INageur.

  2. Les classes n’héritent pas du comportement ; elles implémentent l’interface correspondante.

@startuml
!theme sunlust
interface IVolant {
  + voler()
}
interface INageur {
  + nager()
}

class Aigle {
}
class Pingouin {
}

IVolant <|.. Aigle
INageur <|.. Aigle
INageur <|.. Pingouin

@enduml
// On supprime la méthode voler() de la classe Animal de base.

// Interface pour le comportement (Capacité)
public interface IVolant {
    void voler();
}
public interface INageur {
    void nager();
}

// Aigle implémente les capacités
public class Aigle extends Animal implements IVolant {
    @Override
    public void voler() { /* ... code de vol de l'Aigle ... */ }
}

// Pingouin implémente seulement la capacité de nager
public class Pingouin extends Animal implements INageur {
    @Override
    public void nager() { /* ... code de nage du Pingouin ... */ }
    // 🛑 Pas de méthode voler(), le contrat est respecté
}

// Le code client est ciblé :
public void faireVoler(IVolant oiseauVolant) { 
    oiseauVolant.voler(); // On est sûr que ça fonctionne !
}

4.4. I : Interface Segregation Principle (ISP) – Ségrégation des Interfaces

Le ISP stipule qu’un client ne devrait pas dépendre d’interfaces qu’il n’utilise pas. En d’autres termes, les interfaces doivent être fines et spécifiques.

4.4.1. 💥 Violation de l’ISP

La violation est démontrée dans la correction du LSP si l’on n’avait pas découpé les interfaces.

// INTERFACE MALADROITE
public interface IAnimalActions {
    void voler();
    void nager();
    void marcher();
    void pondre();
}

public class Chien implements IAnimalActions {
    // Le Chien est forcé d'implémenter :
    public void voler() { /* ne fait rien */ }   // 🛑 Non utilisé
    public void pondre() { /* ne fait rien */ }  // 🛑 Non utilisé
    // ... et potentiellement violer le LSP.
}

4.4.2. ✅ Correction de l’ISP

Le découpage des interfaces a déjà été effectué dans la solution du LSP, mais le voici formellement.

// Interfaces fines et spécifiques
public interface IVolant {
    void voler();
}

public interface INageur {
    void nager();
}

public interface IPondeur {
    void pondre();
}

// Le client Chien n'implémente que ce dont il a besoin
public class Chien extends Animal implements INageur { // Le chien nage un peu
    @Override
    public void nager() { /* ... */ }
}

4.5. D : Dependency Inversion Principle (DIP) – Inversion des Dépendances

Le DIP stipule que :

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

  2. Les abstractions ne doivent pas dépendre des détails. Les détails doivent dépendre des abstractions.

4.5.1. 💥 Violation du DIP

Le module de haut niveau (Zoo) dépend directement de la classe concrète de bas niveau (MySQLConnexion).

@startuml
!theme sunlust
class Zoo {
  - db: MySQLConnexion
  + ajouterAnimal(a: Animal)
}
class MySQLConnexion {
  + save(a: Animal)
}

Zoo --> MySQLConnexion : dépend concrètement de
@enduml
// MODULE DE BAS NIVEAU (le détail)
public class MySQLConnexion {
    public void save(Animal a) { /* connexion à MySQL... */ }
}

// MODULE DE HAUT NIVEAU (la logique)
public class Zoo {
    // 🛑 Le Zoo dépend directement du détail MySQL !
    private MySQLConnexion db; 

    public Zoo() {
        this.db = new MySQLConnexion(); // Création de la dépendance ici
    }

    public void ajouterAnimal(Animal a) {
        db.save(a);
    }
    // Si on change de base de données (vers PostgreSQL), on doit modifier la classe Zoo !
}

4.5.2. ✅ Correction du DIP

Nous inversons la dépendance : le Zoo (haut niveau) et MySQLConnexion (bas niveau) dépendent tous deux d’une Abstraction (Interface).

  1. Créer une abstraction pour la persistance (IAnimalRepository).

  2. Le module de bas niveau implémente cette abstraction.

  3. Le module de haut niveau reçoit l’implémentation via Injection de Dépendances (par le constructeur).

@startuml
!theme sunlust
interface IAnimalRepository {
  + save(a: Animal)
}

class Zoo {
  - repository: IAnimalRepository
  + Zoo(repo: IAnimalRepository)
  + ajouterAnimal(a: Animal)
}

class MySQLRepository {
  + save(a: Animal)
}

Zoo ..> IAnimalRepository : dépend de (Abstraction)
MySQLRepository .up.|> IAnimalRepository : implémente (Détail dépend de l'Abstraction)
@enduml
// 1. Abstraction (Contrat)
public interface IAnimalRepository {
    void save(Animal a);
}

// 2. Détail dépend de l'Abstraction
public class MySQLRepository implements IAnimalRepository {
    @Override
    public void save(Animal a) { 
        System.out.println("Sauvegarde de " + a.getNom() + " via MySQL.");
    }
}

// 3. Module de haut niveau dépend de l'Abstraction (via Injection)
public class Zoo {
    private IAnimalRepository repository; // Le Zoo dépend de l'Interface

    // Injection de Dépendances (via constructeur)
    public Zoo(IAnimalRepository repository) {
        this.repository = repository; // Peu importe si c'est MySQL ou PostgreSQL !
    }

    public void ajouterAnimal(Animal a) {
        repository.save(a);
    }
}