Retour au blog

SOLID : Les 5 premiers principes de la conception orientée objet ?

SOLID : Les 5 premiers principes de la conception orientée objet ?

Introduction

SOLID est un acronyme mnémotechnique pour cinq principes de conception orientée objet qui ont été introduits par Robert C. Martin, communément appelé Uncle Bob. Ces principes sont destinés à aider les concepteurs, architectes, ingénieurs et développeurs de logiciels à créer des logiciels plus flexibles, maintenables et évolutifs. En suivant ces principes, vous pouvez concevoir des classes plus faciles à tester, refactoriser, réutiliser et étendre.

L'acronyme SOLID signifie :

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 des dépendances

Dans cet article, nous expliquerons chaque principe individuellement pour comprendre comment il peut vous aider à écrire un meilleur code. De plus, nous ajouterons des extraits de code pour chaque principe afin de vous montrer comment les appliquer dans votre parcours de codage, ainsi que ce que vous devriez éviter dans une architecture de code propre. Pour illustrer ces concepts, nous utiliserons le langage de programmation Kotlin développé par JetBrains et des contributeurs open-source.

Principe de responsabilité unique

Le principe de responsabilité unique (SRP) est un principe de conception logicielle qui stipule que chaque classe ou module d'un programme doit avoir une seule responsabilité bien définie. Cela signifie qu'une classe ne doit avoir qu'une seule raison de changer, et qu'elle ne doit être responsable que d'une seule partie des fonctionnalités du programme.

Considérez l'extrait de code suivant comme un exemple de la façon dont ce principe peut être appliqué en Kotlin:

Le mot-clé data  en Kotlin indique que cette classe est une classe de données (data class), ce qui signifie qu'elle est destinée à contenir des données et n'a pas de comportement complexe. En utilisant cette classe de données, nous pouvons créer une classe UserService  pour gérer les utilisateurs, comme indiqué ci-dessous :

Dans cet exemple, la classe UserService a une seule responsabilité : la gestion des utilisateurs dans une base de données. Chaque utilisateur est représenté par la data class User  partagée précédemment. Toutes les méthodes de la classe UserService sont liées à cette responsabilité et sont donc cohérentes. Cela rend la classe plus facile à comprendre et à maintenir, car il est clair que toutes les méthodes de la classe sont liées à une tâche unique et bien définie.

Une méthode qui viole le principe de responsabilité unique (SRP) dans la classe UserService serait une méthode qui n'est pas liée à la responsabilité principale de la classe, à savoir la gestion des utilisateurs dans une base de données. Par exemple, considérez la variante suivante de la classe UserService :

Dans cet exemple, la sendEmail  méthode ne concerne pas la responsabilité principale de la UserService classe, qui consiste à gérer les utilisateurs dans une base de données. Cette méthode est responsable de l'envoi d'e-mails, ce qui est une préoccupation distincte de la gestion des utilisateurs. Par conséquent, cette méthode viole le SRP, car elle introduit une deuxième raison de modifier la classe UserService.

Pour respecter le principe de responsabilité unique (SRP), il serait préférable de séparer la fonctionnalité d'envoi d'e-mails dans une classe distincte, telle qu'une EmailService  classe. Cela permettrait à la UserService classe de se concentrer sur sa responsabilité principale de gestion des utilisateurs, et à la EmailService classe de se concentrer sur sa responsabilité d'envoi d'e-mails.

Il convient de noter que le principe de responsabilité unique ne concerne pas le nombre de méthodes d'une classe, mais plutôt la cohésion des méthodes et la séparation claire des responsabilités au sein d'une classe.

Principe ouvert-fermé

Le principe ouvert-fermé (OCP) est un principe de conception logicielle qui stipule que les entités logicielles (telles que les classes, les modules ou les fonctions) doivent être ouvertes à l'extension, mais fermées à la modification. Cela signifie qu'il doit être possible d'ajouter de nouvelles fonctionnalités à une classe ou à un module sans modifier son code existant.

