• Développement
·

Temps de lecture : 9 min

 Publié le

Les Principes SOLID

Elegant vector graphic of two abstract shapes, interconnected with smooth curves, signifying the parent and child classes for the Liskov Substitution

En programmation informatique, SOLID est un acronyme représentant cinq principes de base pour la programmation orientée objet. Ces cinq principes sont censés apporter une ligne directrice permettant le développement de logiciels plus fiables, plus robustes, plus maintenables, plus extensibles et plus testables.

Single Responsability Principle

Single Responsability Principle ou « principe de responsabilité unique » qui stipule qu'une classe doit avoir une et une seule responsabilité. Si l'on parvient à identifier dans une classe au moins deux raisons valables de la modifier, c'est que cette classe dispose d'au moins deux responsabilités.

Pour illustrer ce concept, considérons le processus de création d'un document et de sa distribution sous forme de PDF. Ce processus peut être divisé en deux grandes tâches :

  • Génération du Texte : La première tâche concerne la création du contenu du document. Cela implique de rédiger le texte, de structurer l'information et de s'assurer que le message est clair et conforme à l'objectif visé. Les raisons de modifier cette partie peuvent inclure la mise à jour du contenu, la correction d'erreurs ou l'amélioration de la clarté du texte.
  • Conversion en PDF : La seconde tâche est la conversion du document texte en format PDF. Cette étape implique de choisir la mise en page, de définir les marges, de sélectionner les polices et d'autres paramètres esthétiques pour s'assurer que le document est non seulement lisible, mais aussi présentable dans son format final. Les raisons de modifier cette partie peuvent être liées à des préférences esthétiques, à des exigences de formatage spécifiques ou à l'adaptation à différents supports de lecture.

Selon le Principe de Responsabilité Unique, ces deux tâches devraient être prises en charge par des classes ou des modules séparés. Cela signifie que la classe gérant la génération du texte ne devrait pas se préoccuper de la conversion du document en PDF, et vice-versa. Cette séparation des responsabilités assure que les modifications apportées à la génération du texte n'affectent pas directement le processus de conversion en PDF, rendant chaque composant plus simple à maintenir, à tester et à améliorer.

En adoptant cette approche, si le contenu du document doit être mis à jour, seule la partie responsable de la génération du texte nécessite des modifications. De même, si les préférences de mise en forme PDF changent, seules les modifications dans le module de conversion en PDF sont nécessaires. Cela réduit les risques d'erreurs et facilite la gestion du code, tout en permettant une plus grande flexibilité pour les évolutions futures.

Ainsi, l'application du Principe de Responsabilité Unique dans cet exemple de la génération de texte et de sa conversion en PDF illustre comment diviser les tâches en responsabilités distinctes permet d'obtenir un code plus propre, plus modulable et plus facile à gérer.

Open Close Principle

Open Close Principle ou « principe Ouvert / Fermé » qui stipule qu'une classe doit être ouverte à l'extension et fermée aux modifications. En d'autres termes, il doit être possible de facilement enrichir les capacités d'un objet sans que cela implique de modifier le code source de sa classe. Des patrons de conception comme la Stratégie ou le Décorateur répondent à ce principe en favorisant la composition d'objets plutôt que l'héritage.

Une classe, une méthode, un module doit pouvoir être étendu, supporter différentes implémentations (Open for extension) sans pour cela devoir être modifié (closed for modification).

Pour illustrer ce principe, considérons la classe Velo, qui pourrait être équipée de différents types de pédaliers. Plutôt que de modifier la classe Velo chaque fois que nous voulons ajouter un nouveau type de pédalier, nous pouvons la concevoir de manière à ce qu'elle accepte dynamiquement le type de pédalier sans avoir besoin de modification.

Voici un exemple de violation de l'OCP:

        public class Bike {
    private String pedalType;

    public Bike(String pedalType) {
        this.pedalType = pedalType;
        if (pedalType.equals("classic")) {
            // Configuration pour un pédalier classique
        } else if (pedalType.equals("electric")) {
            // Configuration pour un pédalier électrique
        }
    }
}
    

Dans cet exemple, l'ajout d'un nouveau type de pédalier nécessiterait une modification de la classe Bike, en ajoutant une nouvelle condition dans le constructeur, ce qui viole l'OCP.

