Verstoßen Sonderfälle mit Ausweichmanövern gegen das Liskov-Substitutionsprinzip?

20

Angenommen, ich habe eine Schnittstelle FooInterfacemit der folgenden Signatur:

interface FooInterface {
    public function doSomething(SomethingInterface something);
}

Und eine konkrete Klasse ConcreteFoo, die diese Schnittstelle implementiert:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
    }

}

Ich möchte ConcreteFoo::doSomething()etwas Einzigartiges tun, wenn es einem bestimmten SomethingInterfaceObjekttyp übergeben wird (sagen wir, es heißt SpecialSomething).

Es ist definitiv eine LSP-Verletzung, wenn ich die Voraussetzungen der Methode verbessere oder eine neue Ausnahme auslöse. Wäre es dennoch eine LSP-Verletzung, wenn ich SpecialSomethingObjekte mit einem speziellen Gehäuse ausführe und gleichzeitig einen Fallback für generische SomethingInterfaceObjekte bereitstelle ? So etwas wie:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
        if (something instanceof SpecialSomething) {
            // Do SpecialSomething magic
        }
        else {
            // Do generic SomethingInterface magic
        }
    }

}
Evan
quelle

Antworten:

19

Es könnte eine Verletzung von LSP sein, je nachdem , was sonst im Vertrag für das doSomethingVerfahren. Aber es ist mit ziemlicher Sicherheit ein Codegeruch, auch wenn er nicht gegen LSP verstößt.

Wenn es zum Beispiel Teil des Vertrags von doSomethingist, dass er something.commitUpdates()mindestens einmal anruft commitSpecialUpdates(), bevor er zurückkehrt, und für den speziellen Fall, dass er stattdessen anruft , dann ist das eine Verletzung von LSP. Selbst wenn SpecialSomethingdie commitSpecialUpdates()Methode bewusst darauf ausgelegt wäre, all das gleiche zu tun wie commitUpdates(), ist dies nur ein vorbeugendes Hacken um die LSP-Verletzung und genau die Art von Hackery, die man nicht machen müsste, wenn man LSP konsequent befolgt. Ob so etwas auf Ihren Fall zutrifft, müssen Sie herausfinden, indem Sie in Ihrem Vertrag nach dieser Methode suchen (ob explizit oder implizit).

Der Grund, warum dies ein Codegeruch ist, ist, dass das Überprüfen des konkreten Typs eines Ihrer Argumente den Punkt verfehlt, an dem ein Interface / abstrakter Typ dafür definiert wird, und weil Sie im Prinzip nicht mehr garantieren können, dass die Methode überhaupt funktioniert (stellen Sie sich vor) wenn jemand eine Unterklasse von SpecialSomethingmit der Annahme schreibt, dass commitUpdates()sie aufgerufen wird). Versuchen Sie zunächst, diese speziellen Aktualisierungen in den vorhandenen zu integrierenSomethingInterface; das ist das bestmögliche Ergebnis. Wenn Sie sich wirklich sicher sind, dass Sie das nicht können, müssen Sie die Benutzeroberfläche aktualisieren. Wenn Sie die Schnittstelle nicht steuern, müssen Sie möglicherweise überlegen, eine eigene Schnittstelle zu schreiben, die das tut, was Sie wollen. Wenn Sie nicht einmal eine Schnittstelle finden können, die für alle von ihnen funktioniert, sollten Sie die Schnittstelle möglicherweise komplett überarbeiten und mehrere Methoden verwenden, die unterschiedliche konkrete Typen verwenden, oder es ist möglicherweise ein noch größerer Refaktor angebracht. Wir müssen mehr über die Magie wissen, die Sie auskommentiert haben, um festzustellen, welche davon angemessen ist.

Ixrec
quelle
Vielen Dank! Das hilft. Hypothetisch wäre der Zweck der doSomething()Methode die Typkonvertierung in SpecialSomething: Wenn sie empfangen SpecialSomethingwürde, würde sie das Objekt nur unverändert zurückgeben, während sie bei Empfang eines generischen SomethingInterfaceObjekts einen Algorithmus ausführen würde, um es in ein SpecialSomethingObjekt zu konvertieren . Da die Vor- und Nachbedingungen gleich bleiben, glaube ich nicht, dass der Vertrag verletzt wurde.
Evan
1
@Evan Oh wow ... das ist ein interessanter Fall. Das könnte eigentlich völlig unproblematisch sein. Das Einzige, woran ich denken kann, ist, dass, wenn Sie das vorhandene Objekt zurückgeben, anstatt ein neues Objekt zu erstellen , möglicherweise jemand von dieser Methode abhängt, die ein brandneues Objekt zurückgibt die Sprache. Ist es möglich , jemand zu anrufen y = doSomething(x), dann x.setFoo(3), und findet dann , dass die y.getFoo()Rendite 3?
Ixrec
Das ist abhängig von der Sprache problematisch, sollte aber leicht zu umgehen sein, indem Sie SpecialSomethingstattdessen eine Kopie des Objekts zurückgeben. Obwohl der Reinheit halber, konnte ich auch sehen, dass die Sonderfalloptimierung beim Übergeben des Objekts wegfiel SpecialSomethingund nur den größeren Konvertierungsalgorithmus durchlief, da es immer noch funktionieren sollte, da es auch ein SomethingInterfaceObjekt ist.
Evan
1

