Verstößt das staatliche Muster gegen das Liskov-Substitutionsprinzip?


17

Dieses Bild stammt aus Anwenden domänengesteuerter Designs und Muster: Mit Beispielen in C # und .NET

Bildbeschreibung hier eingeben

Dies ist das Klassendiagramm für das Zustandsmuster, in dem a SalesOrderwährend seiner Lebensdauer verschiedene Zustände haben kann. Es sind nur bestimmte Übergänge zwischen den verschiedenen Zuständen zulässig.

Jetzt ist die OrderStateKlasse eine abstractKlasse und alle ihre Methoden werden an ihre Unterklassen vererbt. Wenn wir die Unterklasse betrachten, Cancelleddie ein Endzustand ist, der keine Übergänge zu anderen Zuständen zulässt, müssen wir overridealle Methoden in dieser Klasse verwenden, um Ausnahmen auszulösen.

Verstößt das nicht gegen Liskovs Substitutionsprinzip, da eine Unterklasse das Verhalten der Eltern nicht ändern sollte? Behebt das Ändern der abstrakten Klasse in eine Schnittstelle dieses Problem?
Wie kann das behoben werden?


Ändern Sie OrderState zu einer konkreten Klasse, die standardmäßig in allen Methoden "NotSupported" -Ausnahmen auslöst?
James

Ich habe dieses Buch noch nicht gelesen, aber es scheint mir seltsam, dass OrderState Cancel () enthält und es dann den Status Canceled gibt, der einen anderen Untertyp darstellt.
Euphoric

@Euphoric warum ist es komisch? Aufrufen cancel()ändert den Status in abgebrochen.
Songo

Wie? Wie können Sie Cancel in OrderState aufrufen, wenn sich die Statusinstanz in SalesOrder ändern soll? Ich denke das ganze Modell ist falsch. Ich würde gerne eine vollständige Umsetzung sehen.
Euphoric

Antworten:


3

Diese besondere Implementierung, ja. Wenn Sie die Zustände zu konkreten Klassen machen und nicht zu abstrakten Implementierern, werden Sie davon abkommen.

Das Zustandsmuster, auf das Sie sich beziehen, ist jedoch im Allgemeinen etwas, mit dem ich nicht einverstanden bin, weil ich gesehen habe, wie es wächst. Ich denke, es gibt genügend Gründe, dies als Verstoß gegen das Prinzip der Einzelverantwortung zu bezeichnen, da diese Zustandsverwaltungsmuster letztendlich zentrale Aufbewahrungsorte für die Kenntnis des aktuellen Zustands vieler anderer Teile eines Systems sind. Diese zentralisierte Zustandsverwaltung erfordert häufig Geschäftsregeln, die für viele verschiedene Teile des Systems relevant sind, um sie angemessen zu koordinieren.

Stellen Sie sich vor, alle Teile des Systems, die sich um den Zustand kümmern, befänden sich in unterschiedlichen Diensten, unterschiedlichen Prozessen auf unterschiedlichen Maschinen, ein zentraler Statusmanager, der den Status für jeden dieser Bereiche detailliert angibt, führt effektiv zu Engpässen im gesamten verteilten System, und ich halte den Engpass für ein Zeichen einer SRP-Verletzung sowie allgemein schlechtes Design.

Im Gegensatz dazu würde ich vorschlagen, Objekte intelligenter zu gestalten als das Modellobjekt im MVC-Muster, bei dem das Modell weiß, wie es sich selbst behandelt, und keinen externen Orchestrator für die Verwaltung seiner internen Abläufe oder Gründe benötigt.

Selbst wenn Sie ein Statusmuster wie dieses in ein Objekt einfügen, damit es nur von selbst verwaltet wird, haben Sie das Gefühl, dass Sie dieses Objekt zu groß machen würden. Workflows sollten durch die Komposition verschiedener selbstverantwortlicher Objekte erfolgen, anstatt mit einem einzelnen orchestrierten Zustand, der den Fluss anderer Objekte oder den Fluss der Intelligenz in sich selbst verwaltet.

Aber an diesem Punkt ist es mehr Kunst als Engineering- und so ist es auf jeden Fall Ihren Ansatz dieser Dinge einig subjektiv ist, die besagten , die Prinzipien sind eine gute Anleitung und ja die Implementierung von Liste ist eine LSP Verletzung, kann aber nicht sein korrigiert werden. Seien Sie nur sehr vorsichtig mit der SRP, wenn Sie ein Muster dieser Art verwenden, und Sie sind wahrscheinlich sicher.


Grundsätzlich besteht das große Problem darin, dass die Objektorientierung für das Hinzufügen neuer Klassen gut ist, das Hinzufügen neuer Methoden jedoch erschwert. Und wie Sie bereits sagten, ist es im Fall von state unwahrscheinlich, dass Sie den Code sehr oft mit neuen States erweitern müssen.
Hugomg

3
@ Jimmy Hoffa Entschuldigung, aber was meinst du genau mit "Wenn du die Zustände zu konkreten Klassen anstatt zu abstrakten Implementierern machst, dann kommst du davon weg." ?
Songo