Pour respecter l'OCP, nous pouvons utiliser l'injection de dépendance, permettant de passer un objet pédalier lors de la création d'un objet Velo. Voici comment cela pourrait être implémenté:

        interface Pedal {
    void configure();
}

class ClassicPedal implements Pedal {
    public void configure() {
        // Configuration pour un pédalier classique
    }
}

class ElectricPedal implements Pedal {
    public void configure() {
        // Configuration pour un pédalier électrique
    }
}

public class Bike {
    private Pedal pedal;

    public Bike(Pedal pedal) {
        this.pedal = pedal;
        this.pedal.configure();
    }
}
    

Dans cet exemple, la classe Bike est conçue pour accepter n'importe quel objet qui implémente l'interface Pedal. Cette approche permet d'étendre facilement les capacités du vélo (en ajoutant de nouveaux types de pédaliers) sans modifier la classe Bike elle-même. Si un nouveau type de pédalier est introduit, il suffit de créer une nouvelle classe qui implémente Pedal et de l'injecter lors de la création d'une instance de Bike. Cette méthode assure que la classe Bike est ouverte à l'extension mais fermée à la modification, respectant ainsi le Principe Ouvert/Fermé.

Liskov Substitution Principle

Liskov Substitution Principle ou « principe de substitution de Liskov » qui définit qu'une instance de type T doit pouvoir être remplacée par une instance de type G, tel que G sous-type de T, sans que cela modifie la cohérence du programme. En d'autres termes, il s'agit de conserver les mêmes prototypes ainsi que les mêmes conditions d'entrée / sortie lorsqu'une classe dérive une classe parente et redéfinit ses méthodes. Cela vaut aussi pour les types d'exceptions. Si une méthode de la classe parente lève une exception de type E alors cette même méthode redéfinie dans une sous-classe C' qui hérite de C doit aussi lever une exception de type E dans les mêmes conditions.

L'exemple classique d'une violation du LSP est la suivante :

Soit une classe Rectangle représentant les propriétés d'un rectangle : hauteur, largeur. On lui associe donc des accesseurs pour accéder et modifier la hauteur et la largeur librement. En postcondition, on définit la règle : la hauteur et la largeur sont librement modifiables.Soit une classe Carré que l'on fait dériver de la classe Rectangle. En effet, en mathématiques, un carré est un rectangle. Donc, on définit naturellement la classe Carré comme sous-type de la classe Rectangle. On définit comme postcondition la règle : les « quatre côtés du carré doivent être égaux ».On s'attend à pouvoir utiliser une instance de type Carré n'importe où un type Rectangle est attendu.

Problème : Un carré ayant par définition quatre côtés égaux, il convient de restreindre la modification de la hauteur et de la largeur pour qu'elles soient toujours égales. Néanmoins, si un carré est utilisé là où, comportementalement, on s'attend à interagir avec un rectangle, des comportements incohérents peuvent subvenir : les côtés d'un carré ne peuvent être changés indépendamment, contrairement à ceux d'un rectangle. Une mauvaise solution consisterait à modifier les setter du carré pour préserver l'invariance de ce dernier. Mais ceci violerait la postcondition des setter du rectangle qui spécifie que l'on puisse modifier hauteur et largeur indépendamment.

Une solution pour éviter ces incohérences est de retirer la nature Mutable des classes Carré et Rectangle. Autrement dit, elles ne sont accessibles qu'en lecture. Il n'y a aucune violation du LSP, néanmoins on devra implémenter des méthodes "hauteur" et "largeur" à un carré, ce qui, sémantiquement, est un non sens.

Solution : La solution consiste à ne pas considérer un type Carré comme substitut d'un type Rectangle, et les définir comme deux types complètement indépendants. Ceci ne contredit pas le fait qu'un carré soit un rectangle. La classe Carré est un représentant du concept « carré ». La classe Rectangle est un représentant du concept « rectangle ». Or, les représentants ne partagent pas les mêmes propriétés que ce qu'ils représentent.

Interface Segregation Principle

