Dans cet article, vous allez apprendre les principes SOLID. Vous allez avoir une compréhension de chacun de ces principes ainsi que des exemples de code en Java.
Les principes SOLID sont un ensemble de cinq principes de conception utilisés dans la programmation orientée objet. Respecter ces principes vous aidera à développer un logiciel robuste. Ils rendront votre code plus efficient, lisible et maintainable.
SOLID est un acronyme qui se prononce :
- Principe de Responsabilité Unique
- Principe Ouvert/Fermé
- Principe de Substitution de Liskov
- Principe de Ségrégation d’Interface
- Principe d’Inversion de Dépendance
Principe de Responsabilité Unique
Le principe de Responsabilité Unique stipule que chaque classe doit avoir une seule et unique responsabilité, une seule raison de changer.
public class Employee{
public String getDesignation(int employeeID){ // }
public void updateSalary(int employeeID){ // }
public void sendMail(){ // }
}
Dans l’exemple ci-dessus, la classe Employee
possède quelques comportements spécifiques à la classe employé comme getDesignation
& updateSalary
.
De plus, elle a également une autre méthode nommée sendMail
qui s’éloigne de la responsabilité de la classe Employee
.
Ce comportement n’est pas spécifique à cette classe et en avoir est contraire au principe de Responsabilité Unique. Pour remédier à cela, vous pouvez déplacer la méthode sendMail
vers une classe distincte.
Voici comment :
public class Employee{
public String getDesignation(int employeeID){ // }
public void updateSalary(int employeeID){ // }
}
public class NotificationService {
public void sendMail() { // }
}
Principe d’ouverture/fermeture
Selon le principe d’ouverture/fermeture, les composants doivent être ouverts à l’extension, mais fermés à la modification. Pour comprendre ce principe, prenons l’exemple d’une classe qui calcule l’aire d’une forme.
public class AreaCalculator(){
public double area(Shape shape){
double areaOfShape;
if(shape instanceof Square){
// calcule l'aire du Carré
} else if(shape instanceof Circle){
// calcule l'aire du Cercle
}
return areaOfShape;
}
Le problème avec l’exemple ci-dessus est que si une nouvelle instance de type Shape
pour laquelle vous devez calculer l’aire dans le futur est ajoutée, vous devez modifier la classe ci-dessus en ajoutant un autre bloc conditionnel else-if
. Vous finirez par faire cela pour chaque nouvel objet de type Shape
.
Pour remédier à cela, vous pouvez créer une interface et faire en sorte que chaque Shape
implémente cette interface. Ensuite, chaque classe peut fournir sa propre implémentation pour le calcul de l’aire. Cela rendra votre programme aisément extensible à l’avenir.
interface IAreaCalculator(){
double area();
}
class Square implements IAreaCalculator{
@Override
public double area(){
System.out.println("Calculating area for Square");
return 0.0;
}
}
class Circle implements IAreaCalculator{
@Override
public double area(){
System.out.println("Calculating area for Circle");
return 0.0;
}
}
Principe de substitution de Liskov
Le principe de substitution de Liskov stipule qu’il doit être possible de remplacer un objet de classe supérieure par un objet de sous-classe sans altérer la correction du programme.
abstract class Bird{
abstract void fly();
}
class Eagle extends Bird {
@Override
public void fly() { // certaine implémentation }
}
class Ostrich extends Bird {
@Override
public void fly() { // implémentation factice }
}
Voici la traduction en français :
Dans l’exemple ci-dessus, la classe Aigle
et la classe Autruche
héritent toutes deux de la classe Oiseau
et surchargent la méthode vole()
. Cependant, la classe Autruche
est obligée de fournir une implémentation factice car elle ne peut pas voler, et donc elle ne se comporte pas de la même manière si nous la remplaçons par l’objet de la classe Oiseau
.
Cela viole le principe de substitution de Liskov. Pour résoudre ce problème, nous pouvons créer une classe séparée pour les oiseaux qui peuvent voler et faire hériter Aigle
de celle-ci, tandis que d’autres oiseaux peuvent hériter d’une classe différente, qui ne contiendra pas de comportement de vole
.
abstract class FlyingBird{
abstract void fly();
}
abstract class NonFlyingBird{
abstract void doSomething();
}
class Eagle extends FlyingBird {
@Override
public void fly() { // certaines implémentations }
}
class Ostrich extends NonFlyingBird {
@Override
public void doSomething() { // certaines implémentations }
}
Principe de ségrégation des interfaces
Selon le principe de ségrégation des interfaces, vous devriez construire de petites interfaces centrées sur un domaine spécifique qui ne forcent pas le client à implémenter des comportements qu’il n’a pas besoin.
Un exemple simple serait d’avoir une interface qui calcule à la fois l’aire et le volume d’une forme.
interface IShapeAreaCalculator(){
double calculateArea();
double calculateVolume();
}
class Square implements IShapeAreaCalculator{
double calculateArea(){ // calculer l'aire }
double calculateVolume(){ // implémentation factice }
}
Le problème avec cela est que si une forme Carré
implémente cela, alors elle est forcée d’implémenter la méthode calculateVolume()
, qu’elle n’a pas besoin.
De l’autre côté, un Cube
peut implémenter les deux. Pour surmonter ce problème, nous pouvons séparer l’interface et avoir deux interfaces distinctes : une pour calculer l’aire et l’autre pour calculer le volume. Cela permettra aux formes individuelles de décider ce qu’elles doivent implémenter.
interface IAreaCalculator {
double calculateArea();
}
interface IVolumeCalculator {
double calculateVolume();
}
class Square implements IAreaCalculator {
@Override
public double calculateArea() { // calculer l'aire }
}
class Cube implements IAreaCalculator, IVolumeCalculator {
@Override
public double calculateArea() { // calculer l'aire }
@Override
public double calculateVolume() {// calculer le volume }
}
Principe d’inversion de dépendance
Dans le principe d’inversion de dépendance, les modules de haut niveau ne devraient pas dépendre de modules de bas niveau. En d’autres termes, vous devez respecter l’abstraction et assurer un耦合 léger.
public interface Notification {
void notify();
}
public class EmailNotification implements Notification {
public void notify() {
System.out.println("Sending notification via email");
}
}
public class Employee {
private EmailNotification emailNotification;
public Employee(EmailNotification emailNotification) {
this.emailNotification = emailNotification;
}
public void notifyUser() {
emailNotification.notify();
}
}
Dans l’exemple donné, la classe Employee
dépend directement de la classe EmailNotification
, qui est un module de bas niveau. Cela violate le principe d’inversion de dépendance.
public interface Notification{
public void notify();
}
public class Employee{
private Notification notification;
public Employee(Notification notification){
this.notification = notification;
}
public void notifyUser(){
notification.notify();
}
}
public class EmailNotification implements Notification{
public void notify(){
// implémenter la notification par courriel
}
}
public static void main(String [] args){
Notification notification = new EmailNotification();
Employee employee = new Employee(notification);
employee.notifyUser();
}
Dans l’exemple ci-dessus, nous avons assuré un耦合 léger. Employee
n’est pas dépendante deaucune implémentation concrète, plutôt, elle dépend seulement de l’abstraction (interface de notification).
Si nous avons besoin de changer la méthode de notification, nous pouvons créer une nouvelle implémentation et l’envoyer à la Employee
.
Conclusion
En conclusion, nous avons couvert l’essence des principes SOLID par des exemples simples dans cet article.
Ces principes forment les blocs de construction pour développer des applications hautement extensibles et réutilisables.
Faisons connaissance sur LinkedIn.
Source:
https://www.freecodecamp.org/news/introduction-to-solid-principles/