SOLID Troisième partie - Principes de ségrégation et de substitution d'interface de Liskov

La responsabilité unique (SRP), ouverte / fermée (OCP), Substitution Liskov, ségrégation d'interface, et inversion de dépendance. Cinq principes agiles qui devraient vous guider chaque fois que vous écrivez du code.

Parce que le principe de substitution de Liskov (LSP) et le principe de séparation des interfaces (ISP) sont assez faciles à définir et à illustrer, nous allons parler de ces deux aspects dans cette leçon..

Principe de substitution de Liskov (LSP)

Les classes enfants ne doivent jamais rompre les définitions de type de la classe parent.

Le concept de ce principe a été introduit par Barbara Liskov dans un discours prononcé lors de la conférence de 1987, puis publié dans un article avec Jannette Wing en 1994. Leur définition originale est la suivante:

Soit q (x) une propriété prouvable sur les objets x de type T. Alors q (y) devrait être prouvable pour les objets y de type S où S est un sous-type de T.

Plus tard, avec la publication des principes SOLID par Robert C. Martin dans son livre Développement de logiciels agiles, Principes, modèles et pratiques, puis republiée dans la version C # du livre Principes, modèles et pratiques agiles en C #, la définition est devenu connu comme le principe de substitution de Liskov.

Ceci nous amène à la définition donnée par Robert C. Martin:

Les sous-types doivent être substituables à leurs types de base.

Aussi simple que cela, une sous-classe doit redéfinir les méthodes de la classe parente de manière à ne pas interrompre les fonctionnalités du point de vue du client. Voici un exemple simple pour démontrer le concept.

class Vehicle function startEngine () // Fonction de démarrage par défaut du moteur function accelerate () // Fonction d'accélération par défaut

Donné un cours Véhicule - cela peut être abstrait - et deux implémentations:

La classe Car étend le véhicule fonction startEngine () $ this-> engageIgnition (); parent :: startEngine ();  fonction privée engageIgnition () // procédure d'allumage classe ElectricBus étend le véhicule function accelerate () $ this-> augmentationVoltage (); $ this-> connectIndividualEngines ();  fonction privée augmentationVoltage () // logique électrique fonction privée connectIndividualEngines () // logique de connexion

Une classe de client devrait pouvoir utiliser l’un ou l’autre, si elle peut utiliser Véhicule.

class Driver function go (véhicule $ v) $ v-> startEngine (); $ v-> accelerate (); 

Ce qui nous amène à une implémentation simple du modèle de conception de la méthode template tel que nous l'avons utilisé dans le tutoriel OCP.


Sur la base de notre expérience précédente avec le principe Open / Closed, nous pouvons conclure que le principe de substitution de Liskov est en relation étroite avec OCP. En fait, "une violation de LSP est une violation latente d'OCP" (Robert C. Martin), et le modèle de conception de la méthode de gabarit est un exemple classique de respect et de mise en œuvre de LSP, qui est l'une des solutions pour respecter OCP.

L'exemple classique de violation de LSP

Pour illustrer cela complètement, nous allons utiliser un exemple classique, car il est très significatif et facilement compréhensible..

class Rectangle private $ topLeft; private $ width; hauteur privée $; fonction publique setHeight ($ height) $ this-> height = $ height;  fonction publique getHeight () return $ this-> height;  fonction publique setWidth ($ width) $ this-> width = $ width;  fonction publique getWidth () return $ this-> width; 

Nous commençons avec une forme géométrique de base, une Rectangle. C’est juste un simple objet de données avec des setters et des getters pour largeur et la taille. Imaginez que notre application fonctionne et qu'elle soit déjà déployée sur plusieurs clients. Maintenant, ils ont besoin d'une nouvelle fonctionnalité. Ils doivent pouvoir manipuler des carrés.

Dans la vie réelle, en géométrie, un carré est une forme particulière de rectangle. Donc, nous pourrions essayer de mettre en œuvre un Carré classe qui étend un Rectangle classe. On dit souvent qu'une classe d'enfants est un classe parente, et cette expression est également conforme à LSP, au moins à première vue.


Mais est un Carré vraiment un Rectangle en programmation?

