Die im Folgenden erklärten Prinzipien orientieren sich an der Clean Architecture von Robert C. Martin (ISBN-13: 978-0-13-449416-6).
Barbara Liskov beschrieb eine Vorgehensweise zur Definition von Subtypen:
"Für jedes Objekt o1 des Typs S gibt es ein Objekt o2 des Typs T, sodass alle Programme P, die für den Typ T definiert sind, ihr Verhalten nicht verändern, wenn o1 durch o2 ersetzt wird. In diesem Fall ist S ein Subtyp von T."
Zur Verdeutlichung dieser abstrakten Idee sind konkrete Beispiele hilfreich.
Angenommen, Sie möchten Lizenzgebühren über die Klasse Abrechnung
berechnen, die auf einer Lizenz-Schnittstelle basiert. Diese Schnittstelle definiert die Funktion BerechneGebühr()
. Es gibt zwei Subtypen: PersonenLizenz
und UnternehmenLizenz
. Beide Lizenzarten berechnen die Gebühr mit unterschiedlichen Algorithmen.
Der Entwurf erfüllt das LSP, da das Verhalten der Abrechnung unabhängig davon bleibt, welcher Subtyp verwendet wird. Die Subtypen können Lizenz
nahtlos ersetzen.
Ein klassisches Beispiel für einen Verstoß gegen das LSP ist das Quadrat/Rechteck-Problem.
Ein Rechteck hat unabhängig voneinander veränderbare Breite und Höhe. Im Gegensatz dazu müssen beim Quadrat beide Dimensionen gemeinsam geändert werden. Dadurch ist ein Quadrat kein geeigneter Subtyp eines Rechtecks.
Das folgende Code-Beispiel verdeutlicht diese Problematik:
1using System;
2
3public class Program
4{
5 public static void Main()
6 {
7 Benutzer dummy = new Benutzer("Dully");
8 dummy.HantierttMitQuadratenUndRechtecken();
9 }
10
11 public class Rechteck
12 {
13 protected int hoehe;
14 protected int breite;
15
16 public Rechteck(int hoehe, int breite)
17 {
18 SetzeHoehe(hoehe);
19 SetzeBreite(breite);
20 }
21
22 public virtual void SetzeHoehe(int hoehe)
23 {
24 this.hoehe = hoehe;
25 }
26
27 public virtual void SetzeBreite(int breite)
28 {
29 this.breite = breite;
30 }
31
32 public virtual int Flaeche()
33 {
34 return hoehe * breite;
35 }
36 }
37
38 public class Quadrat : Rechteck
39 {
40 public Quadrat(int seite) : base(seite, seite)
41 {
42 }
43
44 public override void SetzeSeite(int seite)
45 {
46 this.hoehe = seite;
47 this.breite = seite;
48 }
49 }
1 public class Benutzer
2 {
3 string name;
4 public Benutzer(string name)
5 {
6 this.name = name;
7 }
8
9 public void HantierttMitQuadratenUndRechtecken()
10 {
11 Rechteck rechteck = new Rechteck(2, 5);
12 Quadrat quadrat = new Quadrat(2);
13 Rechteck imRechteckVerborgenesQuadrat = new Quadrat(3);
14 imRechteckVerborgenesQuadrat.SetzeBreite(5);
15 imRechteckVerborgenesQuadrat.SetzeHoehe(3);
16 Console.WriteLine(rechteck.Flaeche());
17 Console.WriteLine(quadrat.Flaeche());
18 Console.WriteLine(ImRechteckVerborgenesQuadrat.Flaeche());
19
20 PruefeRechteck(imRechtEckVerbogenesQuadrat);
21 }
22
23 public bool PruefeRechteck(Rechteck r)
24 {
25 bool result = r is Quadrat;
26 if (result)
27 {
28 Console.WriteLine("Quadrat");
29 }
30 else
31 {
32 Console.WriteLine("Kein Quadrat");
33 }
34 return result;
35 }
36 }
37}
Um solche Fehler zu vermeiden, sollte der Entwurf alternative Mechanismen zur Erkennung spezifischer Typen bereitstellen.
Das LSP wird häufig im Softwaredesign angewandt, insbesondere bei der Definition von Schnittstellen und Implementierungen. Diese können sich über verschiedene Ebenen hinweg erstrecken, wie Frontend, Services, Backend und externe Systeme.
Gut durchdachte Schnittstellen sind entscheidend, um einheitliche Implementierungen ohne Missverständnisse und Fehler zu ermöglichen.
In diesem Abschnitt wird erläutert, was das Liskov Substitution Principle (LSP) nicht ist. Stellen Sie sich vor, wir entwickeln eine Taxi-Bestell-Web-App, die einen Aggregator-Dienst implementiert. Dieser Dienst erstellt ein Angebot verschiedener Taxizentralen, aus dem Kunden eine Auswahl treffen können. Sobald ein Kunde sich für ein Angebot einer Taxizentrale entschieden hat, wird vom System der Fahrer zur Abholung des Kunden ermittelt.
Ein Beispiel: Der Fahrer Kai von DevwareTaxi GmbH besitzt die folgende URI:
devwaretaxi.wu/fahrer/kai
Unser System ergänzt diese URI mit Abholdaten für einen PUT-Request, der so aussehen könnte:
devwaretaxi.wuppertal/fahrer/kai/abholAdresse/Gueterstr.20/anfrageZeitstempel/20240705162450/anmerkung/JGA
Alle Taxiunternehmen, die Benachrichtigungen an das System senden möchten, müssen standardisiert mit der Schnittstelle kommunizieren. Das betrifft sowohl die Feldbezeichnungen (fahrer
, abholAdresse
, anfrageZeitstempel
, anmerkung
) als auch die Datenformatierung, die zwischen Absender und Empfänger übereinstimmen müssen.
Warum standardisierte Kommunikation erforderlich ist, wird im folgenden Beispiel deutlich:
Ein Entwickler eines Taxiunternehmens führt eine Abkürzung, wie etwa abholAdr
anstelle von abholAdresse
, ein. Um diese abweichende Anfrage im übergeordneten System zu verarbeiten, müsste im Endpunkt ein Sonderfall implementiert werden. Ein solcher Sonderfall könnte folgendermaßen aussehen:
1if (fahrer.HoleAbholURI().BeginntMit("AbweichendesTaxiunternehmen")
2{
3 //Dann mappe die Daten von abholAdr auf abholAdresse.
4}
Derartige Sonderfälle widersprechen einer dynamischen Softwarearchitektur. Allein die Abfrage, ob die URI mit einer spezifischen Domäne wie AbweichendesTaxiUnternehmen.de
übereinstimmt, ist bereits zu starr. Dadurch entstehen potenzielle Fehlerquellen und Sicherheitslücken.
Zusätzlich wird die Situation komplexer, wenn ein Taxiunternehmen ein anderes übernimmt und beide ihre Webseiten getrennt betreiben, während die Systeme zusammengeführt werden sollen. Müssten wir in einem solchen Szenario nicht einen weiteren Sonderfall implementieren?
1if (fahrer.HoleAbholURI().BeginntMit("WeiteresAbweichendesTaxiunternehmen")
2{
3 //Dann mappe die Daten von anfrageTS auf anfrageZeitstempel.
4 //und die Daten von bemerkung auf anmerkung.
5 //usw…
6}
Ein erfahrener Softwarearchitekt erkennt solche Probleme und isoliert sie. Eine Lösung wäre der Aufbau eines Abhol-Kommando-Erstellungs-Moduls, das mithilfe einer URI-Schlüssel-basierten Datenbanktabelle ein Interpretationsschema für jedes potenzielle Taxiunternehmen bereitstellt. Die Tabelle könnte so aussehen:
Dadurch würde ein komplexer Mechanismus eingebaut, um Sonderfälle zu behandeln, welche sonst nicht für die Schnittstelle geeignet wären.
Das LSP kann und sollte zur Erweiterung einer Architekturherangezogen werden. Allerdings kann die Missachtung einer gewissen Ersetzbarkeit mit zusätzlichen Mechanismen verbunden sein.
Unser Geschäftsführer Tibor Csizmadia und unser Kundenbetreuer Jens Walter stehen Ihnen persönlich zur Verfügung. Profitieren Sie von unserer langjährigen Erfahrung und erhalten Sie eine kompetente Erstberatung in einem unverbindlichen Austausch.