Le modèle d'état viole-t-il le principe de substitution de Liskov?

17

Cette image est tirée de l' application de modèles et de modèles pilotés par domaine: avec des exemples en C # et .NET

entrez la description de l'image ici

Il s'agit du diagramme de classes du modèle d'état dans lequel un état SalesOrderpeut avoir différents états au cours de sa durée de vie. Seules certaines transitions sont autorisées entre les différents états.

Maintenant, la OrderStateclasse est une abstractclasse et toutes ses méthodes sont héritées de ses sous-classes. Si nous considérons la sous-classe Cancelledqui est un état final qui n'autorise aucune transition vers d'autres états, nous devrons utiliser overridetoutes ses méthodes dans cette classe pour lever des exceptions.

Maintenant, cela ne viole-t-il pas le principe de substitution de Liskov, car une sous-classe ne devrait pas modifier le comportement du parent? La modification de la classe abstraite en interface résout-elle cela?
Comment résoudre ce problème?

Songo
la source
Changer OrderState pour être une classe concrète qui lève par défaut des exceptions "NotSupported" dans toutes ses méthodes?
James
Je n'ai pas lu ce livre, mais il me semble bizarre que OrderState contienne Cancel () et puis il y ait l'état Canceled, c'est-à-dire un sous-type différent.
Euphoric
@Euphoric pourquoi est-ce bizarre? L'appel cancel()change l'état en annulé.
Songo
Comment? Comment pouvez-vous appeler Annuler sur OrderState, lorsque l'instance d'état dans SalesOrder est censée changer. Je pense que l'ensemble du modèle est faux. J'aimerais voir une mise en œuvre complète.
Euphoric

Réponses:

3

Cette implémentation particulière, oui. Si vous faites des états des classes concrètes plutôt que des implémenteurs abstraits, vous vous en sortirez.

Cependant, le modèle d'état auquel vous faites référence, qui est en fait une conception de machine d'état, est en général quelque chose avec lequel je ne suis pas d'accord avec la façon dont je l'ai vu grandir. Je pense qu'il existe des motifs suffisants pour dénoncer cela comme une violation du principe de responsabilité unique, car ces schémas de gestion de l'État finissent par être des référentiels centraux pour la connaissance de l'état actuel de nombreuses autres parties d'un système. Cette partie de la gestion de l'état étant centralisée nécessitera le plus souvent des règles commerciales pertinentes pour de nombreuses parties différentes du système pour les coordonner de manière raisonnable.

Imaginez si toutes les parties du système qui se soucient de l'état se trouvaient dans différents services, différents processus sur différentes machines, un gestionnaire d'état central qui détaille l'état de chacun de ces endroits goulot d'étranglement efficace de tout ce système distribué et je pense que le goulot d'étranglement est un signe d'une violation SRP ainsi que d'une conception généralement mauvaise.

En revanche, je suggérerais de rendre les objets plus intelligents comme dans l'objet Model dans le modèle MVC où le modèle sait comment se gérer, il n'a pas besoin d'un orchestrateur externe pour gérer son fonctionnement interne ou sa raison.

Même en plaçant un modèle d'état comme celui-ci à l'intérieur d'un objet afin qu'il ne se gère que lui-même, il semble que vous rendriez cet objet trop grand. Les workflows devraient être effectués par la composition de divers objets auto-responsables, je dirais, plutôt qu'avec un seul état orchestré qui gère le flux d'autres objets, ou le flux d'intelligence en lui-même.

Mais à ce stade, c'est plus de l'art que de l'ingénierie et il est donc certainement subjectif de votre approche de certaines de ces choses, cela dit que les principes sont un bon guide et oui l'implémentation que vous listez est une violation LSP, mais pourrait être corrigée de ne pas l'être. Faites très attention au SRP lorsque vous utilisez n'importe quel modèle de cette nature et vous serez probablement en sécurité.

