🛡️ 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 :
-
Si la logique métier (calcul de l’âge du chien) change.
-
Si la gestion des propriétés de base change.
-
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
-
Animal: Gère uniquement les propriétés de base. -
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.
-
Créer une méthode abstraite ou une interface pour le calcul.
-
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.
-
Créer une interface
IVolantet une interfaceINageur. -
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 :
-
Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre d’abstractions.
-
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).
-
Créer une abstraction pour la persistance (
IAnimalRepository). -
Le module de bas niveau implémente cette abstraction.
-
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);
}
}