Un guide pour vous aider à mieux comprendre le complexe de substitution de Liskov
Comme nous le savons tous, les exigences logicielles changent constamment et nous, en tant que développeurs, devons nous assurer que ces modifications ne cassent pas le code existant. Pour cette raison, les principes SOLID ont été introduits dans la conception orientée objet pour faciliter ce processus.
Les principes SOLID sont un ensemble de principes créés par Robert C. Martin (Oncle Bob). Ces principes nous aident à créer des logiciels plus flexibles, maintenables et compréhensibles.
Après avoir présenté le principe ouvert-fermé dans l’article précédent, nous aborderons le troisième principe, le principe de substitution de Liskov (LSP), qui est le « L » dans l’acronyme SOLID.
Introduisons la définition mathématique du LSP, puis sautons dans les détails. Barbara Liskov a introduit la définition mathématique en 1988 :
« Si pour chaque objet o1 de type S il existe un objet o2 de type T tel que pour tous les programmes P définis en termes de T, le comportement de P est inchangé lorsque o1 est substitué à o2 alors S est un sous-type de T. »
La conception orientée objet de base contrôle la relation entre les objets en utilisant soit Héritage ou alors Composition. L’héritage, la relation IS-A, se produit lorsque quelque chose EST une sorte d’autre chose. Par exemple, un cheval EST UN animal.
D’autre part, Composition, le rapport HAS-A avec autre chose. Par exemple, une adresse A une ville qui lui est liée. Le LSP apporte une contrainte supplémentaire à la conception orientée objet et indique que ces relations ne sont pas suffisantes et doivent être remplacées par IS-SUBSTITUTABLE-FOR.
Mais qu’est ce que ça veut dire? Cela signifie simplement qu’un supertype doit pouvoir être remplacé par son sous-type sans casser le code existant. En d’autres termes, le supertype doit se comporter de la même manière que son sous-type.
Cela dit, comment pouvons-nous garantir que le remplacement d’un supertype par un sous-type n’aura pas d’effets secondaires sur notre code existant ?
Gardez à l’esprit que les cinq principes SOLID sont liés d’une manière ou d’une autre. Et suivre un principe ne garantit pas que vous suivez correctement les autres.
Comme nous le verrons, le LSP étend le principe Open-Closed, et suivre les règles de l’OCP ne suffit pas pour garantir que votre code est ouvert pour l’extension et fermé pour les modifications. Mais votre code doit également être conforme au principe de substitution de Liskov pour éviter les effets secondaires.
Voici un exemple qui vous aidera à mieux comprendre ce point :
Comme vous le voyez, cet exemple suit parfaitement l’OCP. Autrement dit, si nous voulons ajouter un nouveau rôle d’employé, tout ce que nous avons à faire est d’ajouter une nouvelle classe, y compris la nouvelle fonctionnalité de rôle qui se conforme au IEmployee
Contrat.
Ça sonne bien. Mais permettez-moi de vous poser une question, comment construiriez-vous le Guest
rôle? Oui, le rôle d’invité peut faire listPosts
mais comment feriez-vous face à login
fonctionnalité car il n’y a pas d’authentification pour un employé invité ?
Je pense que tu pourrais répondre, Je peux le laisser vide sans aucune fonctionnalité, ou je peux lancer une exception non prise en charge dans le login
méthode. Oui, ces solutions sont intuitives si vous ne considérez pas le LSP.
Encore une fois, vous pourriez me demander, puisque j’ai parfaitement rempli l’OCP, et c’est la chose la plus importante pour moi, alors quel est le problème si je viole le LSP ? Je pense que cette question a un point implicite correct : certains principes sont plus importants que d’autres. Cependant, nous ne devons pas ignorer un principe parce qu’il est moins important.
Comme nous le savions déjà, le LSP consiste à substituer un supertype et un sous-type sans impacter le code client existant. Gardez ce point à l’esprit et reprenons vos solutions :
- Laisser vide sans fonctionnalité : Maintenant, votre code client attend le
login
fonction pour renvoyer un jeton d’utilisateur authentifié. Que se passera-t-il si vous avez utilisé leGuest.login
ça ne retourne rien ? Cela cassera votre code existant, n’est-ce pas ? - Lever une exception non prise en charge : Encore une fois, votre code existant ne gère pas cette nouvelle exception de
Guest.login
. Donc, par conséquent, en utilisant leGuest.login
cassera le code existant.
Étonnamment, cette conception suit parfaitement l’OCP. Cependant, il viole le LSP.
Malheureusement, il n’existe pas de moyen simple d’appliquer ce principe dans votre code. Cependant, pour appliquer correctement ce principe dans votre code, vous devez suivre deux types de règles : Signature et Comportement.
Bien que vous puissiez appliquer les règles de signature à l’aide d’un langage compilé tel que Java, vous ne pouvez pas appliquer les règles de comportement. Au lieu de cela, vous devez implémenter des vérifications pour appliquer un comportement spécifique.
Tout d’abord, introduisons les règles de signature
1. Contre-variance d’arguments de méthode : il s’agit d’une conversion d’un type plus spécifique à un type plus général. En d’autres termes, l’argument de méthode de sous-type remplacé doit être identique au supertype ou plus large.
Si le code client attend un string
-seul argument dans le SuperType
et vous remplacez qu’un SuperType
avec SubType
qui accepte string
ou alors number
arguments (plus larges), le code client ne remarquera aucune différence.
2. Covariance des types de retour : il convertit d’un type plus général en un type plus spécifique. En d’autres termes, le type de retour de la méthode de sous-type surchargée doit être le même que le supertype ou plus étroit.
Le code client a déjà traité un string
ou alors number
réponse venant de la SuperType
. Donc, si vous remplaciez le SuperType
avec le SubType
qui ne renvoie qu’un string
réponse, le code client ne se briserait pas.
3. Exceptions : la méthode de sous-type doit lever les mêmes exceptions que le supertype ou plus restreint. Tous les langages compilés ne pouvaient pas appliquer cette règle. Certains langages peuvent l’appliquer, comme Java, et d’autres qui ne peuvent pas l’appliquer, comme TypeScript.
Comme la règle précédente, tant que le code client dépend du SuperType
gère plus d’exceptions. Si vous l’avez remplacé SuperType
avec le SubType
qui gère moins d’exceptions, le code client ne remarquera aucune différence.
Deuxièmement, introduisons les règles de comportement
1. Invariants de classe (règle de propriété) : les méthodes de sous-type doivent préserver ou renforcer les invariants de classe du supertype.
Le SubType
doit maintenir le même SuperType
invariants ou les renforcer. Pensez-y, si le SubType
ne maintient pas le même SuperType
invariants, il ne serait pas substituable aux SuperType
et casserait le code client qui dépend d’un comportement spécifique du SuperType
.
2. Contrainte d’historique (règle de propriété) : les méthodes de sous-type ne doivent pas autoriser un changement d’état que le supertype n’autorise pas.
Ici, si le SubType
ignoré les contraintes imposées par leSuperType
, cela cassera tout code client qui s’appuie sur ces contraintes. Ainsi, SubType
ne peut être substitué à SuperType
.
3. Conditions préalables (règle de méthode) : la méthode de sous-type doit préserver ou affaiblir les conditions préalables de la méthode de supertype remplacée. Ici, si vous fragilisez la condition, vous relâchez ses contraintes.
Dans l’exemple précédent, tout code client qui fournit le hour
entrée imposée au SuperType
état hour < 0 && hour > 12
seront imposés à un éventail plus large hour < 0 && hour > 23
du SubType
. En d’autres termes, SubType
pourrait être substituable à SuperType
sans aucun effet secondaire.
4. Postconditions (règle de méthode) : la méthode de sous-type doit préserver ou renforcer les postconditions de la méthode de supertype remplacée.
Comme dans l’exemple précédent, si le code client attend la valeur renvoyée par SuperType
avoir une valeur maximale de 50
sera par conséquent valide si vous le remplacez par un SubType
renvoie une valeur avec une valeur maximale de 30
.
À première vue, vous pourriez penser que le principe de substitution de Liskov concerne uniquement l’héritage, mais ce n’est pas vrai. J’ai préféré consacrer une section séparée à ce point pour le souligner davantage car cela m’a beaucoup déconcerté lors de l’apprentissage de ce principe. Je pensais que LSP ne pouvait être appliqué que si j’utilisais l’héritage.
Le principe de substitution de Liskov n’a rien à voir avec l’héritage. Le LSP consiste simplement à sous-typer. Quoi qu’il en soit, ce sous-typage provient de l’héritage ou de la composition. Étant donné que le LSP n’a rien à voir avec l’héritage, que vous utilisiez ou non l’héritage n’est pas pertinent pour savoir si le LSP s’applique ou non. Découvrez cette solution sur StackExchange.
Donc, ne couplez pas étroitement les concepts de LSP et d’héritage. Au lieu de cela, gardez à l’esprit le LSP si vous êtes obligé d’utiliser l’héritage. Reprenez l’exemple de la section « LSP étend OCP ».
Introduisons les violations les plus courantes du LSP et essayons de le reconcevoir pour suivre le LSP.
1. Vérification de type
Si vous vérifiez le type d’une variable dans un code polymorphe. Jetez un oeil à l’exemple ci-dessous:
Comme vous le voyez, cette boucle a deux fonctionnalités différentes en fonction du type d’employé. Mais quel est le problème avec cette implémentation ?
Détrompez-vous. Le premier problème ici est que chaque fois que vous travaillez avec des employés, vous devrez peut-être effectuer une vérification pour voir si cet employé est un Guest
type pour faire une fonctionnalité spécifique ou un autre type pour faire une autre fonctionnalité.
Le deuxième problème est que vous pourriez ajouter de nouveaux types à l’avenir et que vous deviez vous rendre partout où cette vérification existe pour ajouter des comportements spécifiques pour prendre en charge ces nouveaux types. En plus de cela, cela viole le principe ouvert-fermé.
Alors, comment pouvons-nous résoudre ce problème ? Une solution consiste à utiliser le Principe Dites, ne demandez pas ou alors Encapsulation. Cela signifie ne pas demander à une instance son type, puis effectuer une action spécifique de manière conditionnelle. Au lieu de cela, encapsulez cette logique dans le type et dites-lui d’effectuer une action. Appliquons cela à l’exemple précédent :
2. Vérification nulle
Il a le même comportement que la vérification de type. Jetez un oeil à l’exemple ci-dessous. Au lieu de vérifier Guest
tapez, vous êtes en train de vérifier null
valeur comme ça if (employee === null)
. Cela viole également le LSP.
Mais comment pouvons-nous résoudre ce problème ? Une solution courante à ce problème consiste à utiliser le Modèle de conception d’objet nul. Jetez un œil à cette refonte :
3. Lancer une exception non implémentée
Ceci est courant en raison de l’implémentation partielle d’une interface ou d’une classe de base. Reprenez l’exemple de la section « LSP étend OCP ». Vous devez lancer une exception non implémentée dans la méthode login
du Guest
sous-type car il ne peut pas implémenter complètement le IEmployee
interface (surtype).
La solution à ce problème est de s’assurer d’implémenter complètement le supertype, qu’il s’agisse d’une interface ou d’une classe de base.
Cependant, vous pourriez dire qu’il est parfois difficile d’implémenter complètement l’interface, comme dans notre exemple. C’est vrai. Si vous avez un tel cas, vous devrez probablement revérifier la relation entre le supertype et le sous-type. Le sous-type peut ne pas être qualifié de sous-type pour ce supertype, ou en d’autres termes, il s’agit probablement d’une violation du principe de ségrégation d’interface.
Dans cet article, nous avons présenté le principe de substitution de Liskov. Nous savions que LSP ajoutait une nouvelle contrainte à la conception orientée objet. Il indique que les relations sont insuffisantes et que vous devez vous assurer que les sous-types sont substituables aux supertypes.
Nous connaissions également les règles que vous devez suivre pour appliquer correctement ce principe. Et ces règles pourraient être classées dans les règles de signature et de comportement.
Après cela, nous avons présenté quelques violations courantes de ce principe et des solutions pour celles-ci.
Merci beaucoup d’être resté avec moi jusqu’à ce point. J’espère que vous apprécierez la lecture de cet article.
Si vous avez trouvé cet article utile, consultez ces autres :