Fahrradverleih: Vererbung, Liskov-Prinzip und polymorphe Preisberechnung
Ein Schülerteam modelliert für die Schule einen Fahrradverleih mit 24 Rädern, davon 10 E-Bikes und 3 Lastenräder. Im Entwurf erbt EBike von Fahrrad und überschreibt reserviere(): Bei Akkustand unter 20 % wird eine UnsupportedOperationException geworfen. Lastenrad erbt ebenfalls von Fahrrad, verlangt im Konstruktor jedoch eine zusätzliche Ladefläche, die die Oberklasse nicht kennt; deshalb reserviert das Team Lastenräder in einer separaten Methode mit dupliziertem Code. Eine Liste enthält alle Räder; beim Reservieren über die Oberklasse stürzt das Programm bei E-Bikes mit niedrigem Akkustand ab. Zusätzlich wird der Tagespreis derzeit hartkodiert mit 5 € pro Listeneintrag berechnet, obwohl E-Bikes 9 € und Lastenräder 14 € kosten. Pro Woche werden etwa 80 Reservierungen bearbeitet; ein Lastenrad wird häufig für Transporte am Nachmittag gebucht. Die Lehrkraft fordert ein Modell, das neue Radtypen ohne Änderung bestehender Berechnungsklassen erlaubt und die Reservierungsregeln nicht in die Unterklassen verlagert.
1. Erläutere, welche Prinzipien der objektorientierten Modellierung verletzt werden, und erkläre den Laufzeitabsturz bei der Liste<Fahrrad>.
Read the worked answer
Verletzt wird das Liskov-Substitutionsprinzip: Fahrrad.reserviere() verspricht offenbar, dass eine Reservierung möglich ist oder ein definiertes Ergebnis liefert. EBike verstärkt die Vorbedingung (Akku ≥ 20 %) und wirft eine unchecked Exception. Ein Aufrufer, der nur List kennt, kann damit nicht rechnen; die Liste bricht zur Laufzeit. Die zusätzliche Ladefläche im Lastenrad-Konstruktor und die separate Reservierungsmethode zeigen, dass die Vererbung nicht sauber ist: Lastenrad ist kein substituierbares Fahrrad im Sinne des Konstruktorvertrags, und Code wird dupliziert.
Die Preisberechnung mit hartkodierten 5 € verletzt zusätzlich das Open-Closed-Prinzip, weil jeder neue Typ die Berechnungsklasse ändert. Die Oberklasse sollte einen stabilen Vertrag haben, z. B. reserviere() liefert ein Ergebnisobjekt (Erfolg, Grund) statt einer Ausnahme; die Akkuregel gehört in den Reservierungsservice oder in eine eigene Policy. Extra-Zustand von Lastenrad kann über Komposition (Ladefläche) statt Vererbung modelliert werden.
2. Entwirf ein überarbeitetes Klassendiagramm in Textform. Begründe, wo Vererbung und wo Komposition sinnvoll ist, und wie die Reservierungsregeln zentralisiert werden.
Read the worked answer
Textuelles Diagramm: interface Vermietbar { Reservierungsergebnis reserviere(Kunde k); BigDecimal preisProTag(); }. abstract class Fahrrad implements Vermietbar mit gemeinsamen Attributen (id, rahmenhoehe, verfuegbar). Klassen CityRad, EBike, Lastenrad erben von Fahrrad; Lastenrad hat zusätzlich ein Feld ladeflaeche (Komposition), EBike hat akkuLadung. Die Reservierung erfolgt über einen Reservierungsservice, der Prüfregeln (Akku, Verfügbarkeit, Führerschein) zentral anwendet und ein Ergebnisobjekt zurückgibt. Preis ist polymorph: Jede Klasse implementiert preisProTag().
Begründung: Vererbung nur für echte Ist-ein-Beziehungen mit identischem Vertrag; Komposition für austauschbare Teile (Ladefläche, Akku). Ein neuer Typ Tandem erbt von Fahrrad und implementiert preisProTag() mit 12 €, ohne bestehende Berechnungsklassen zu ändern. Die Liste wird zu List. Der Service macht die Regeln testbar und verhindert, dass Unterklassen ihre Vorbedingungen verschärfen.
3. Implementiere die Preisberechnung polymorph in Java und erläutere, wie der neue Typ Tandem mit 12 € pro Tag ergänzt wird, ohne bestehenden Code zu ändern. Prüfe deine Lösung mit einem konkreten Zahlenbeispiel.
Read the worked answer
abstract class Fahrrad implements Vermietbar {
public abstract BigDecimal preisProTag();
}
class EBike extends Fahrrad {
public BigDecimal preisProTag(){ return BigDecimal.valueOf(9); }
}
class Lastenrad extends Fahrrad {
public BigDecimal preisProTag(){ return BigDecimal.valueOf(14); }
}
class Tandem extends Fahrrad {
public BigDecimal preisProTag(){ return BigDecimal.valueOf(12); }
}
// Berechnung:
BigDecimal summe = raeder.stream()
.map(Fahrrad::preisProTag)
.reduce(BigDecimal.ZERO, BigDecimal::add);
Erklärung: Die Schleife kennt nur den abstrakten Vertrag; neue Klasse erweitert, ohne den Stream-Code zu ändern (Open-Closed). Keine instanceof-Kaskade. Zahlenbeispiel: 10 E-Bikes, 3 Lastenräder und 11 CityRäder ergeben 10 × 9 + 3 × 14 + 11 × 5 = 90 + 42 + 55 = 187 € pro Tag, wenn alle vermietet sind. Der neue Typ Tandem fügt 12 € hinzu, ohne die Berechnung zu ändern.