classe Square s étend Rectangle fonction publique setHeight ($ value) $ this-> width = $ value; $ this-> height = $ value;  fonction publique setWidth ($ value) $ this-> width = $ value; $ this-> height = $ value; 

Un carré est un rectangle d'égale largeur et hauteur, et nous pourrions faire une implémentation étrange comme dans l'exemple ci-dessus. Nous pourrions écraser les deux réglages pour définir la hauteur et la largeur. Mais comment cela affecterait le code client?

class Client function areaVerifier (Rectangle $ r) $ r-> setWidth (5); $ r-> setHeight (4); if ($ r-> area ()! = 20) lance new Exception ('Bad area!');  return true; 

Il est envisageable d'avoir une classe client qui vérifie la surface du rectangle et lève une exception si elle est fausse.

zone de fonction () return $ this-> width * $ this-> height; 

Bien sûr, nous avons ajouté la méthode ci-dessus à notre Rectangle classe pour fournir la zone.

La classe LspTest étend PHPUnit_Framework_TestCase function testRectangleArea () $ r = new Rectangle (); $ c = nouveau client (); $ this-> assertTrue ($ c-> areaVerifier ($ r)); 

Et nous avons créé un test simple en envoyant un objet rectangle vide au vérificateur de zone et le test réussit. Si notre Carré la classe est correctement définie, elle est envoyée au client areaVerifier () ne devrait pas casser sa fonctionnalité. Après tout, un Carré est un Rectangle dans tous les sens mathématique. Mais est notre classe?

fonction testSquareArea () $ r = new Square (); $ c = nouveau client (); $ this-> assertTrue ($ c-> areaVerifier ($ r)); 

Il est très facile de le tester et cela brise beaucoup de temps. Une exception nous est lancée lorsque nous effectuons le test ci-dessus.

PHPUnit 3.7.28 de Sebastian Bergmann. Exception: mauvais secteur! # 0 / paht /: /… /… /LspTest.php(18): Client-> areaVerifier (Object (Square)) # 1 [fonction interne]: LspTest-> testSquareArea ()

Donc notre Carré la classe n'est pas un Rectangle après tout. Il enfreint les lois de la géométrie. Il échoue et viole le principe de substitution de Liskov.

J'aime particulièrement cet exemple, car il ne viole pas seulement le LSP, il montre également que la programmation orientée objet ne consiste pas à mapper la vie réelle aux objets. Chaque objet de notre programme doit être une abstraction sur un concept. Si nous essayons de mapper des objets réels un-à-un à des objets programmés, nous échouerons presque toujours.

Le principe de séparation des interfaces

Le principe de responsabilité unique concerne les acteurs et une architecture de haut niveau. Le principe d'ouverture / fermeture concerne la conception de classe et les extensions de fonctionnalités. Le principe de substitution de Liskov concerne le sous-typage et l'héritage. Le principe de la séparation des interfaces (ISP) concerne la logique métier pour la communication des clients.

Dans toutes les applications modulaires, il doit exister une interface sur laquelle le client peut compter. Il peut s’agir d’entités de type Interface ou d’autres objets classiques implémentant des modèles de conception tels que Façades. Peu importe la solution utilisée. Il a toujours la même portée: communiquer au code client sur l'utilisation du module. Ces interfaces peuvent résider entre différents modules de la même application ou du même projet, ou entre un projet en tant que bibliothèque tierce servant un autre projet. Encore une fois, cela n'a pas d'importance. La communication est la communication et les clients sont les clients, quels que soient les auteurs du code..

Alors, comment devrions-nous définir ces interfaces? Nous pourrions réfléchir à notre module et exposer toutes les fonctionnalités que nous voulons lui offrir.


Cela semble être un bon début, un excellent moyen de définir ce que nous voulons implémenter dans notre module. Ou est-ce? Un début comme celui-ci mènera à l'une des deux implémentations possibles:

  • Un énorme Voiture ou Autobus classe mettant en œuvre toutes les méthodes sur le Véhicule interface. Seules les dimensions de telles classes devraient nous dire de les éviter à tout prix.
  • Ou, beaucoup de petites classes comme LightsControl, Contrôle de vitesse, ou RadioCD qui implémentent l’ensemble de l’interface mais fournissent en réalité quelque chose d’utile que pour les parties qu’ils implémentent.

