Principes SOLID
Le principe SOLID constitue un ensemble de bonnes pratiques essentielles en développement logiciel, visant à améliorer la lisibilité, l'extensibilité et la maintenabilité du code. C'est un acronyme pour cinq principes fondamentaux de programmation :
- S : Single Responsibility Principle (Principe de responsabilité unique)
- O : Open/Closed Principle (Principe ouvert/fermé)
- L : Liskov Substitution Principle (Principe de substitution de Liskov)
- I : Interface Segregation Principle (Principe de ségrégation des interfaces)
- D : Dependency Inversion Principle (Principe d'inversion des dépendances)
Single Responsibility Principle (SRP)
Le Single Responsibility Principle (SRP) stipule qu'une classe ne doit avoir qu'une seule responsabilité ou raison de changer. En d'autres termes, elle doit se concentrer sur une seule tâche spécifique.
Si vous devez décrire ce que fait une classe et que vous énumérez plusieurs tâches ou fonctionnalités, vous êtes probablement en train de violer ce principe.
L'application de ce principe permet de rendre le code plus clair et plus facile à comprendre. Le nom de la classe reflète explicitement son rôle, ce qui simplifie son évolution et sa maintenance.
Open/Closed Principle (OCP)
Le Open/Closed Principle (OCP) stipule que les classes, fonctions, méthodes, ou tout autre composant d'un système doivent être conçus de manière à être ouvertes à l'extension, mais fermées à la modification. Cela signifie que le comportement d’une entité doit pouvoir être étendu sans avoir à modifier son code source. L'idée est d'éviter de toucher au code existant, ce qui pourrait introduire des bugs dans un code fonctionnel.
Par exemple, au lieu d'utiliser des structures conditionnelles comme if ... else
ou des instanceof
pour gérer différents types d'objets, il vaut mieux recourir au polymorphisme. Chaque nouvelle fonctionnalité peut être ajoutée via une nouvelle classe ou méthode qui étend une abstraction existante sans toucher au code de base.
Exemple :
Imaginons une classe qui calcule les prix de différents types de produits. Au lieu d'utiliser des conditions pour gérer chaque type de produit, nous utiliserons des classes différentes pour chaque type de calcul.
Violation de l'OCP : Dans cet exemple, chaque fois qu'un nouveau type de produit est ajouté, il faut modifier la méthode calculatePrice
, ce qui viole le principe OCP.
class Product {
public function __construct(private string $name, private string $price)
{
}
}
class PriceCalculator {
public function calculatePrice(Product $product): int {
if ($product->name === 'Book') {
return $product->price * 1.1; // 10% de taxe sur les livres
} elseif ($product->name === 'Food') {
return $product->price * 1.05; // 5% de taxe sur la nourriture
} else {
return $product->price;
}
}
}
$product1 = new Product("Book", 100);
$product2 = new Product("Food", 50);
$calculator = new PriceCalculator();
echo $calculator->calculatePrice($product1); // Affiche 110
echo $calculator->calculatePrice($product2); // Affiche 52.5
Respect du OCP : Nous allons créer des classes de calcul de prix spécifiques pour chaque type de produit.
interface PriceCalculator {
public function calculatePrice(Product $product): int;
}
class BookPriceCalculator implements PriceCalculator {
public function calculatePrice(Product $product): int {
return $product->price * 1.1; // 10% de taxe sur les livres
}
}
class FoodPriceCalculator implements PriceCalculator {
public function calculatePrice(Product $product): int {
return $product->price * 1.05; // 5% de taxe sur la nourriture
}
}
class Product {
public function __construct(private string $name, private string $price)
{
}
}
$product1 = new Product("Book", 100);
$product2 = new Product("Food", 50);
$bookCalculator = new BookPriceCalculator();
$foodCalculator = new FoodPriceCalculator();
echo $bookCalculator->calculatePrice($product1); // Affiche 110
echo $foodCalculator->calculatePrice($product2); // Affiche 52.5
Liskov Substitution Principle (LSP)
Le Liskov Substitution Principle (LSP) stipule que les objets d'une classe dérivée doivent pouvoir remplacer les objets de la classe parente sans altérer le bon fonctionnement du programme. Autrement dit, si une classe dérivée hérite d'une classe de base, elle doit pouvoir être utilisée à la place de la classe parente sans que cela cause des erreurs ou des comportements inattendus.
Pour respecter ce principe, il est essentiel que la signature des méthodes (c’est-à-dire les paramètres et les types de retour) dans les sous-classes soit compatible avec celle des méthodes de la classe parente. De plus, les sous-classes ne doivent pas modifier la logique fondamentale de la classe parente, ce qui inclut le fait de ne pas introduire des exceptions ou des comportements qui ne sont pas gérés par la classe de base.
Le respect de ce principe garantit que les hiérarchies d'héritage restent cohérentes et prévisibles. En cas de non-respect du LSP, le code devient difficile à comprendre et à maintenir, car les sous-classes introduisent des comportements inattendus.
I : Interface Segregation Principle (ISP)
Le principe de ségrégation des interfaces (ISP) stipule qu'aucune classe ne devrait être forcée d'implémenter des méthodes qu'elle n'utilise pas. En d'autres termes, il est préférable de créer plusieurs interfaces spécifiques et petites plutôt qu'une seule interface large et générique.
L'idée centrale de ce principe est de réduire le couplage entre les classes. Lorsque les classes sont obligées d'implémenter des méthodes qu'elles n'utilisent pas, cela peut mener à des classes encombrées, difficiles à maintenir et à comprendre. De plus, cela rend le code moins flexible, car les modifications apportées à une interface générique peuvent impacter de nombreuses classes, même celles qui n'utilisent pas certaines de ses méthodes.
Exemple :
Imaginons que nous ayons une interface Animal
qui définit plusieurs méthodes :
interface Animal {
public function manger(): void;
public function dormir(): void;
public function voler(): void;
public function nager(): void;
}
Si nous avons une classe Oiseau
qui implémente cette interface, elle devra fournir des implémentations pour toutes ces méthodes, même si toutes ne sont pas pertinentes. Par exemple, un Oiseau
peut voler mais ne nage pas. Inversement, une classe Poisson
qui implémente cette même interface devra fournir une implémentation pour la méthode voler()
, même si cela n'a pas de sens.
class Oiseau implements Animal {
public function manger(): void {
echo "L'oiseau mange des graines.";
}
public function dormir(): void {
echo "L'oiseau dort sur une branche.";
}
public function voler(): void {
echo "L'oiseau vole dans le ciel.";
}
public function nager(): void {
throw new Exception("L'oiseau ne peut pas nager.");
}
}
Pour respecter le principe de ségrégation des interfaces, nous devrions diviser cette interface en plusieurs interfaces plus spécifiques :
interface Mangeable {
public function manger(): void;
}
interface Dormable {
public function dormir(): void;
}
interface Volant {
public function voler(): void;
}
interface Nageant {
public function nager(): void;
}
Désormais, nous pouvons créer des classes spécifiques qui n'implémentent que les interfaces dont elles ont besoin
class Oiseau implements Mangeable, Dormable, Volant {
public function manger(): void {
echo "L'oiseau mange des graines.";
}
public function dormir(): void {
echo "L'oiseau dort sur une branche.";
}
public function voler(): void {
echo "L'oiseau vole dans le ciel.";
}
}
D : Principe d'Inversion des Dépendances (DIP)
Le principe d'inversion des dépendances (DIP) stipule qu'une classe doit dépendre d'une abstraction plutôt que d'une implémentation concrète. En d'autres termes, il est préférable d'interagir avec des interfaces ou des classes abstraites plutôt qu'avec des classes spécifiques.
Pour appliquer ce principe, il est recommandé d'utiliser des interfaces lors du passage de paramètres. En passant une interface, on s'assure que l'objet que vous manipulez, quel que soit son type, possède les méthodes appropriées définies par cette interface. Cela permet également de faciliter les changements futurs, car vous pourrez modifier ou remplacer l'implémentation sans impacter le code qui utilise l'interface.
Bien qu'il ne soit pas toujours problématique de passer des objets comme paramètres à vos fonctions, le principe d'inversion des dépendances est particulièrement pertinent lorsque vous devez effectuer des actions communes sur plusieurs objets différents. Dans ces cas, définir une interface commune permet de garantir que chaque objet respectera un contrat de comportement attendu, ce qui renforce la flexibilité et la maintenabilité du code.
Exemple :
Imaginons que nous avons une application qui envoie des notifications aux utilisateurs. Pour cela, nous avons besoin d'un service pour envoyer des emails. Au lieu de créer directement une dépendance entre le service de notification et le service d'email, nous allons utiliser une interface.
1. Définir une interface : Nous allons créer une interface nommée NotificationServiceInterface
, qui définira la méthode sendNotification
. Cette interface servira de contrat que toutes les implémentations de services de notification devront respecter.
interface NotificationServiceInterface
{
public function sendNotification(string $message): void;
}
2. Implémenter l'interface : Nous allons créer une classe EmailService
qui implémente cette interface. Cette classe contiendra la logique spécifique pour envoyer des emails.
class EmailService implements NotificationServiceInterface
{
public function sendNotification(string $message): void
{
// Code pour envoyer un email avec le message donné
}
}
3. Utiliser l'interface dans un gestionnaire : Nous allons créer une classe NotificationManager
qui utilisera NotificationServiceInterface
pour envoyer des notifications. Ainsi, en utilisant une interface, la classe NotificationManager
n'est pas liée à une implémentation spécifique, ce qui lui permet d'être plus flexible.
class NotificationManager
{
public function __construct(private NotificationServiceInterface $notificationService)
{
}
public function notify(string $message): void
{
$this->notificationService->sendNotification($message);
}
}
4. Utilisation : Lorsque nous voulons envoyer une notification, nous créons d'abord une instance de EmailService
, puis nous la passons au NotificationManager
. Cela permet de gérer les notifications sans que NotificationManager
connaisse les détails d'implémentation de EmailService
.
// Création d'une instance de EmailService
$emailService = new EmailService();
// Passer le service à NotificationManager
$notificationManager = new NotificationManager($emailService);
// Envoyer une notification
$notificationManager->notify("Bonjour, ceci est un message de test.");
Dans cet exemple, si un jour nous voulons ajouter une nouvelle méthode de notification, comme un service de SMS, il suffira de créer une nouvelle classe qui implémente NotificationServiceInterface
, sans modifier le NotificationManager
. Cela démontre comment le principe d'inversion des dépendances favorise la flexibilité et la maintenabilité du code.
Aucune page ou chapitre n'a été ajouté à cet article.