@ Jimmy: Ich habe versucht, den Status ohne das Statusmuster zu implementieren, indem jede Komponente nur ihren eigenen Status kennt. Dies führte dazu, dass große Änderungen wesentlich komplexer in der Implementierung und Wartung waren. Ich denke jedoch, dass Sie eine Zustandsmaschine (im Idealfall die Bibliothek eines anderen, um sie als Black Box zu behandeln) anstelle des Zustandsentwurfsmusters verwenden (daher sind Zustände nur Elemente in einer Aufzählung und keine vollständigen Klassen, und Übergänge sind nur dumm) Kanten) bietet viele der Vorteile des Zustandsmusters, während es den Versuchen von Entwicklern, es zu missbrauchen, feindlich gegenübersteht.
Brian

8

Eine Unterklasse sollte das Verhalten der Eltern nicht verändern.

Das ist eine häufige Fehlinterpretation von LSP. Eine Unterklasse kann das Verhalten des übergeordneten Elements ändern, solange es dem übergeordneten Elementtyp entspricht.

Es gibt eine lange Erklärung auf Wikipedia , die die Dinge vorschlägt, die gegen LSP verstoßen:

... gibt es eine Reihe von Verhaltensbedingungen, die der Subtyp erfüllen muss. Diese werden in einer Terminologie detailliert beschrieben, die der Entwurfsmethode nach Verträgen ähnelt, was zu einigen Einschränkungen bei der Interaktion von Verträgen mit der Vererbung führt:

  • Voraussetzungen können in einem Subtyp nicht gestärkt werden.
  • Nachbedingungen können in einem Subtyp nicht abgeschwächt werden.
  • Invarianten des Supertyps müssen in einem Subtyp erhalten bleiben.
  • Verlaufsbeschränkung (die "Verlaufsregel"). Objekte gelten nur durch ihre Methoden als veränderbar (Kapselung). Da Subtypen Methoden einführen können, die im Supertyp nicht vorhanden sind, kann die Einführung dieser Methoden Zustandsänderungen im Subtyp ermöglichen, die im Supertyp nicht zulässig sind. Die Geschichtsbedingung verbietet dies. Es war das neuartige Element, das von Liskov und Wing eingeführt wurde. Ein Verstoß gegen diese Einschränkung kann beispielhaft dargestellt werden, indem ein MutablePoint als Untertyp eines ImmutablePoint definiert wird. Dies verstößt gegen die History-Einschränkung, da in der History des Unveränderlichen Punkts der Status nach der Erstellung immer derselbe ist, sodass die History eines MutablePoint im Allgemeinen nicht berücksichtigt werden kann. Dem Subtyp hinzugefügte Felder können jedoch sicher geändert werden, da sie mit den Supertype-Methoden nicht beobachtet werden können.

Persönlich fällt es mir leichter, mich daran zu erinnern: Wenn ich einen Parameter in einer Methode vom Typ A betrachte, würde mich jemand überraschen, der einen Subtyp B übergibt? Andernfalls liegt ein Verstoß gegen LSP vor.

Ist es eine Überraschung, eine Exception zu werfen? Nicht wirklich. Dies kann jederzeit passieren, unabhängig davon, ob ich die Ship-Methode bei OrderState oder Granted oder Shipped aufrufe. Also muss ich das erklären und es ist nicht wirklich ein Verstoß gegen LSP.

Trotzdem denke ich, dass es bessere Möglichkeiten gibt, mit dieser Situation umzugehen. Wenn ich dies in C # schreiben würde, würde ich Interfaces verwenden und nach der Implementierung eines Interfaces suchen, bevor ich die Methode aufrufe. Wenn der aktuelle OrderState beispielsweise IShippable nicht implementiert, rufen Sie keine Ship-Methode auf.

Aber dann würde ich auch nicht das Zustandsmuster für diese spezielle Situation verwenden. Das Statusmuster ist dem Status einer Anwendung viel angemessener als dem Status eines solchen Domänenobjekts.

Kurz gesagt, dies ist ein schlecht erfundenes Beispiel für das Zustandsmuster und keine besonders gute Möglichkeit, mit dem Zustand eines Auftrags umzugehen. Aber es verletzt wohl nicht LSP. Und das staatliche Muster an und für sich tut es mit Sicherheit nicht.


Es klingt für mich so, als würden Sie LSP mit dem Prinzip der geringsten Überraschung / des geringsten Erstaunens verwechseln. Ich glaube, dass LSP klarere Grenzen hat, wo dies gegen sie verstößt, obwohl Sie das subjektivere POLA anwenden, das auf subjektiven Erwartungen basiert ; das ist, dass jeder etwas anderes erwartet als der andere. LSP basiert auf vertraglichen und genau definierten Garantien des Anbieters und nicht auf subjektiv bewerteten Erwartungen des Verbrauchers.
Jimmy Hoffa

