Grundsätzlich wollen wir, dass sich die Dinge vernünftig verhalten.
Betrachten Sie das folgende Problem:
Ich bekomme eine Gruppe von Rechtecken und möchte deren Fläche um 10% vergrößern. Ich stelle also die Länge des Rechtecks auf das 1,1-fache der vorherigen Länge ein.
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
foreach(var rectangle in rectangles)
{
rectangle.Length = rectangle.Length * 1.1;
}
}
In diesem Fall wurde die Länge aller Rechtecke um 10% erhöht, wodurch die Fläche um 10% vergrößert wird. Leider hat mir tatsächlich jemand eine Mischung aus Quadraten und Rechtecken übergeben, und als die Länge des Rechtecks geändert wurde, war auch die Breite verändert.
Meine Komponententests bestehen, weil ich alle meine Komponententests geschrieben habe, um eine Sammlung von Rechtecken zu verwenden. Ich habe jetzt einen subtilen Fehler in meine Anwendung eingefügt, der monatelang unbemerkt bleiben kann.
Schlimmer noch, Jim aus der Buchhaltung sieht meine Methode und schreibt einen anderen Code, der die Tatsache ausnutzt, dass er eine sehr schöne Vergrößerung von 21% erhält, wenn er Quadrate in meine Methode übergibt. Jim ist glücklich und niemand ist klüger.
Jim wird für hervorragende Arbeit in eine andere Abteilung befördert. Alfred tritt als Junior in das Unternehmen ein. In seinem ersten Fehlerbericht hat Jill von Advertising berichtet, dass das Übergeben von Quadraten an diese Methode zu einer Steigerung von 21% führt und den Fehler beheben möchte. Alfred erkennt, dass überall im Code Quadrate und Rechtecke verwendet werden, und erkennt, dass es unmöglich ist, die Vererbungskette zu durchbrechen. Er hat auch keinen Zugriff auf den Quellcode des Rechnungswesens. Also behebt Alfred den Fehler folgendermaßen:
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
foreach(var rectangle in rectangles)
{
if (typeof(rectangle) == Rectangle)
{
rectangle.Length = rectangle.Length * 1.1;
}
if (typeof(rectangle) == Square)
{
rectangle.Length = rectangle.Length * 1.04880884817;
}
}
}
Alfred ist mit seinen überragenden Hacking-Fähigkeiten zufrieden und Jill meldet, dass der Fehler behoben ist.
Im nächsten Monat niemand wird bezahlt , weil Accounting abhängig war auf die Möglichkeit, Quadrate an die passieren IncreaseRectangleSizeByTenPercent
Verfahren und eine Erhöhung der Fläche von 21% bekommen. Das gesamte Unternehmen wechselt in den Bugfix-Modus "Priorität 1", um die Ursache des Problems zu ermitteln. Sie verfolgen das Problem zu Alfred's Verlegenheit. Sie wissen, dass sie sowohl Buchhaltung als auch Werbung glücklich machen müssen. Sie beheben das Problem, indem sie den Benutzer mit dem Methodenaufruf folgendermaßen identifizieren:
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
IncreaseRectangleSizeByTenPercent(
rectangles,
new User() { Department = Department.Accounting });
}
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles, User user)
{
foreach(var rectangle in rectangles)
{
if (typeof(rectangle) == Rectangle || user.Department == Department.Accounting)
{
rectangle.Length = rectangle.Length * 1.1;
}
else if (typeof(rectangle) == Square)
{
rectangle.Length = rectangle.Length * 1.04880884817;
}
}
}
Und so weiter und so fort.
Diese Anekdote basiert auf realen Situationen, mit denen Programmierer täglich konfrontiert sind. Verstöße gegen das Liskov-Substitutionsprinzip können sehr subtile Fehler hervorrufen, die erst Jahre nach dem Verfassen aufgedeckt werden. Zu diesem Zeitpunkt wird die Behebung des Verstoßes eine Reihe von Problemen lösen und Ihren größten Kunden verärgern , wenn sie nicht behoben werden.
Es gibt zwei realistische Möglichkeiten, dieses Problem zu beheben.
Der erste Weg ist, das Rechteck unveränderlich zu machen. Wenn der Benutzer von Rectangle die Eigenschaften Length und Width nicht ändern kann, wird dieses Problem behoben. Wenn Sie ein Rechteck mit einer anderen Länge und Breite möchten, erstellen Sie ein neues. Quadrate können glücklich von Rechtecken erben.
Die zweite Möglichkeit besteht darin, die Vererbungskette zwischen Quadraten und Rechtecken zu unterbrechen. Wenn ein Quadrat als mit einem einzelnen definiert ist SideLength
Eigentum und Rechtecken haben Length
und Width
Eigentum und es gibt keine Vererbung, dann ist es unmöglich, aus Versehen bricht durch ein Rechteck und bekommen einen Platz erwartet. In C # können Sie seal
Ihre Rechteckklasse verwenden, wodurch sichergestellt wird, dass alle Rechtecke, die Sie jemals erhalten, tatsächlich Rechtecke sind.
In diesem Fall gefällt mir die Methode "Unveränderliche Objekte", mit der das Problem behoben werden kann. Die Identität eines Rechtecks ist seine Länge und Breite. Wenn Sie die Identität eines Objekts ändern möchten, ist es sinnvoll, dass Sie wirklich ein neues Objekt möchten . Wenn Sie einen alten Kunden verlieren und einen neuen Kunden gewinnen, ändern Sie nicht das Customer.Id
Feld vom alten Kunden zum neuen Kunden, sondern erstellen einen neuen Customer
.
Verstöße gegen das Liskov-Substitutionsprinzip sind in der realen Welt weit verbreitet, vor allem, weil eine Menge Code von Leuten geschrieben wird, die inkompetent sind / unter Zeitdruck stehen / sich nicht darum kümmern / Fehler machen. Es kann und führt zu einigen sehr schlimmen Problemen. In den meisten Fällen möchten Sie stattdessen die Komposition der Vererbung vorziehen.