paint-brush
Principes SOLID en Java : guide du débutantpar@ps06756
14,604 lectures
14,604 lectures

Principes SOLID en Java : guide du débutant

par Pratik Singhal14m2024/03/22
Read on Terminal Reader

Trop long; Pour lire

Les principes SOLID sont les principes de programmation orientée objet essentiels pour développer des logiciels évolutifs. Les principes sont : S : Principe de responsabilité unique O : principe ouvert/fermé L : principe de substitution de Liskov I : Principe de ségrégation des interfaces D : Principe d’inversion de dépendance
featured image - Principes SOLID en Java : guide du débutant
Pratik Singhal HackerNoon profile picture
0-item

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.

Pourquoi les principes SOLID sont-ils importants ?

Les principes SOLID sont importants pour plusieurs raisons :


  • Les principes SOLID nous permettent d'écrire du code propre et maintenable : lorsque nous démarrons un nouveau projet, la qualité du code est initialement bonne car nous disposons d'un ensemble limité de fonctionnalités que nous implémentons. Cependant, à mesure que nous intégrons davantage de fonctionnalités, le code commence à devenir encombré.


  • Les principes SOLID s'appuient sur les fondements de l'abstraction, du polymorphisme et de l'héritage et conduisent à des modèles de conception pour des cas d'utilisation courants. Comprendre ces modèles de conception aide à mettre en œuvre des cas d'utilisation courants en programmation.


  • Les principes SOLID nous aident à écrire du code propre qui améliore la testabilité du code. En effet, le code est modulaire et faiblement couplé. Chaque module peut être développé indépendamment et testé indépendamment.


Explorons maintenant chacun des principes SOLID en détail avec des exemples concrets.

1. Principe de responsabilité unique

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 :


  1. S'il y a un changement dans la logique de base de la classe SavingsAccount (comme debit , credit , etc.).


  2. 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.

2. Principe d'ouverture/fermeture

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 :


  • Il arrive souvent que la source originale de la classe ne soit même pas disponible. Il pourrait s'agir d'une dépendance consommée par votre projet.


  • Garder la classe d'origine inchangée réduit les risques de bugs. Puisqu’il peut y avoir d’autres classes dépendantes de la classe que nous souhaitons modifier.


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.

3. Le principe de substitution de Liskov

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. } }


4. Principe de ségrégation des interfaces

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.

Principe d'inversion de dépendance

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.