Récemment, nous avons couvert les 30 meilleures questions d'entretien Java, et aujourd'hui, nous voulons approfondir les principes SOLID. Ces principes, inventés par le gourou du logiciel Robert C. Martin (alias Uncle Bob), sont :

  • Principe de responsabilité unique (SRP)
  • Principe ouvert/fermé (OCP)
  • Principe de substitution de Liskov (LSP)
  • Principe de ségrégation des interfaces (ISP)
  • Principe d'inversion des dépendances (DIP)

Mais pourquoi devriez-vous vous en soucier ? Imaginez que vous construisez une tour en Lego. Les principes SOLID sont comme le manuel d'instructions qui garantit que votre tour ne s'effondre pas lorsque vous ajoutez de nouvelles pièces. Ils rendent votre code :

  • Plus lisible (votre futur vous vous en remerciera)
  • Plus facile à maintenir et à modifier
  • Plus robuste face aux changements de besoins
  • Moins sujet aux bugs lorsque vous ajoutez de nouvelles fonctionnalités

Ça a l'air bien, non ? Décomposons chaque principe et voyons comment ils fonctionnent en pratique.

Principe de responsabilité unique (SRP) : Une tâche, une classe

Le principe de responsabilité unique est comme le Marie Kondo de la programmation - il s'agit de désencombrer vos classes. L'idée est simple : une classe doit avoir une, et une seule, raison de changer.

Voyons une violation classique du SRP :


public class Report {
    public void generateReport() {
        // Générer le contenu du rapport
    }

    public void saveToDatabase() {
        // Enregistrer le rapport dans la base de données
    }

    public void sendEmail() {
        // Envoyer le rapport par email
    }
}

Cette classe Report en fait beaucoup trop. Elle génère le rapport, l'enregistre et l'envoie. C'est comme un couteau suisse - pratique, mais pas idéal pour une tâche spécifique.

Réorganisons cela pour suivre le SRP :


public class ReportGenerator {
    public String generateReport() {
        // Générer et retourner le contenu du rapport
    }
}

public class DatabaseSaver {
    public void saveToDatabase(String report) {
        // Enregistrer le rapport dans la base de données
    }
}

public class EmailSender {
    public void sendEmail(String report) {
        // Envoyer le rapport par email
    }
}

Maintenant, chaque classe a une responsabilité unique. Si nous devons changer la façon dont les rapports sont générés, nous ne touchons qu'à la classe ReportGenerator. Si le schéma de la base de données change, nous mettons à jour uniquement DatabaseSaver. Cette séparation rend notre code plus modulaire et plus facile à maintenir.

Principe ouvert/fermé (OCP) : Ouvert à l'extension, fermé à la modification

Le principe ouvert/fermé semble être un paradoxe, mais il est en fait assez astucieux. Il stipule que les entités logicielles (classes, modules, fonctions, etc.) doivent être ouvertes à l'extension, mais fermées à la modification. En d'autres termes, vous devriez pouvoir étendre le comportement d'une classe sans modifier son code existant.

Voyons une violation courante de l'OCP :


public class PaymentProcessor {
    public void processPayment(String paymentMethod) {
        if (paymentMethod.equals("creditCard")) {
            // Traiter le paiement par carte de crédit
        } else if (paymentMethod.equals("paypal")) {
            // Traiter le paiement par PayPal
        }
        // Plus de méthodes de paiement...
    }
}

Chaque fois que nous voulons ajouter une nouvelle méthode de paiement, nous devons modifier cette classe. C'est une recette pour les bugs et les maux de tête.

Voici comment nous pouvons réorganiser cela pour suivre l'OCP :


public interface PaymentMethod {
    void processPayment();
}

public class CreditCardPayment implements PaymentMethod {
    public void processPayment() {
        // Traiter le paiement par carte de crédit
    }
}

public class PayPalPayment implements PaymentMethod {
    public void processPayment() {
        // Traiter le paiement par PayPal
    }
}

public class PaymentProcessor {
    public void processPayment(PaymentMethod paymentMethod) {
        paymentMethod.processPayment();
    }
}

Maintenant, lorsque nous voulons ajouter une nouvelle méthode de paiement, nous créons simplement une nouvelle classe qui implémente PaymentMethod. La classe PaymentProcessor n'a pas besoin de changer du tout. C'est la puissance de l'OCP !

Principe de substitution de Liskov (LSP) : Si ça ressemble à un canard et que ça cancane comme un canard, ça doit être un canard

Le principe de substitution de Liskov, nommé d'après la scientifique informatique Barbara Liskov, stipule que les objets d'une superclasse doivent pouvoir être remplacés par des objets de ses sous-classes sans affecter la correction du programme. En termes plus simples, si la classe B est une sous-classe de la classe A, nous devrions pouvoir utiliser B partout où nous utilisons A sans que cela ne pose de problème.

Voici un exemple classique de violation du LSP :


public class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