Considérez la UserService classe de l'exemple précédent. Supposons que nous voulions ajouter une nouvelle fonctionnalité à la UserService classe qui nous permet de rechercher des utilisateurs par adresse e-mail. Une façon de procéder consisterait à ajouter une nouvelle méthode à la UserService classe comme mis en évidence dans le code ci-dessous :

Cette approche fonctionne, mais elle viole le principe ouvert-fermé, car nous avons dû modifier la classe existante UserService afin d'ajouter la nouvelle fonctionnalité. Une meilleure approche consisterait à utiliser l'héritage ou la composition pour étendre les fonctionnalités de la classe UserService sans modifier son code.

Pour y parvenir, nous pourrions créer une nouvelle classe appelée UserSearchService  qui étend la classe UserService et ajoute la fonctionnalité de recherche par e-mail :

Dans cet exemple, la UserSearchService classe est ouverte à l'extension, car elle fournit des fonctionnalités supplémentaires au-delà de ce qui est proposé par la UserService classe. En même temps, la UserService classe reste fermée à la modification, car nous n'avons pas eu besoin de modifier son code afin d'ajouter la fonctionnalité de recherche par e-mail.

Principe de substitution de Liskov

Le principe de substitution de Liskov (LSP) est un principe de conception logicielle qui stipule que les objets d'une superclasse doivent pouvoir être remplacés par des objets d'une sous-classe sans affecter la correction du programme. Cela signifie qu'une sous-classe doit être un substitut valide pour sa superclasse, et doit se comporter de la même manière que la superclasse lorsqu'elle est utilisée dans le même contexte.

Nous allons continuer la démonstration en utilisant la User et la UserService classes des exemples précédents. Pour permettre à la classe User d'être étendue, nous utilisons le mot-clé open  en Kotlin :

Voici la classe originale UserService qui utilise la classe de données User ci-dessus :

Supposons que nous voulions créer une sous-classe de User appelée AdminUser qui représente les utilisateurs ayant des privilèges administratifs. Nous pourrions faire cela ainsi :

Dans cet exemple, la classe AdminUser est un substitut valide pour la classe User , car elle se comporte de la même manière que la classe User et peut être utilisée partout où un objet User est attendu. Par exemple, nous pouvons utiliser la classe AdminUser avec la classe UserService  comme ceci :

Ce code est correct, car la classe AdminUser est un substitut valide pour la classe User et peut être utilisée de la même manière qu'un objet User .

Il’s important de noter que le principe de substitution de Liskov va au-delà de la simple héritabilité. Il’s s'agit de s'assurer que les objets d'une sous-classe se comportent de la même manière que les objets de la superclasse, quelle que soit la façon dont la sous-classe est implémentée. Par exemple, si la classe AdminUser devait se comporter différemment de la classe User d'une manière ou d'une autre, cela violerait le principe de substitution de Liskov, car elle ne serait pas un substitut valide pour la classe User .

Principe de ségrégation des interfaces

Le principe de ségrégation des interfaces (ISP) est un principe de conception logicielle qui stipule que les clients ne doivent pas être forcés de dépendre d' interfacesqu'ils n'utilisent pas. Cela signifie qu'il est généralement judicieux de créer de petites interfaces ciblées qui font bien une seule chose, plutôt que de créer de grandes interfaces d'usage général qui tentent de faire beaucoup de choses.

Voici un exemple de la façon dont ce principe peut être appliqué en réécrivant les classes User  et UserService  des exemples précédents :

Dans cet exemple, l'interface UserService définit quatre méthodes liées à la gestion des utilisateurs dans une base de données. La classe DatabaseUserService  implémente cette interface et fournit des implémentations concrètes pour ces méthodes.

Supposons que nous voulions ajouter une nouvelle fonctionnalité à l'interface UserService qui nous permet de rechercher des utilisateurs par adresse e-mail. Une façon de procéder consisterait à ajouter une nouvelle méthode à l'interface UserService :

Votre code ne s'exécutera pas, à moins que vous n'implémentiez également cette méthode dans toutes les classes qui implémentent l'interface UserService :