Il est évident qu'aucune des solutions n'est acceptable pour implémenter notre logique métier.


Nous pourrions prendre une autre approche. Casser l'interface en morceaux, spécialisés pour chaque implémentation. Cela aiderait à utiliser de petites classes soucieuses de leur propre interface. Les objets implémentant les interfaces seront utilisés par les différents types de véhicules, comme voiture dans l'image ci-dessus. La voiture utilisera les implémentations mais dépendra des interfaces. Donc, un schéma comme celui ci-dessous peut être encore plus expressif.


Mais cela change fondamentalement notre perception de l'architecture. le Voiture devient le client au lieu de l'implémentation. Nous voulons toujours fournir à nos clients des moyens d’utiliser tout notre module, c’est un type de véhicule..


Supposons que nous ayons résolu le problème de mise en œuvre et que nous ayons une logique métier stable. Le plus simple est de fournir une interface unique avec toutes les implémentations et de laisser les clients, dans notre cas Station de bus, Autoroute, Chauffeur et ainsi de suite, pour utiliser ce que les gens veulent de l'implémentation de l'interface. En gros, cela déplace la responsabilité de la sélection du comportement vers les clients. Vous pouvez trouver ce type de solution dans de nombreuses applications plus anciennes.

Le principe de séparation des interfaces (ISP) stipule qu'aucun client ne doit être obligé de dépendre de méthodes qu'il n'utilise pas.

Cependant, cette solution a ses problèmes. Maintenant, tous les clients dépendent de toutes les méthodes. Pourquoi un Station de bus dépend de l’état des lumières du bus ou des canaux radio sélectionnés par le conducteur? Ça ne devrait pas. Mais si c'est le cas? Est-ce que ça importe? Eh bien, si nous pensons au principe de responsabilité unique, c'est un concept jumeau à celui-ci. Si Station de bus dépend de nombreuses implémentations individuelles, même pas utilisées, elle peut nécessiter des modifications si l’une des petites implémentations individuelles change. C’est particulièrement vrai pour les langages compilés, mais nous pouvons toujours voir l’effet de la LightControl changement impactant Station de bus. Ces choses ne devraient jamais arriver.

Les interfaces appartiennent à leurs clients et non aux implémentations. Ainsi, nous devrions toujours les concevoir de manière à satisfaire au mieux nos clients. Parfois nous pouvons, parfois nous ne pouvons pas connaître exactement nos clients. Mais lorsque nous le pouvons, nous devrions casser nos interfaces en plusieurs petites pour mieux répondre aux besoins exacts de nos clients..


Bien sûr, cela conduira à un certain degré de duplication. Mais rappelles-toi! Les interfaces ne sont que des définitions de nom de fonction simples. Il n'y a aucune implémentation d'aucune sorte de logique en eux. Donc, les duplications sont petites et gérables.

Ensuite, nous avons le grand avantage que les clients ne dépendent que de ce dont ils ont réellement besoin et qu’ils utilisent. Dans certains cas, les clients peuvent utiliser et avoir besoin de plusieurs interfaces, ce qui est correct, à condition qu'ils utilisent toutes les méthodes de toutes les interfaces dont ils dépendent..

Une autre astuce intéressante est que dans notre logique métier, une seule classe peut implémenter plusieurs interfaces si nécessaire. Nous pouvons donc fournir une seule implémentation pour toutes les méthodes communes entre les interfaces. Les interfaces séparées nous obligeront également à penser notre code davantage du point de vue du client, ce qui conduira à un couplage lâche et à des tests faciles. Nous avons donc non seulement amélioré notre code pour nos clients, mais également simplifié la compréhension, le test et la mise en œuvre..

Dernières pensées

LSP nous a appris pourquoi la réalité ne peut pas être représentée comme une relation individuelle avec des objets programmés et comment les sous-types doivent respecter leurs parents. Nous l'avons également mis à la lumière des autres principes que nous connaissions déjà.

ISP nous apprend à respecter nos clients plus que nous le pensions. Le respect de leurs besoins améliorera notre code et facilitera notre vie de programmeurs..

Merci pour votre temps.