Jimmy Hoffa
la source
Fondamentalement, le gros problème est que l'orientation des objets est bonne pour ajouter de nouvelles classes mais rend difficile l'ajout de nouvelles méthodes. Et dans le cas d'un État, comme vous l'avez dit, il est peu probable que vous ayez besoin d'étendre le code avec de nouveaux États très souvent.
hugomg
3
@Jimmy Hoffa Désolé, mais que voulez-vous dire exactement par "Si vous faites des états des classes concrètes plutôt que des implémenteurs abstraits, alors vous vous en sortirez." ?
Songo
@Jimmy: J'ai essayé d'implémenter l'état sans le modèle d'état en faisant en sorte que chaque composant ne connaisse que son propre état. Cela a fini par apporter des modifications importantes à un flux beaucoup plus complexe à mettre en œuvre et à maintenir. Cela dit, je pense que l'utilisation d'une machine à états (idéalement la bibliothèque de quelqu'un d'autre pour la forcer à être traitée comme une boîte noire) plutôt que le modèle de conception d'état (ainsi, les états ne sont que des éléments dans une énumération plutôt que des classes complètes et les transitions sont juste stupides bords) donne de nombreux avantages du modèle d'état tout en étant hostile aux tentatives des développeurs d'en abuser.
Brian
8

une sous-classe ne devrait pas modifier le comportement du parent?

C'est une mauvaise interprétation courante de LSP. Une sous-classe peut modifier le comportement du parent, tant qu'elle reste fidèle au type de parent.

Il y a une bonne longue explication sur Wikipedia , suggérant les choses qui violeront le LSP:

... il existe un certain nombre de conditions comportementales que le sous-type doit remplir. Celles-ci sont détaillées dans une terminologie ressemblant à celle de la méthodologie de conception par contrat, conduisant à certaines restrictions sur la façon dont les contrats peuvent interagir avec l'héritage:

  • Les conditions préalables ne peuvent pas être renforcées dans un sous-type.
  • Les post-conditions ne peuvent pas être affaiblies dans un sous-type.
  • Les invariants du supertype doivent être conservés dans un sous-type.
  • Contrainte d'historique (la "règle d'historique"). Les objets ne sont considérés comme modifiables que par leurs méthodes (encapsulation). Étant donné que les sous-types peuvent introduire des méthodes qui ne sont pas présentes dans le supertype, l'introduction de ces méthodes peut permettre des changements d'état dans le sous-type qui ne sont pas autorisés dans le supertype. La contrainte d'historique l'interdit. C'était l'élément roman introduit par Liskov et Wing. Une violation de cette contrainte peut être illustrée en définissant un MutablePoint comme un sous-type d'un ImmutablePoint. Il s'agit d'une violation de la contrainte d'historique, car dans l'historique du point immuable, l'état est toujours le même après la création, il ne peut donc pas inclure l'historique d'un MutablePoint en général. Les champs ajoutés au sous-type peuvent cependant être modifiés en toute sécurité car ils ne sont pas observables via les méthodes de supertype.

Personnellement, je trouve plus facile de simplement me souvenir de ceci: si je regarde un paramètre dans une méthode qui a le type A, est-ce que quelqu'un qui passe un sous-type B me surprendra? S'ils le faisaient, il y aurait alors violation du LSP.

Lancer une exception est-il une surprise? Pas vraiment. C'est quelque chose qui peut arriver à tout moment, que j'appelle la méthode Ship sur OrderState ou Granted ou Shipped. Je dois donc en tenir compte et ce n'est pas vraiment une violation de LSP.

Cela dit , je pense qu'il existe de meilleures façons de gérer cette situation. Si j'écrivais cela en C #, j'utiliserais des interfaces et vérifierais l'implémentation d'une interface avant d'appeler la méthode. Par exemple, si le OrderState actuel n'implémente pas IShippable, n'appelez pas de méthode Ship dessus.

Mais je n'utiliserais pas non plus le modèle d'État pour cette situation particulière. Le modèle d'état est beaucoup plus approprié à l'état d'une application qu'à l'état d'un objet de domaine comme celui-ci.

Donc, en un mot, c'est un exemple mal conçu du modèle d'état et pas un moyen particulièrement bon de gérer l'état d'une commande. Mais cela ne contrevient sans doute pas au LSP. Et le modèle d'État, en soi, ne l'est certainement pas.