Es ist keine Verletzung des LSP. Es ist jedoch immer noch ein Verstoß gegen die Regel "Keine Typprüfung durchführen". Es wäre besser, den Code so zu gestalten, dass das Besondere natürlich entsteht. Vielleicht SomethingInterfacebraucht man ein anderes Mitglied, das dies erreichen könnte, oder man muss irgendwo eine abstrakte Fabrik einbauen.

Dies ist jedoch keine feste Regel, daher müssen Sie entscheiden, ob sich der Kompromiss lohnt. Im Moment haben Sie einen Codegeruch und ein mögliches Hindernis für zukünftige Verbesserungen. Es loszuwerden, könnte eine wesentlich komplexere Architektur bedeuten. Ohne weitere Informationen kann ich nicht sagen, welches besser ist.

Sebastian Redl
quelle
1

Nein, wenn Sie die Tatsache ausnutzen, dass ein gegebenes Argument nicht nur die Schnittstelle A, sondern auch A2 bereitstellt, wird der LSP nicht verletzt.

Stellen Sie nur sicher, dass der spezielle Pfad keine stärkeren Vorbedingungen (abgesehen von denjenigen, die bei der Entscheidung für ihn getestet wurden) und keine schwächeren Nachbedingungen aufweist.

C ++ - Vorlagen tun dies häufig, um eine bessere Leistung zu erzielen, z. B. indem sie InputIterators erfordern , aber zusätzliche Garantien geben, wenn sie mit RandomAccessIterators aufgerufen werden .

Wenn Sie sich stattdessen zur Laufzeit entscheiden müssen (z. B. mithilfe von dynamischem Casting), müssen Sie sich vor der Entscheidung hüten, welchen Weg Sie einschlagen, um all Ihre potenziellen Gewinne oder noch mehr zu verbrauchen.

Das Ausnutzen des Sonderfalls wirkt sich häufig negativ auf DRY aus (wiederholen Sie sich nicht), da Sie möglicherweise Code duplizieren müssen, und auf KISS (Keep it Simple), da dieser komplexer ist.

Deduplizierer
quelle
0

Es gibt einen Kompromiss zwischen dem Prinzip "Keine Typprüfungen durchführen" und "Schnittstellen trennen". Wenn viele Klassen ein praktikables, aber möglicherweise ineffizientes Mittel zum Ausführen einiger Aufgaben bereitstellen, und einige von ihnen auch ein besseres Mittel bereitstellen können, besteht ein Bedarf an Code, der alle Elemente der breiteren Kategorie akzeptieren kann, die die Aufgabe ausführen können (Vielleicht ineffizient) Wenn Sie die Aufgabe jedoch so effizient wie möglich ausführen möchten, müssen entweder alle Objekte eine Schnittstelle implementieren, die ein Mitglied enthält, das angibt, ob die effizientere Methode unterstützt wird, oder eine andere, die sie verwendet, wenn dies der Fall ist Lassen Sie den Code, der das Objekt empfängt, prüfen, ob es eine erweiterte Schnittstelle unterstützt, und setzen Sie sie gegebenenfalls um.

Persönlich bevorzuge ich den ersteren Ansatz, obwohl ich mir wünsche, dass objektorientierte Frameworks wie .NET es Schnittstellen ermöglichen würden, Standardmethoden anzugeben (wodurch die Arbeit mit größeren Schnittstellen weniger schmerzhaft wird). Wenn die allgemeine Schnittstelle optionale Methoden enthält, kann eine einzelne Wrapper-Klasse Objekte mit vielen verschiedenen Fähigkeitskombinationen verarbeiten, wobei nur die Fähigkeiten für Verbraucher in Frage kommen, die im ursprünglichen Wrapper-Objekt vorhanden sind. Wenn viele Funktionen auf verschiedene Schnittstellen aufgeteilt sind, wird für jede andere Kombination von Schnittstellen, die umgebrochene Objekte möglicherweise unterstützen müssen, ein anderes Wrapper-Objekt benötigt.

Superkatze
quelle
0

Beim Liskov-Substitutionsprinzip handelt es sich um Subtypen, die gemäß einem Vertrag ihres Supertyps handeln. Wie Ixrec schrieb, gibt es nicht genügend Informationen, um zu beantworten, ob es sich um eine LSP-Verletzung handelt.

Was hier jedoch verletzt wird, ist ein offenes geschlossenes Prinzip. Wenn Sie eine neue Anforderung haben - Do SpecialSomething Magic - und Sie vorhandenen Code ändern müssen, verstoßen Sie definitiv gegen OCP .

Zapadlo
quelle