Les principes de conception SOLID sont les principes de conception les plus importants que vous devez connaître pour écrire du code propre. Avoir une solide maîtrise des principes SOLID est une compétence indispensable pour tout programmeur. Ils constituent la base sur laquelle d’autres modèles de conception sont développés.
Dans cet article, nous aborderons les principes de conception SOLID à l'aide de quelques exemples réels et comprendrons leur importance.
Avec le polymorphisme, l'abstraction et l'héritage, les principes SOLID sont vraiment importants pour être bon en programmation orientée objectifs.
Les principes SOLID sont importants pour plusieurs raisons :
Explorons maintenant chacun des principes SOLID en détail avec des exemples concrets.
S dans les principes SOLID signifie principe de responsabilité unique. Le principe de responsabilité unique stipule qu’une classe ne devrait avoir qu’une seule raison de changer. Cela limite le nombre d'endroits où nous devons apporter des modifications lors de l'intégration d'exigences supplémentaires dans notre projet.
Chaque classe devrait avoir exactement une raison de changer.
Par exemple, disons que nous concevons une application bancaire en Java dans laquelle nous avons une classe SavingsAccount
qui permet des opérations de base telles que le débit, le crédit et sendUpdates
. La méthode sendUpdate
prend une énumération appelée NotificationMedium
(comme Email, SMS, etc.) et envoie la mise à jour avec le support approprié. Nous allons écrire le code pour cela comme indiqué ci-dessous.
public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void sendNotification(NotificationMedium medium) { if (medium == NotificationMedium.SMS) { // Send SMS here } else if (medium == NotificationMedium.EMAIL) { // Send Email here } } }
public enum NotificationMedium { SMS, EMAIL }
Maintenant, si vous regardez la classe SavingsAccount
ci-dessus, elle peut changer pour plusieurs raisons :
S'il y a un changement dans la logique de base de la classe SavingsAccount
(comme debit
, credit
, etc.).
Si la banque décide d'introduire un nouveau support de notification (disons WhatsApp).
Il s’agit d’une violation du principe de responsabilité unique des principes SOLID. Pour résoudre ce problème, nous créerons une classe distincte qui envoie la notification.
Refactorisons le code ci-dessus selon les principes SOLID
public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void printBalance() { System.out.println("Name: " + name+ " Account Balance: " + balance); } public void sendNotification(Medium medium) { Sender.sendNotification(medium, this); } }
public enum NotificationMedium { SMS, EMAIL }
public class Sender { public static void sendNotification(NotificationMedium medium, SavingsAccount account) { // extract account data from the account object if (medium == NotificationMedium.SMS) { //logic to send SMS here } else if (medium == NotificationMedium.EMAIL) { // logic to send Email here } } }
Maintenant que nous avons refactorisé le code, s'il y a un changement dans le NotificationMedium
ou le format, nous modifierons la classe Sender
. Cependant, s'il y a un changement dans la logique de base de SavingsAccount
, des changements seront apportés à la classe SavingsAccount
.
Cela corrige la violation que nous avons observée dans le premier exemple.
Le principe Open Close stipule que nous devons concevoir les classes de telle manière qu'elles soient ouvertes à l'extension (en cas d'ajout de fonctionnalités supplémentaires) mais fermées à la modification. Être fermé pour modification nous aide de deux manières :
Pour voir un exemple du principe Open Close, prenons l'exemple d'un panier d'achat (comme celui mis en œuvre sur les sites de commerce électronique).
Je vais créer une classe appelée Cart
qui contiendra une liste d' Item
que vous pourrez y ajouter. En fonction du type d'article et de sa fiscalité, nous souhaitons créer une méthode qui calcule la valeur totale du panier dans la classe Cart
.
Les classes doivent être ouvertes pour extension et fermées pour modification
import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart() { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { if (item.getItemType() == GIFT) { // 8% tax on gift + 2% gift wrap cost value += (item.getValue()*1.08) + item.getValue()*0.02 } else if (item.getItemType() == ItemType.ELECTRONIC_ITEM) { value += (item.getValue()*1.11); } else { value += item.getValue()*1.10; } } return value; } }
@Getter @Setter public abstract class Item { protected double price; private ItemType itemType; public double getValue() { return price; } }
public enum ItemType { ELECTRONIC, GIFT }
Dans l'exemple ci-dessus, la méthode calculateCartValue
calcule la valeur du panier en itérant sur tous les articles à l'intérieur du panier et en appelant une logique basée sur le type d'article.
Bien que ce code semble correct, il viole les principes SOLID.
Disons que nous devons ajouter une nouvelle règle pour un type d'article différent (par exemple l'épicerie) lors du calcul de la valeur du panier. Dans ce cas, nous devrons modifier la classe d'origine Cart
et écrire une autre else if
à l'intérieur qui vérifie les articles de type Grocery
.
Cependant, avec peu de refactorisation, nous pouvons faire en sorte que le code adhère au principe d'ouverture/fermeture. Voyons comment.
Tout d’abord, nous allons rendre la classe Item
abstraite et créer des classes concrètes pour différents types d’ Item
, comme indiqué ci-dessous.
public abstract class Item { protected double price; public double getValue() { return price; } }
public class ElectronicItem extends Item { public ElectronicItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.11; } }
public class GiftItem extends Item { public GiftItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.08 + super.getValue()*0.02; } }
public class GroceryItem extends Item { public GroceryItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.03; } }
À l'intérieur de chaque classe Item concrète comme GroceryItem
, GiftItem
et ElectronicItem
implémentent la méthode getValue()
qui contient la logique métier pour la taxation et le calcul de la valeur.
Maintenant, nous allons faire dépendre la classe Cart
de la classe abstraite Item
et invoquer la méthode getValue()
pour chaque élément, comme indiqué ci-dessous.
import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } }
Désormais, dans ce code refactorisé, même si de nouveaux types d' Item
(s) sont introduits, la classe Cart
reste inchangée. En raison du polymorphisme, quel que soit le type réel de l' Item
à l'intérieur des items
ArrayList
, la méthode getValue()
de cette classe serait invoquée.
Le principe de substitution de Liskov stipule que dans un code donné, même si l'on remplace l'objet d'une superclasse par un objet de la classe enfant, le code ne doit pas se casser. En d’autres termes, lorsqu’une sous-classe hérite d’une superclasse et remplace ses méthodes, elle doit maintenir la cohérence avec le comportement de la méthode dans la superclasse.
Par exemple, si nous créons les classes suivantes Vehicle
et deux classes Car
et Bicycle
. Maintenant, disons que nous créons une méthode appelée startEngine()
dans la classe Vehicle, elle peut être remplacée dans la classe Car
, mais elle ne sera pas prise en charge dans la classe Bicycle
car Bicycle
n'a pas de moteur (voir l'exemple de code ci-dessous)
La sous-classe doit maintenir la cohérence avec le comportement de la superclasse lors du remplacement des méthodes.
class Vehicle { public void startEngine() { // start engine of the vehicle } } class Car extends Vehicle { @Override public void startEngine() { // Start Engine } } class Bicycle extends Vehicle { @Override public void startEngine(){ throw new UnsupportedOperationException("Bicycle doesn't have engine"); } }
Maintenant, disons qu'il y a du code qui attend un objet de type véhicule et s'appuie sur la méthode startEngine()
. Si, en appelant ce morceau de code au lieu de passer un objet de type Vehicle
, nous passons un objet Bicycle
, cela entraînerait des problèmes dans le code. Puisque la méthode de la classe Bicycle
(s) lèvera une exception lorsque la méthode startEngine()
est appelée. Ce serait une violation des principes SOLID (principe de substitution de Liskov)
Pour résoudre ce problème, nous pouvons créer deux classes MotorizedVehicle
et NonMotorizedVehicle
et faire en sorte que Car
hérite de la classe MotorizedVehicle
et que Bicycle
hérite de NonMotorizedVehicle
class Vehicle { } class MotorizedVehicle extends Vehicle { public void startEngine() { // start engine here } } class Car extends MotorizedVehicle { @Override public void startEngine() { // Start Engine } } class NonMotorizedVehicle extends Vehicle { public void startRiding() { // Start without engine } } class Bicycle extends NonMotorizedVehicle { @Override public void startRiding(){ // Start riding without the engine. } }
Le « I » dans SOLID Principes signifie le principe de ségrégation d'interface.
Le principe de ségrégation des interfaces stipule qu'au lieu d'avoir des interfaces plus grandes qui obligent les classes d'implémentation à implémenter des méthodes inutilisées, nous devrions avoir des interfaces plus petites et implémenter des classes. De cette façon, les classes implémentent uniquement les méthodes pertinentes et restent propres.
Divisez vos interfaces en plusieurs interfaces plus petites plutôt qu'en une seule grande interface.
Par exemple, regardons le framework Collections intégré en Java. Entre autres structures de données, Java fournit également la structure de données LinkedList
et ArrayList
,
La classe ArrayList
implémente les interfaces suivantes : Serializable
, Cloneable
, Iterable
, Collection
, List
et RandomAccess
.
La classe LinkedList
implémente Serializable
, Cloneable
, Iterable
, Collection
, Deque
, List
et Queue
.
Cela fait beaucoup d'interfaces !
Plutôt que d'avoir autant d'interfaces, les développeurs Java auraient pu combiner Serializable
, Cloneable
, Iterable
, Collecton
, List
et RandomAccess
en une seule interface, disons l'interface IList
. Désormais, les classes ArrayList
et LinkedList
auraient pu implémenter cette nouvelle interface IList
.
Cependant, comme LinkedList
ne prend pas en charge l'accès aléatoire, il aurait pu implémenter les méthodes dans l'interface RandomAccess
et aurait pu lancer UnsupportedOperationException
lorsque quelqu'un essaie de l'appeler.
Cependant, cela constituerait une violation du principe de ségrégation d'interface dans les principes SOLID car cela « forcerait » la classe LinkedList à implémenter les méthodes à l'intérieur de l'interface RandomAccess
même si cela n'est pas nécessaire.
Par conséquent, il est préférable de diviser l'interface en fonction du comportement commun et de laisser chaque classe implémenter plusieurs interfaces plutôt qu'une grande interface.
Le principe d'inversion des dépendances stipule que les classes du niveau supérieur ne doivent pas dépendre directement des classes du niveau inférieur. Cela provoque un couplage étroit entre les deux niveaux.
Au lieu de cela, les classes inférieures devraient fournir une interface dont dépendraient les classes de niveau supérieur.
Dépendre des interfaces plutôt que des classes
Par exemple, continuons avec l'exemple Cart
que nous avons vu ci-dessus et améliorons-le pour ajouter des options de paiement. Supposons que nous ayons deux types d'options de paiement avec nous, DebitCard
et Paypal
. Maintenant, dans la classe Cart
, nous souhaitons ajouter une méthode à placeOrder
qui calculerait la valeur du panier et lancerait le paiement en fonction du paiement fourni. méthode.
Pour ce faire, nous aurions pu ajouter une dépendance dans l'exemple Cart
ci-dessus en ajoutant les deux options de paiement en tant que champs à l'intérieur de la classe Cart
. Cependant, cela couplerait étroitement la classe Cart
avec les classes DebitCard
et Paypal
.
Au lieu de cela, nous créerions une interface Payment
et demanderions aux classes DebitCard
et Paypal
d'implémenter les interfaces Payment
. Désormais, la classe Cart
dépendra de l’interface Payment
, et non des types de paiement individuels. Cela maintient les classes faiblement couplées.
Voir le code ci-dessous.
public interface Payment { void doPayment(double amount); } public class PaypalPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate paypal payment } } public class DebitCardPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate payment via debit card } } import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; private Payment paymentOption; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); this.paymentOption = paymentOption; } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } public void placeOrder() { this.paymentOption.doPayment(calculateCartValue()); } }
Si vous souhaitez en savoir plus sur la programmation orientée objet, les modèles de conception GoF et les entretiens de conception de bas niveau, consultez mon cours très apprécié Programmation orientée objet + modèles de conception Java.