@JimmyHoffa: Sie haben Recht, und deshalb habe ich die genaue Bedeutung dargelegt, bevor ich eine einfachere Methode zum Erinnern an LSP angegeben habe. Wenn ich jedoch eine Frage habe, was "Überraschung" bedeutet, gehe ich zurück und überprüfe die genauen Regeln von LSP (können Sie sie wirklich nach Belieben zurückrufen?). Ausnahmen, die die Funktionalität ersetzen (oder umgekehrt), sind schwierig, da sie nicht gegen eine bestimmte Klausel verstoßen. Dies ist wahrscheinlich ein Hinweis darauf, dass sie auf eine völlig andere Weise behandelt werden sollten.
pdr

1
Eine Ausnahme ist eine erwartete Bedingung, die im Vertrag definiert ist. Denken Sie an ein NetworkSocket. Es kann eine erwartete Ausnahme beim Senden geben, wenn es nicht geöffnet ist. Wenn Ihre Implementierung diese Ausnahme nicht für ein geschlossenes Socket auslöst, das gegen LSP verstößt Wenn im Vertrag das Gegenteil angegeben ist und Ihr Subtyp die Ausnahme auslöst, verstoßen Sie gegen LSP. So klassifiziere ich Ausnahmen in LSP. Wie für das Erinnern an die Regeln; Ich könnte mich in diesem Punkt irren und kann es mir gerne mitteilen, aber mein Verständnis von LSP vs POLA ist im letzten Satz meines obigen Kommentars definiert.
Jimmy Hoffa

1
@JimmyHoffa: Ich hatte nie gedacht, dass POLA und LSP auch nur über Fernzugriff miteinander verbunden sind (ich denke an POLA in Bezug auf die Benutzeroberfläche), aber jetzt, da Sie darauf hingewiesen haben, sind sie es. Ich bin mir nicht sicher, ob sie sich in einem Konflikt befinden oder ob einer subjektiver ist als der andere. Ist LSP nicht eine frühe softwaretechnische Beschreibung von POLA? Ebenso wie ich frustriert bin, wenn Strg-C nicht Kopieren (POLA) ist, bin ich auch frustriert, wenn ein ImmutablePoint veränderbar ist, weil es sich tatsächlich um eine MutablePoint-Unterklasse (LSP) handelt.
pdr

1
Ich würde eine CircleWithFixedCenterButMutableRadiusals wahrscheinliche LSP-Verletzung betrachten, wenn der Verbrauchervertrag für ImmutablePointspezifiziert, dass X.equals(Y)Verbraucher X frei für Y und umgekehrt ersetzen können, und daher davon absehen müssen, die Klasse so zu verwenden, dass eine solche Substitution Probleme verursachen würde. Der Typ kann möglicherweise legitim definieren equals, dass alle Instanzen als eindeutig verglichen werden, aber ein solches Verhalten würde wahrscheinlich seine Nützlichkeit einschränken.
Supercat

2

(Dies ist aus Sicht von C # geschrieben, also keine geprüften Ausnahmen.)

Laut Wikipedia-Artikel über LSP ist eine der Bedingungen von LSP:

Von Methoden des Subtyps sollten keine neuen Ausnahmen ausgelöst werden, es sei denn, diese Ausnahmen sind selbst Subtypen von Ausnahmen, die von den Methoden des Supertyps ausgelöst werden.

Wie solltest du das verstehen? Was genau sind "Ausnahmen, die von den Methoden des Supertyps ausgelöst werden", wenn die Methoden des Supertyps abstrakt sind? Ich denke, das sind die Ausnahmen, die als die Ausnahmen dokumentiert sind, die durch die Methoden des Supertyps ausgelöst werden können.

Was dies bedeutet ist, dass, wenn OrderState.Ship()dokumentiert ist als etwas "wirft, InvalidOperationExceptionwenn diese Operationen nicht vom aktuellen Status unterstützt werden", dann denke ich, dass dieses Design LSP nicht kaputt macht. Wenn andererseits die Supertyp-Methoden nicht auf diese Weise dokumentiert werden, wird der LSP verletzt.

Dies bedeutet jedoch nicht, dass dies ein gutes Design ist. Sie sollten keine Ausnahmen für den normalen Kontrollfluss verwenden, dem dies sehr nahe zu kommen scheint. In einer realen Anwendung möchten Sie wahrscheinlich auch wissen, ob eine Operation ausgeführt werden kann, bevor Sie dies versuchen, z. B. um die Schaltfläche "Versenden" in der Benutzeroberfläche zu deaktivieren.


Genau das hat mich zum Nachdenken gebracht. Wenn Unterklassen Ausnahmen auslösen, die nicht im übergeordneten Objekt definiert sind, muss dies eine Verletzung des LSP sein.
Songo

Ich denke, dass in einer Sprache, in der Ausnahmen nicht überprüft werden, das Werfen von Ausnahmen keine Verletzung von LSP darstellt. Es ist nur ein Konzept einer Sprache selbst, das überall und jederzeit jede Ausnahme auslösen kann. Daher sollte jeder Client-Code dafür bereit sein.
Zapadlo
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.