pdr
la source
Il me semble que vous confondez le LSP avec le principe de la moindre surprise / étonnement, je crois que le LSP a des limites plus claires où cela les viole, bien que vous appliquiez le POLA plus subjectif qui est basé sur des attentes subjectives ; c'est-à-dire que chaque personne attend quelque chose de différent de la suivante. Le LSP est basé sur des garanties contractuelles et bien définies données par le prestataire, et non sur des attentes évaluées subjectivement devinées par le consommateur.
Jimmy Hoffa
@JimmyHoffa: Vous avez raison, et c'est pourquoi j'ai précisé la signification exacte avant de vous indiquer une manière plus simple de se souvenir de LSP. Cependant, s'il y a une question dans mon esprit ce que signifie "surprise" alors je vais revenir en arrière et vérifier les règles exactes de LSP (pouvez-vous vraiment les rappeler à volonté?). Les exceptions remplaçant la fonctionnalité (ou vice versa) sont difficiles car elles ne violent aucune clause spécifique ci-dessus, ce qui est probablement un indice qu'elle doit être traitée d'une manière entièrement différente.
pdr
1
Une exception est une condition attendue définie sur le contrat dans mon esprit, pensez à un NetworkSocket, il peut y avoir une exception attendue à l'envoi s'il n'est pas ouvert, si votre implémentation ne lève pas cette exception pour un socket fermé que vous violez LSP , si le contrat indique le contraire et que votre sous-type lève l'exception, vous violez le LSP. C'est plus ou moins ainsi que je classe les exceptions dans LSP. Quant à se souvenir des règles; Je peux me tromper sur ce point et n'hésitez pas à me le dire, mais ma compréhension de LSP vs POLA est définie dans la dernière phrase de mon commentaire ci-dessus.
Jimmy Hoffa
1
@JimmyHoffa: Je n'avais jamais considéré que POLA et LSP étaient même connectés à distance (je pense à POLA en termes d'interface utilisateur) mais, maintenant que vous l'avez souligné, ils le sont en quelque sorte. Je ne suis pas sûr qu'ils soient en conflit cependant, ou que l'un soit plus subjectif que l'autre. LSP n'est-il pas une première description de POLA en termes d'ingénierie logicielle? De la même manière que je suis frustré lorsque Ctrl-C n'est pas Copier (POLA), je suis également frustré lorsqu'un ImmutablePoint est modifiable car il s'agit en fait d'une sous-classe MutablePoint (LSP).
pdr
1
Je considérerais a CircleWithFixedCenterButMutableRadiuscomme une violation probable du LSP si le contrat de consommation pour ImmutablePointspécifie que si X.equals(Y), les consommateurs peuvent librement substituer X à Y, et vice versa, et doivent donc s'abstenir d'utiliser la classe de telle manière qu'une telle substitution causerait des problèmes. Le type pourrait être en mesure de définir légitimement de equalssorte que toutes les instances se compareraient comme distinctes, mais un tel comportement limiterait probablement son utilité.
supercat
2

(Ceci est écrit du point de vue de C #, donc pas d'exceptions vérifiées.)

Selon un article de Wikipedia sur le LSP , l'une des conditions du LSP est:

Aucune nouvelle exception ne doit être levée par les méthodes du sous-type, sauf lorsque ces exceptions sont elles-mêmes des sous-types d'exceptions levées par les méthodes du supertype.

Comment devez-vous comprendre cela? Que sont exactement les «exceptions levées par les méthodes du supertype» lorsque les méthodes du supertype sont abstraites? Je pense que ce sont les exceptions qui sont documentées comme les exceptions qui peuvent être levées par les méthodes du supertype.

Cela signifie que si OrderState.Ship()est documenté comme quelque chose comme «lance InvalidOperationExceptionsi ces opérations ne sont pas prises en charge par l'état actuel», alors je pense que cette conception ne rompt pas le LSP. D'un autre côté, si les méthodes de supertype ne sont pas documentées de cette façon, LSP est violé.

Mais cela ne signifie pas que c'est une bonne conception, vous ne devez pas utiliser d'exceptions pour le flux de contrôle normal, ce qui semble très proche. De plus, dans une application réelle, vous voudrez probablement savoir si une opération peut être effectuée avant de tenter de le faire, par exemple pour désactiver le bouton «Expédier» dans l'interface utilisateur.

svick
la source
En fait, c'est exactement ce point qui m'a fait réfléchir. Si les sous-classes lèvent des exceptions non définies dans le parent, cela doit être une violation de LSP.
Songo
Je pense que dans une langue où les exceptions ne sont pas vérifiées, les jeter n'est pas une violation de LSP. C'est juste un concept de langage lui-même, où n'importe où, n'importe quand, n'importe quelle exception peut être levée. Tout code client doit donc être prêt pour cela.
Zapadlo