Solid Reihe - Liskov Substitution Principle

Liskov Principle: Schnittstellenfehler & Sicherheitslücken vermeiden
Kai P. | Softwareentwickler
12/2024

LSP – Liskov Substitution Principle

Die im Folgenden erklärten Prinzipien orientieren sich an der Clean Architecture von Robert C. Martin (ISBN-13: 978-0-13-449416-6).

Definition des LSP

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.

Beispiel 1: Subtypen und Vererbung

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.

Beispiel 2: Das Quadrat/Rechteck-Problem

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.

Architektur und das LSP

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.

Standardisierte Kommunikation

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.

Beispiel für fehlende Standardisierung

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.

Erweiterung durch Übernahmen

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}

Lösung: Abhol-Kommando-Erstellungs-Modul

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.

Zusammenfassung

Das LSP kann und sollte zur Erweiterung einer Architekturherangezogen werden. Allerdings kann die Missachtung einer gewissen Ersetzbarkeit mit zusätzlichen Mechanismen verbunden sein.

Mehr zum Thema

Pfeil nach rechts (Verlinkung)
FluentValidation in Blazor: Validierungslogik mit Custom Rules und Feldern
04/2025

Blazor FluentValidation | C# Validation Best Practices

Pfeil nach rechts (Verlinkung)
Open/Closed Principle: Erweiterbare Software dank klarer Struktur
11/2024

Solid Reihe - Open Close Principle

Pfeil nach rechts (Verlinkung)
Clean Code in C#: Strukturierter, verständlicher und wartbarer Code
11/2024

Clean Code Prinzipien | Lesbarer C# Code Tutorial

Pfeil nach rechts (Verlinkung)
Blazor Cancellation Token: Performance-Tuning und UX-Verbesserung
10/2024

Blazor Cancellation Token | C# Performance Optimierung

Pfeil nach rechts (Verlinkung)

Gemeinsam Großes schaffen

Devware GmbH verpflichtet sich, Ihre Privatsphäre zu schützen. Wir benötigen Ihre Kontaktinformationen, um Sie bezüglich unserer Produkte und Dienstleistungen zu kontaktieren. Mit Klick auf Absenden geben Sie sich damit einverstanden. Weitere Informationen finden Sie unter Datenschutz.
Vielen Dank für Ihr Vertrauen.
Unser Team prüft Ihre Anfrage sorgfältig und meldet sich in der Regel innerhalb von 48 Stunden bei Ihnen zurück.
Falls es besonders eilig ist, erreichen Sie uns auch telefonisch:
+ 49 (0) 202 478 269 0.
Da ist etwas schief gegangen beim Absenden des Formulars.