Bien que cette approche fonctionne, elle viole le principe de ségrégation des interfaces, car elle oblige la classe DatabaseUserService  à implémenter une méthode ( searchUsersByEmail ) dont elle n'a peut-être pas besoin ou qu'elle n'utilise pas.

Une meilleure approche consisterait à créer une interface  séparée pour la fonctionnalité de recherche par e-mail :

Nous avons maintenant des interfaces distinctes, petites et ciblées, c'est-à-dire UserService  et UserSearchServicequi ont une responsabilité unique. Une classe nécessitant toutes les fonctionnalités de ces interfaces peut les implémenter comme indiqué dans l'extrait de code ci-dessous :

Cela respecte le principe de ségrégation des interfaces, car cela garantit que les clients (comme la classe DatabaseUserService ) ne sont pas obligés de dépendre d'interfaces qu'ils n'utilisent pas.

Pour mieux comprendre ce concept, supposons que nous ayons une autre classe appelée MemoryUserService  qui implémente l'interface UserService  mais qui n'a pas besoin de la fonctionnalité de recherche par e-mail, nous pouvons écrire le code ainsi :

Dans cet exemple, la classe MemoryUserService a seulement besoin d'implémenter les méthodes définies dans l'interface UserService , et n'a pas besoin de se soucier de la fonctionnalité de recherche par e-mail. Cela permet à la classe MemoryUserService de se concentrer sur sa responsabilité principale de gestion des utilisateurs en mémoire, plutôt que d'être contrainte d'implémenter des fonctionnalités non liées.

Principe d'inversion des dépendances

Le principe d'inversion des dépendances (DIP) est un principe de conception logicielle qui stipule que les modules de haut niveau ne doivent pas dépendre des modules de bas niveau, mais que les deux doivent dépendre d'abstractions. Cela signifie qu'il est généralement judicieux de concevoir votre logiciel de manière à ce que les composants de haut niveau ne soient pas liés à des implémentations spécifiques de composants de bas niveau, mais dépendent plutôt d'abstractions (telles que des interfaces ou des classes abstraites) qui peuvent être implémentées de différentes manières.

Voyons un exemple de la manière dont ce principe peut être appliqué aux classes User et UserService utilisées dans les extraits de code précédents :

Dans cet exemple, la classe UserService dépend d’une abstraction appelée l’interface UserRepository, plutôt que de dépendre d’une implémentation spécifique d’un dépôt d’utilisateurs. Cela nous permet d’implémenter l’interface UserRepository de différentes manières, comme avec une base de données ou un stockage en mémoire, sans affecter la classe UserService.

Par exemple, voici une implémentation de l’interface UserRepository qui utilise une base de données :

Voici une autre implémentation de l’interface UserRepository qui utilise un stockage en mémoire :

Cela rend le système plus flexible et plus facile à maintenir, car cela nous permet de modifier l’implémentation du dépôt sans affecter le reste du système. Cela facilite également le test de la classe UserService, car nous pouvons simuler la dépendance UserRepository dans nos tests.

Conclusion

Dans cet article, nous avons parlé des cinq principes de SOLID  du code et des extraits de code partagés respectant chaque principe. Respecter ces principes peut vous aider à concevoir des systèmes logiciels plus flexibles, faciles à maintenir et évolutifs. Cependant, il est important de garder à l'esprit que ces principes sont des lignes directrices, plutôt que des règles strictes, et c’est au développeur de décider quand et comment les appliquer dans le contexte de son projet spécifique. Continuez votre apprentissage en consultant notre blog pour des articles plus approfondis et actualisés et des tutoriels sur le cloud computing et le DevOps, la conception et le développement de logiciels, les tendances technologiques à surveiller, et plus encore.

Bon code !

author

Preslav Dobrev

Auteur · CloudSigma

Preslav Dobrev est un designer créatif chez CloudSigma, axé sur une identité commerciale cohérente à travers des canaux marketing traditionnels et innovants. Il excelle à fusionner la vision artistique avec le marketing stratégique pour créer des récits de marque percutants.

Commentaires

Aucun commentaire pour l'instant. Soyez le premier.