Interface Segregation Principle ou « principe de ségrégation d'interface » qui recommande de découper de grosses interfaces en plus petites interfaces spécialisées. Ainsi les classes clientes peuvent implémenter une ou plusieurs petites interfaces spécialisées plutôt qu'une grosse interface afin d'obtenir seulement les méthodes dont elles ont besoin.

Le Principe de Ségrégation des Interfaces (ISP) est un concept crucial en programmation orientée objet, qui encourage la création d'interfaces spécifiques pour des besoins fonctionnels distincts, au lieu d'utiliser une interface générique pour plusieurs fonctionnalités. Ce principe aide à réduire le couplage entre les composants logiciels, permettant ainsi une plus grande flexibilité et facilité de maintenance.

Pour illustrer ce principe, prenons l'exemple d'un restaurant proposant un menu complet avec entrées, plats principaux, desserts, options véganes, et menus enfants. Plutôt que de présenter tous ces éléments sur une seule et même page de menu, ce qui pourrait rendre la lecture difficile et la sélection des plats peu pratique, l'ISP suggère une approche plus structurée.

Imaginons maintenant que le menu soit divisé en plusieurs sections ou interfaces distinctes : une pour les entrées, une pour les plats principaux, une pour les desserts, une autre pour les options véganes, et une dernière pour le menu enfants. Chaque section répond à un besoin spécifique du client et simplifie sa recherche. Un client intéressé uniquement par les options véganes n'aurait pas à parcourir tout le menu pour trouver ce qu'il cherche ; il pourrait directement consulter la section dédiée.

Appliqué au développement logiciel, ce principe signifie qu'au lieu d'avoir une interface unique qui regroupe toutes les méthodes dont différents clients pourraient avoir besoin, il est préférable de séparer ces méthodes en interfaces plus petites et plus ciblées. Cela permet aux clients (dans ce cas, d'autres classes ou composants du logiciel) de dépendre uniquement des méthodes qui leur sont réellement utiles, sans être forcés d'implémenter ou d'interagir avec des méthodes inutiles pour eux.

Cependant, comme dans l'exemple du menu de restaurant, il est important de ne pas tomber dans l'excès inverse en créant une interface pour chaque petite fonctionnalité ou méthode, ce qui pourrait entraîner une complexité inutile et une fragmentation excessive du code. La clé est de trouver un équilibre, en regroupant logiquement les fonctionnalités en interfaces de manière à ce qu'elles servent les besoins fonctionnels sans surcharger les clients avec des dépendances inutiles. La modération, l'expérience et le pragmatisme sont essentiels pour appliquer efficacement ce principe.

Dependency Inversion Principle

Dependency Inversion Principle ou « principe d'inversion des dépendances » qui stipule que les objets doivent dépendre d'abstractions plutôt que d'implémentations. Cela signifie qu'il est préférable de typer des arguments avec des types abstraits (classes concrètes ou interfaces) plutôt que des types concrets (classes concrètes) afin de diminuer les couplages et favoriser d'autres implémentations.

Attardons-nous sur la notion importante de ce principe : Inversion. Le principe de DIP stipule que les modules de haut niveau ne doivent pas dépendre de modules de plus bas niveau. Mais pour quelle raison ? Pour répondre à cette question, prenons la définition à l’envers : les modules de haut niveau dépendent de modules de bas niveau. En règle générale les modules de haut niveau contiennent le cœur – business – des applications. Lorsque ces modules dépendent de modules de plus bas niveau, les modifications effectuées dans les modules « bas niveau » peuvent avoir des répercussions sur les modules « haut niveau » et les « forcer » à appliquer des changements.

Conclusion

Pour savoir si le code d’une application ne respecte pas les principes SOLID et s’il est nécessaire de les appliquer, il est possible d’utiliser les métriques structurelles d’instabilité (relative au couplage) et d’abstraction pour calculer le rapport entre le degré d’utilisation d’un module et son niveau d’abstraction. Un module sera d’autant plus stable qu’il aura de dépendances entrantes par rapport à ses dépendances sortantes.

Le « Stable Abstraction Principle » part du principe que les packages stables doivent être abstraits.

Un module majoritairement utilisé par d’autres (défini comme stable) devra avoir un niveau d’abstraction plus important. Inversement, un module qui n’est utilisé par aucun autre, n’aura aucun intérêt à être abstrait.