public class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    @Override
    public void setHeight(int height) {
        super.setHeight(height);
        super.setWidth(height);
    }
}

Cela semble logique à première vue - un carré est un type spécial de rectangle, non ? Mais cela viole le LSP car vous ne pouvez pas utiliser un Square partout où vous utilisez un Rectangle sans comportement inattendu. Si vous définissez la largeur et la hauteur d'un Square séparément, vous obtiendrez des résultats inattendus.

Une meilleure approche serait d'utiliser la composition au lieu de l'héritage :


public interface Shape {
    int getArea();
}

public class Rectangle implements Shape {
    private int width;
    private int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

public class Square implements Shape {
    private int side;

    public Square(int side) {
        this.side = side;
    }

    public int getArea() {
        return side * side;
    }
}

Maintenant, Square et Rectangle sont des implémentations distinctes de l'interface Shape, et nous évitons la violation du LSP.

Principe de ségrégation des interfaces (ISP) : Petit est beau

Le principe de ségrégation des interfaces stipule qu'aucun client ne doit être forcé de dépendre de méthodes qu'il n'utilise pas. En d'autres termes, ne créez pas d'interfaces volumineuses ; divisez-les en interfaces plus petites et plus ciblées.

Voici un exemple d'interface surchargée :


public interface Worker {
    void work();
    void eat();
    void sleep();
}

public class Human implements Worker {
    public void work() { /* ... */ }
    public void eat() { /* ... */ }
    public void sleep() { /* ... */ }
}

public class Robot implements Worker {
    public void work() { /* ... */ }
    public void eat() { throw new UnsupportedOperationException(); }
    public void sleep() { throw new UnsupportedOperationException(); }
}

La classe Robot est forcée d'implémenter des méthodes dont elle n'a pas besoin. Corrigeons cela en séparant l'interface :


public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public interface Sleepable {
    void sleep();
}

public class Human implements Workable, Eatable, Sleepable {
    public void work() { /* ... */ }
    public void eat() { /* ... */ }
    public void sleep() { /* ... */ }
}

public class Robot implements Workable {
    public void work() { /* ... */ }
}

Maintenant, notre Robot n'implémente que ce dont il a besoin. Cela rend notre code plus flexible et moins sujet aux erreurs.

Principe d'inversion des dépendances (DIP) : Les modules de haut niveau ne devraient pas dépendre des modules de bas niveau

Le principe d'inversion des dépendances peut sembler complexe, mais il est en fait assez simple. Il stipule que :

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

Voici un exemple de violation du DIP :


public class LightBulb {
    public void turnOn() {
        // Allumer l'ampoule
    }

    public void turnOff() {
        // Éteindre l'ampoule
    }
}

public class Switch {
    private LightBulb bulb;

    public Switch() {
        bulb = new LightBulb();
    }

    public void operate() {
        // Logique de l'interrupteur
    }
}

Dans cet exemple, la classe Switch (module de haut niveau) dépend directement de la classe LightBulb (module de bas niveau). Cela rend difficile le changement de l'Switch pour contrôler d'autres appareils.

Réorganisons cela pour suivre le DIP :


public interface Switchable {
    void turnOn();
    void turnOff();
}

public class LightBulb implements Switchable {
    public void turnOn() {
        // Allumer l'ampoule
    }

    public void turnOff() {
        // Éteindre l'ampoule
    }
}

public class Switch {
    private Switchable device;

    public Switch(Switchable device) {
        this.device = device;
    }

    public void operate() {
        // Logique de l'interrupteur utilisant device.turnOn() et device.turnOff()
    }
}

Maintenant, Switch et LightBulb dépendent tous deux de l'abstraction Switchable. Nous pouvons facilement étendre cela pour contrôler d'autres appareils sans changer la classe Switch.

Conclusion : SOLID comme un roc

Les principes SOLID peuvent sembler beaucoup à assimiler au début, mais ce sont des outils incroyablement puissants dans votre boîte à outils POO. Ils vous aident à écrire du code qui est :

  • Plus facile à comprendre et à maintenir
  • Plus flexible et adaptable aux changements
  • Moins sujet aux bugs lors de l'ajout de nouvelles fonctionnalités

Rappelez-vous, SOLID n'est pas un ensemble strict de règles, mais plutôt un guide pour vous aider à prendre de meilleures décisions de conception. En appliquant ces principes dans votre codage quotidien, vous commencerez à voir des motifs émerger, et votre code deviendra naturellement plus robuste et plus facile à maintenir.

Alors, la prochaine fois que vous concevez une classe ou que vous refactorisez du code, demandez-vous : "Est-ce que c'est SOLID ?" Votre futur vous (et votre équipe) vous en remerciera !

"Le secret pour construire de grandes applications est de ne jamais construire de grandes applications. Divisez vos applications en petits morceaux. Ensuite, assemblez ces morceaux testables et de petite taille dans votre grande application" - Justin Meyer

Bon codage, et que votre code soit toujours SOLID !