Übungsaufgaben zum Einsatz von Vererbung: Liskov Substitution Principle und Favour Composition over Inheritance
https://bildung.social/@oerinformatik/
https://oer-informatik.de/vererbung-ersetzbarkeit-uebungsaufgaben
Clean Code, Liskov Substitution Principle, Favour Composition over Inheritance, Vererbung, Komposition, Delegation, Design by Contract, fragile Basisklasse, UML-Klassendiagramm, Übungsaufgaben, Prüfungsvorbereitung
tl/dr; (Bearbeitungszeit insgesamt 55 min, mehrere Aufgaben): Drei Übungsaufgaben zum Einsatz von Vererbung: Du findest im Mitfahrlabor LSP-Verstöße bei den Benachrichtigungskanälen und behebst sie, baust in der CSB-Rad-Kata die Kurierzustellung ohne LSP-Verstoß ein und ersetzt eine Vererbungshierarchie aus Fahrtangebot, Mitfahrerliste und ArrayList durch Komposition, mit Klassendiagramm vor und nach dem Umbau [AI-Assisted]. (Zuletzt geändert am 10.11.2026)
Diese Übungsaufgaben gehören zum Artikel Vererbung richtig einsetzen: Liskov Substitution Principle und Favour Composition over Inheritance.
Hinweise zur Bearbeitung: Als FIA musst Du zur Teil-2-Prüfung Aufgaben auf Kernkompetenz-Ebene lösen können, das ist Aufgabe 2.1. Von FIA wird zusätzlich erwartet, dass ihnen Aufgaben auf Profilkompetenz-Ebene (Aufgabe 3.1 und Aufgabe 3.2) keine Probleme bereiten. Die Codeausschnitte stehen in Java und Python; wähle die Sprache, die Du gerade lernst, und schreib Deine Lösung in dieser Sprache. Für die Aufgaben 2.1, 3.1 und 3.2 brauchst Du eine Entwicklungsumgebung mit JUnit 5 bzw. pytest, damit Du nach jedem Umbauschritt die Tests ausführen kannst. Achte auf die Operatoren: „Nenne“ verlangt Stichworte, „Ordne zu“ verlangt die Zuordnung mit kurzer Begründung, „Erläutere“ verlangt eine Erklärung mit Beispiel aus dem Code, „Baue um“, „Implementiere“ und „Schreibe“ verlangen lauffähigen Code, „Zeichne“ verlangt ein UML-Klassendiagramm, „Beurteile“ und „Begründe“ verlangen eine begründete eigene Aussage. Schreib Deine Lösung zuerst vollständig auf, bevor Du die Beispiellösung aufklappst.
Du möchtest eigene Aufgaben zum Üben von einem KI-Sprachmodell generieren lassen?
Kopiere den Text in ein KI-Sprachmodell Deiner Wahl und passe Kompetenzstufe, Anzahl, Bearbeitungszeit und Szenario bei Bedarf an. Wie die Aufgaben aufgebaut sind, beschreibt der Artikel Wie sind die Aufgaben aufgebaut?. Prüfe die Ergebnisse mit dem Lehrartikel: KI-Sprachmodelle machen Fehler.
Erstelle neue Übungsaufgaben zum Thema „Einsatz von Vererbung: Liskov
Substitution Principle und Favour Composition over Inheritance“.
Bauplan: Halte Dich an Aufbau, Kompetenzstufen, Operatoren,
Anforderungsbereiche, Punkte und Zeitschätzung, die im Artikel
https://oer-informatik.de/aufgaben-aufbau beschrieben sind.
Fachliche Grundlage:
- https://oer-informatik.de/vererbung-ersetzbarkeit
- https://oer-informatik.de/klassen-schneiden
- https://oer-informatik.de/uml-klassendiagramm-vererbung
- https://oer-informatik.de/uml-klassendiagramm-polymorphie
- https://oer-informatik.de/refactoring
- https://oer-informatik.de/erweitern-statt-aendern
- Stilbeispiel: https://oer-informatik.de/vererbung-ersetzbarkeit-uebungsaufgaben
Die Aufgaben sollen Inhalte zu diesen Schlagwörtern abdecken:
- Clean Code
- Liskov Substitution Principle
- Favour Composition over Inheritance
- Vererbung
- Komposition
- Delegation
- Design by Contract
- fragile Basisklasse
- UML-Klassendiagramm
Kompetenzstufe: Profilkompetenz (alternativ: Basiskompetenz, Kernkompetenz, Expertise)
Anzahl: 1 Aufgabe mit Teilaufgaben
Bearbeitungszeit in Minuten ca.: <5 bis 30 min>
Szenario: eines der Szenarien aus dem Bauplan
Vorgehen:
1. Lies zuerst die verlinkten Artikel. Kannst Du keine Webseiten
abrufen, sag mir das, statt zu raten.
2. Steht bei einer Angabe noch ein Platzhalter in <…>, frag mich
zuerst danach. Richte Punkte und Zahl der Teilaufgaben nach der
Bearbeitungszeit (ca. 0,9 min je Punkt).
3. Gib je Aufgabe Titel mit Kompetenzstufe, Ausgangssituation und
Teilaufgaben mit Operator und Punktzahl aus, dazu die geschätzte
Bearbeitungszeit.
4. Gib die Beispiellösungen mit Punkteverteilung erst aus, wenn ich
danach frage.
Aufgabe 2.1: Benachrichtigungskanäle im Mitfahrlabor ersetzbar machen (Kernkompetenz)
(ca. 15 min Bearbeitungszeit)
Die Mitfahr-App Mitfahrlabor erinnert Mitfahrende vor einer gebuchten Fahrt. Dafür gibt es mehrere Kanäle: Mail, Push-Nachricht in der App und die Flurmonitore der Schule. Die Flurmonitore zeigen offene Angebote anonym an, also nie Namen oder persönliche Nachrichten. Push-Nachrichten erreichen nur Personen, die Push in der App aktiviert haben. Ein Teammitglied hat alle drei Kanäle als Unterklassen einer gemeinsamen Oberklasse Benachrichtigung angelegt:
// Paket de.mitfahrlabor, Stand vor dem Umbau
// ===== Person.java =====
package de.mitfahrlabor;
public record Person(String name, String email, boolean pushAktiv) {
}
// ===== Benachrichtigung.java =====
package de.mitfahrlabor;
import java.util.ArrayList;
import java.util.List;
/** Ein Kanal, über den die App Personen benachrichtigt. */
public abstract class Benachrichtigung {
protected final List<String> ausgang = new ArrayList<>();
/** Stellt der Person den Text zu; danach steht genau ein Eintrag mehr im Ausgang. */
public abstract void sende(Person person, String text);
public List<String> ausgang() {
return List.copyOf(ausgang);
}
}
// ===== MailBenachrichtigung.java =====
package de.mitfahrlabor;
public class MailBenachrichtigung extends Benachrichtigung {
@Override
public void sende(Person person, String text) {
ausgang.add("Mail an " + person.email() + ": " + text);
}
}
// ===== PushBenachrichtigung.java =====
package de.mitfahrlabor;
public class PushBenachrichtigung extends Benachrichtigung {
@Override
public void sende(Person person, String text) {
if (!person.pushAktiv()) {
throw new IllegalStateException("Push ist für " + person.name() + " nicht aktiviert");
}
ausgang.add("Push an " + person.name() + ": " + text);
}
}
// ===== FlurmonitorAnzeige.java =====
package de.mitfahrlabor;
public class FlurmonitorAnzeige extends Benachrichtigung {
@Override
public void sende(Person person, String text) {
throw new UnsupportedOperationException("Flurmonitore zeigen keine persönlichen Nachrichten");
}
public void zeige(String haltepunkt, String abfahrt, int freiePlaetze) {
ausgang.add(abfahrt + " ab " + haltepunkt + ": " + freiePlaetze + " Plätze frei");
}
}
// ===== Erinnerungsdienst.java =====
package de.mitfahrlabor;
import java.util.List;
public class Erinnerungsdienst {
private final List<Benachrichtigung> kanaele;
public Erinnerungsdienst(List<Benachrichtigung> kanaele) {
this.kanaele = List.copyOf(kanaele);
}
public void erinnere(List<Person> mitfahrende, String fahrt) {
for (Benachrichtigung kanal : kanaele) {
if (kanal instanceof FlurmonitorAnzeige) {
continue;
}
for (Person person : mitfahrende) {
if (kanal instanceof PushBenachrichtigung && !person.pushAktiv()) {
continue;
}
kanal.sende(person, "Erinnerung: " + fahrt);
}
}
}
}Diese Tests sind grün:
package de.mitfahrlabor;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.Test;
class ErinnerungsdienstTest {
private static final Person AYLIN = new Person("Aylin Kaya", "aylin.kaya@example.org", true);
private static final Person LUKAS = new Person("Lukas Hofmann", "lukas.hofmann@example.org", false);
private static final String FAHRT = "Mo 07:15 ab Gütersloh Hbf";
private final MailBenachrichtigung mail = new MailBenachrichtigung();
private final PushBenachrichtigung push = new PushBenachrichtigung();
@Test
void mailErreichtAlle() {
new Erinnerungsdienst(List.of(mail)).erinnere(List.of(AYLIN, LUKAS), FAHRT);
assertEquals(List.of("Mail an aylin.kaya@example.org: Erinnerung: " + FAHRT,
"Mail an lukas.hofmann@example.org: Erinnerung: " + FAHRT), mail.ausgang());
}
@Test
void pushNurBeiAktiviertemPush() {
new Erinnerungsdienst(List.of(mail, push)).erinnere(List.of(AYLIN, LUKAS), FAHRT);
assertEquals(List.of("Push an Aylin Kaya: Erinnerung: " + FAHRT), push.ausgang());
assertEquals(2, mail.ausgang().size());
}
@Test
void flurmonitorZeigtNurAnonymeAngebote() {
FlurmonitorAnzeige monitor = new FlurmonitorAnzeige();
monitor.zeige("Gütersloh Hbf", "07:15", 2);
assertEquals(List.of("07:15 ab Gütersloh Hbf: 2 Plätze frei"), monitor.ausgang());
}
// entfällt nach dem Umbau: Der Flurmonitor kann dann gar nicht mehr übergeben werden
@Test
void erinnerungsdienstUebergehtFlurmonitor() {
FlurmonitorAnzeige monitor = new FlurmonitorAnzeige();
new Erinnerungsdienst(List.of(mail, monitor)).erinnere(List.of(AYLIN), FAHRT);
assertEquals(List.of(), monitor.ausgang());
}
}a) Nenne drei Stellen im Code, an denen das Liskov Substitution Principle verletzt oder umgangen wird, und ordne jeder Stelle eine Art von Verstoß zu (Ausnahme in überschriebener Methode, verschärfte Vorbedingung, typabhängige Fallunterscheidung beim Aufrufer). (6 Punkte)
| Stelle | Art des Verstoßes |
|---|---|
FlurmonitorAnzeige.sende wirft immer eine UnsupportedOperationException bzw. NotImplementedError |
Ausnahme in überschriebener Methode: Der Kanal kann nicht, was die Oberklasse verspricht. |
PushBenachrichtigung.sende wirft, wenn Push nicht aktiviert ist |
verschärfte Vorbedingung: Die Oberklasse verlangt nur eine Person und einen Text. |
Erinnerungsdienst.erinnere mit instanceof/isinstance für FlurmonitorAnzeige und PushBenachrichtigung |
typabhängige Fallunterscheidung beim Aufrufer: Der Dienst muss die Unterklassen kennen, um die Verstöße zu umgehen. |
Bewertung: je Stelle 1 Punkt, je richtige Zuordnung 1 Punkt.
b) Erläutere am Vertrag der Methode sende (Vor- und Nachbedingung laut Kommentar), warum ein neuer Programmteil, der gegen Benachrichtigung geschrieben ist, mit diesen Unterklassen scheitern kann. Gib ein konkretes Beispiel an. (4 Punkte)
Der Vertrag von sende verlangt als Vorbedingung nur eine Person und einen Text und verspricht als Nachbedingung, dass danach genau ein Eintrag mehr im Ausgang steht (1 Punkt). PushBenachrichtigung verlangt zusätzlich pushAktiv, verschärft also die Vorbedingung; FlurmonitorAnzeige erfüllt die Nachbedingung nie (1 Punkt). Ein neuer Programmteil, etwa eine Funktion „Fahrt abgesagt“, die allen Bestätigten über alle Kanäle schreibt, kennt die Sonderfälle nicht: Sie bricht mit einer Ausnahme ab, sobald sie Lukas Hofmann (Push nicht aktiviert) über Push oder irgendwen über den Flurmonitor benachrichtigt (1 Punkt). Jeder neue Aufrufer müsste die instanceof-Abfragen des Erinnerungsdienstes kopieren, und jeder neue Kanal mit Einschränkung verlangt neue Abfragen bei allen Aufrufern (1 Punkt).
c) Baue den Code so um, dass das LSP eingehalten wird und der Erinnerungsdienst keine Unterklassen mehr abfragt. Der Flurmonitor ist fachlich kein Kanal für einzelne Personen. Die drei ersten Tests müssen grün bleiben; der vierte Test darf entfallen, wenn sich ein Flurmonitor gar nicht mehr als Benachrichtigung übergeben lässt. (6 Punkte)
Zwei Schritte: Die Einschränkung von Push wird Teil des Vertrags der Oberklasse (neue Methode erreicht, Standard true), und FlurmonitorAnzeige erbt nicht mehr von Benachrichtigung. Person und MailBenachrichtigung bleiben unverändert.
// geänderte Klassen nach dem Umbau
// ===== Benachrichtigung.java =====
package de.mitfahrlabor;
import java.util.ArrayList;
import java.util.List;
/** Ein Kanal, über den die App einzelne Personen benachrichtigt. */
public abstract class Benachrichtigung {
protected final List<String> ausgang = new ArrayList<>();
/** Kann dieser Kanal die Person erreichen? Standard: ja. */
public boolean erreicht(Person person) {
return true;
}
/**
* Stellt der Person den Text zu; danach steht genau ein Eintrag mehr im Ausgang.
* Vorbedingung: erreicht(person) ist true.
*/
public abstract void sende(Person person, String text);
public List<String> ausgang() {
return List.copyOf(ausgang);
}
}
// ===== PushBenachrichtigung.java =====
package de.mitfahrlabor;
public class PushBenachrichtigung extends Benachrichtigung {
@Override
public boolean erreicht(Person person) {
return person.pushAktiv();
}
@Override
public void sende(Person person, String text) {
if (!erreicht(person)) {
throw new IllegalStateException("Push ist für " + person.name() + " nicht aktiviert");
}
ausgang.add("Push an " + person.name() + ": " + text);
}
}
// ===== FlurmonitorAnzeige.java =====
package de.mitfahrlabor;
import java.util.ArrayList;
import java.util.List;
/** Zeigt offene Angebote anonym an; ist KEINE Benachrichtigung einzelner Personen. */
public class FlurmonitorAnzeige {
private final List<String> ausgang = new ArrayList<>();
public void zeige(String haltepunkt, String abfahrt, int freiePlaetze) {
ausgang.add(abfahrt + " ab " + haltepunkt + ": " + freiePlaetze + " Plätze frei");
}
public List<String> ausgang() {
return List.copyOf(ausgang);
}
}
// ===== Erinnerungsdienst.java =====
package de.mitfahrlabor;
import java.util.List;
public class Erinnerungsdienst {
private final List<Benachrichtigung> kanaele;
public Erinnerungsdienst(List<Benachrichtigung> kanaele) {
this.kanaele = List.copyOf(kanaele);
}
public void erinnere(List<Person> mitfahrende, String fahrt) {
for (Benachrichtigung kanal : kanaele) {
for (Person person : mitfahrende) {
if (kanal.erreicht(person)) {
kanal.sende(person, "Erinnerung: " + fahrt);
}
}
}
}
}Die drei verbliebenen Tests sind grün. Der Versuch, eine FlurmonitorAnzeige in die Liste der Kanäle zu legen, ist jetzt ein Übersetzungsfehler bzw. ein Fehler bei der Typprüfung; der vierte Test entfällt deshalb.
Andere Lösungen sind gleichwertig, etwa eine Liste von Kanälen je Person statt erreicht, oder ein Push-Kanal, der bei deaktiviertem Push still nichts tut, wenn der Vertrag der Oberklasse das ausdrücklich erlaubt (dann muss aber die Nachbedingung „ein Eintrag mehr“ angepasst werden).
Bewertung: Flurmonitor aus der Hierarchie gelöst (2 Punkte), Einschränkung von Push im Vertrag der Oberklasse bzw. vor dem Aufruf erfragbar (2 Punkte), Erinnerungsdienst ohne Typabfragen (1 Punkt), Tests grün (1 Punkt).
Aufgabe 3.1: Kurierzustellung in die Kata einbauen (Profilkompetenz)
(ca. 20 min Bearbeitungszeit)
Die CSB-Rad GmbH (siehe Szenario CSB-Rad) will kleine Bestellungen im Stadtgebiet per Lastenrad zustellen lassen. Die neue Versandart KURIER kostet pauschal 12,90 €, nimmt höchstens 25 kg mit und transportiert keine Räder. Die Bestätigungsmail nennt dann als letzte Zeile „Versand: Kurier (Lastenrad)“. Du startest mit Stand 06 der Code-Kata; im Begleit-Repository liegt er unter src/kata/java/stand-06 bzw. src/kata/python/stand-06. Der vollständige Code:
// Kata CSB-Rad, Stand 06, Paket de.csbrad.shop (src/main/java)
// ===== Position.java =====
package de.csbrad.shop;
/** Eine Bestellposition; Preise sind Nettopreise in Cent, Gewichte in Gramm. */
public record Position(String artikelnr, String bezeichnung, int nettopreisCent, int menge, int gewichtGramm, boolean istRad) {
}
// ===== Bestellanfrage.java =====
package de.csbrad.shop;
import java.util.List;
/** Parameterobjekt: alles, was der Bestellabschluss als Eingabe braucht. */
public record Bestellanfrage(List<Position> positionen, String versandart, String gutscheincode, String kundenEmail) {
}
// ===== Bestellung.java =====
package de.csbrad.shop;
import java.util.List;
/** Eine Bestellung kennt ihre Positionen und berechnet ihre Beträge selbst (Tell, don't ask). */
public final class Bestellung {
private static final String GUTSCHEINCODE_KURIER = "KURIER10";
private static final int GUTSCHEIN_RABATT_PROZENT = 10;
private static final int MEHRWERTSTEUER_PROZENT = 19;
private final List<Position> positionen;
private final String versandart;
private final String gutscheincode;
private final String kundenEmail;
private final Versandkostenrechner versandkostenrechner;
private String bestellnummer;
public Bestellung(Bestellanfrage anfrage, Versandkostenrechner versandkostenrechner) {
pruefePositionen(anfrage.positionen());
this.positionen = List.copyOf(anfrage.positionen());
this.versandart = anfrage.versandart();
this.gutscheincode = anfrage.gutscheincode();
this.kundenEmail = anfrage.kundenEmail();
this.versandkostenrechner = versandkostenrechner;
if (warenwert() == 0) {
throw new IllegalArgumentException("Bestellung ohne Warenwert");
}
}
private static void pruefePositionen(List<Position> positionen) {
if (positionen.isEmpty()) {
throw new IllegalArgumentException("Bestellung ohne Positionen");
}
for (Position position : positionen) {
if (position.menge() <= 0) {
throw new IllegalArgumentException("Menge muss positiv sein: " + position.artikelnr());
}
}
}
public int warenwert() {
return positionen.stream().mapToInt(p -> p.nettopreisCent() * p.menge()).sum();
}
public int rabatt() {
if (GUTSCHEINCODE_KURIER.equals(gutscheincode)) {
return prozentKaufmaennischGerundet(warenwert(), GUTSCHEIN_RABATT_PROZENT);
}
return 0;
}
public int warenwertNachRabatt() {
return warenwert() - rabatt();
}
public int versandkosten() {
return versandkostenrechner.berechne(this);
}
public int mehrwertsteuer() {
return prozentKaufmaennischGerundet(warenwertNachRabatt() + versandkosten(), MEHRWERTSTEUER_PROZENT);
}
public int gesamtbetrag() {
return warenwertNachRabatt() + versandkosten() + mehrwertsteuer();
}
public int gesamtgewichtGramm() {
return positionen.stream().mapToInt(p -> p.gewichtGramm() * p.menge()).sum();
}
public int anzahlRaeder() {
return positionen.stream().filter(Position::istRad).mapToInt(Position::menge).sum();
}
public boolean enthaeltRad() {
return positionen.stream().anyMatch(Position::istRad);
}
public String versandart() {
return versandart;
}
public String kundenEmail() {
return kundenEmail;
}
public String bestellnummer() {
return bestellnummer;
}
void vergibNummer(String nummer) {
if (bestellnummer != null) {
throw new IllegalStateException("Bestellung hat schon eine Nummer: " + bestellnummer);
}
bestellnummer = nummer;
}
private static int prozentKaufmaennischGerundet(int betragCent, int prozent) {
return (betragCent * prozent + 50) / 100;
}
}
// ===== Versandkostenrechner.java =====
package de.csbrad.shop;
import java.util.Set;
/** Kennt die Versandregeln der CSB-Rad GmbH. */
public class Versandkostenrechner {
private static final Set<String> VERSANDARTEN = Set.of("PAKET", "SPEDITION", "ABHOLUNG");
private static final int PAKETPAUSCHALE_CENT = 690;
private static final int HOECHSTGEWICHT_PAKET_GRAMM = 31500;
private static final int VERSANDKOSTENFREI_AB_CENT = 10000;
private static final int SPEDITION_JE_RAD_CENT = 4900;
public void pruefeVersandmoeglich(Bestellung bestellung) {
if (!VERSANDARTEN.contains(bestellung.versandart())) {
throw new IllegalArgumentException("Unbekannte Versandart: " + bestellung.versandart());
}
if (bestellung.versandart().equals("PAKET") && bestellung.enthaeltRad()) {
throw new IllegalArgumentException("Räder können nicht als Paket versandt werden");
}
}
public int berechne(Bestellung bestellung) {
return switch (bestellung.versandart()) {
case "PAKET" -> paketkosten(bestellung);
// ohne Rad wird eine Speditionspauschale berechnet
case "SPEDITION" -> Math.max(1, bestellung.anzahlRaeder()) * SPEDITION_JE_RAD_CENT;
default -> 0;
};
}
private int paketkosten(Bestellung bestellung) {
if (bestellung.warenwertNachRabatt() >= VERSANDKOSTENFREI_AB_CENT) {
return 0;
}
int anzahlPakete = Math.max(1, (bestellung.gesamtgewichtGramm() + HOECHSTGEWICHT_PAKET_GRAMM - 1) / HOECHSTGEWICHT_PAKET_GRAMM);
return anzahlPakete * PAKETPAUSCHALE_CENT;
}
}
// ===== Mailversand.java =====
package de.csbrad.shop;
import java.util.ArrayList;
import java.util.List;
/** Verschickt Bestätigungsmails; hier nur in einen Postausgang (Liste). */
public class Mailversand {
private final List<String> postausgang = new ArrayList<>();
public void sendeBestaetigung(Bestellung bestellung) {
postausgang.add("An: " + bestellung.kundenEmail() + "\nBestellung " + bestellung.bestellnummer()
+ "\nGesamtbetrag: " + alsEuroText(bestellung.gesamtbetrag()) + "\n" + versandzeile(bestellung.versandart()));
}
public List<String> postausgang() {
return List.copyOf(postausgang);
}
private static String versandzeile(String versandart) {
return switch (versandart) {
case "PAKET" -> "Versand: Paket";
case "SPEDITION" -> "Versand: Spedition";
default -> "Abholung in der Werkstatt Bielefeld-Sennestadt";
};
}
private static String alsEuroText(int betragCent) {
return (betragCent / 100) + "," + String.format("%02d", betragCent % 100) + " EUR";
}
}
// ===== BestellRepository.java =====
package de.csbrad.shop;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/** Speichert Bestellungen (hier im Arbeitsspeicher) und vergibt fortlaufende Bestellnummern. */
public class BestellRepository {
private static final int ERSTE_BESTELLNUMMER = 1001;
private final List<Bestellung> bestellungen = new ArrayList<>();
private int naechsteNummer = ERSTE_BESTELLNUMMER;
public void speichere(Bestellung bestellung) {
bestellung.vergibNummer("CSB-" + naechsteNummer);
naechsteNummer = naechsteNummer + 1;
bestellungen.add(bestellung);
}
public Optional<Bestellung> findeNachNummer(String bestellnummer) {
return bestellungen.stream().filter(b -> b.bestellnummer().equals(bestellnummer)).findFirst();
}
public List<Bestellung> alle() {
return List.copyOf(bestellungen);
}
}
// ===== Bestellabschluss.java =====
package de.csbrad.shop;
/** Ablauf des Bestellabschlusses: prüfen, speichern, bestätigen. */
public class Bestellabschluss {
private final Versandkostenrechner versandkostenrechner = new Versandkostenrechner();
private final Mailversand mailversand = new Mailversand();
private final BestellRepository repository = new BestellRepository();
public Bestellung abschliessen(Bestellanfrage anfrage) {
Bestellung bestellung = new Bestellung(anfrage, versandkostenrechner);
versandkostenrechner.pruefeVersandmoeglich(bestellung);
repository.speichere(bestellung);
mailversand.sendeBestaetigung(bestellung);
return bestellung;
}
public Mailversand mailversand() {
return mailversand;
}
public BestellRepository repository() {
return repository;
}
}Die Tests von Stand 06:
package de.csbrad.shop;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Charakterisierungstests (seit Stand 04), angepasst an die Klassen aus Stand 06.
* betraege(): {Warenwert, Rabatt, Versandkosten, Mehrwertsteuer, Gesamtbetrag} in Cent.
*/
class BestellabschlussTest {
static final Position LICHTSET = new Position("LIC-SET-01", "Lichtset Nabendynamo StVZO", 7563, 1, 350, false);
static final Position BREMSE = new Position("BRE-HYD-01", "Hydraulische Scheibenbremse (Paar)", 13361, 1, 750, false);
static final Position BUEGELSCHLOSS = new Position("SCH-BUE-01", "Bügelschloss, gleichschließend", 6218, 1, 1300, false);
static final Position RAHMENSCHLOSS = new Position("SCH-RAH-01", "Rahmenschloss, gleichschließend", 4118, 1, 450, false);
static final Position STADTRAD = new Position("RAD-CITY-01", "Stadtrad „Promenade“ 7-Gang", 67143, 1, 16800, true);
static final Position EBIKE = new Position("RAD-EB-02", "E-Bike „Kanal“ Mittelmotor 500 Wh", 252017, 1, 24500, true);
static final String MAIL = "mira.engel@example.org";
private Bestellabschluss abschluss;
@BeforeEach
void neuerAbschluss() {
abschluss = new Bestellabschluss();
}
static int[] betraege(Bestellung b) {
return new int[] {b.warenwert(), b.rabatt(), b.versandkosten(), b.mehrwertsteuer(), b.gesamtbetrag()};
}
static Position menge(Position p, int m) {
return new Position(p.artikelnr(), p.bezeichnung(), p.nettopreisCent(), m, p.gewichtGramm(), p.istRad());
}
@Test
void paketUnterFreigrenze() {
assertArrayEquals(new int[] {7563, 0, 690, 1568, 9821}, betraege(abschluss.abschliessen(new Bestellanfrage(List.of(LICHTSET), "PAKET", null, MAIL))));
}
@Test
void paketAbFreigrenzeIstVersandfrei() {
assertArrayEquals(new int[] {13361, 0, 0, 2539, 15900}, betraege(abschluss.abschliessen(new Bestellanfrage(List.of(BREMSE), "PAKET", null, MAIL))));
}
@Test
void freigrenzeGiltNachRabatt() {
assertArrayEquals(new int[] {10336, 1034, 690, 1898, 11890}, betraege(abschluss.abschliessen(new Bestellanfrage(List.of(BUEGELSCHLOSS, RAHMENSCHLOSS), "PAKET", "KURIER10", MAIL))));
}
@Test
void ohneRabattUeberFreigrenze() {
assertArrayEquals(new int[] {10336, 0, 0, 1964, 12300}, betraege(abschluss.abschliessen(new Bestellanfrage(List.of(BUEGELSCHLOSS, RAHMENSCHLOSS), "PAKET", null, MAIL))));
}
@Test
void schwerePaketbestellungMitRabatt() {
assertArrayEquals(new int[] {155450, 15545, 0, 26582, 166487}, betraege(abschluss.abschliessen(new Bestellanfrage(List.of(menge(BUEGELSCHLOSS, 25)), "PAKET", "KURIER10", MAIL))));
}
@Test
void speditionJeRad() {
assertArrayEquals(new int[] {149412, 0, 9800, 30250, 189462}, betraege(abschluss.abschliessen(new Bestellanfrage(List.of(menge(STADTRAD, 2), menge(LICHTSET, 2)), "SPEDITION", null, MAIL))));
}
@Test
void speditionOhneRadKostetEinePauschale() {
assertArrayEquals(new int[] {7563, 0, 4900, 2368, 14831}, betraege(abschluss.abschliessen(new Bestellanfrage(List.of(LICHTSET), "SPEDITION", null, MAIL))));
}
@Test
void abholungMitRabattcode() {
assertArrayEquals(new int[] {252017, 25202, 0, 43095, 269910}, betraege(abschluss.abschliessen(new Bestellanfrage(List.of(EBIKE), "ABHOLUNG", "KURIER10", MAIL))));
}
@Test
void unbekannterCodeGibtKeinenRabatt() {
assertArrayEquals(new int[] {7563, 0, 690, 1568, 9821}, betraege(abschluss.abschliessen(new Bestellanfrage(List.of(LICHTSET), "PAKET", "SOMMER20", MAIL))));
}
@Test
void radAlsPaketWirdAbgelehnt() {
assertThrows(IllegalArgumentException.class, () -> abschluss.abschliessen(new Bestellanfrage(List.of(STADTRAD), "PAKET", null, MAIL)));
assertEquals(0, abschluss.mailversand().postausgang().size());
assertEquals(0, abschluss.repository().alle().size());
}
@Test
void leereBestellungWirdAbgelehnt() {
assertThrows(IllegalArgumentException.class, () -> abschluss.abschliessen(new Bestellanfrage(List.of(), "PAKET", null, MAIL)));
assertEquals(0, abschluss.mailversand().postausgang().size());
}
@Test
void mengeNullWirdAbgelehnt() {
assertThrows(IllegalArgumentException.class, () -> abschluss.abschliessen(new Bestellanfrage(List.of(menge(LICHTSET, 0)), "PAKET", null, MAIL)));
}
@Test
void unbekannteVersandartWirdAbgelehnt() {
assertThrows(IllegalArgumentException.class, () -> abschluss.abschliessen(new Bestellanfrage(List.of(LICHTSET), "BRIEF", null, MAIL)));
}
@Test
void mailFuerPaketversand() {
abschluss.abschliessen(new Bestellanfrage(List.of(LICHTSET), "PAKET", null, MAIL));
assertEquals("An: mira.engel@example.org\nBestellung CSB-1001\nGesamtbetrag: 98,21 EUR\nVersand: Paket", abschluss.mailversand().postausgang().get(0));
}
@Test
void mailFuerAbholung() {
abschluss.abschliessen(new Bestellanfrage(List.of(EBIKE), "ABHOLUNG", "KURIER10", MAIL));
assertEquals("An: mira.engel@example.org\nBestellung CSB-1001\nGesamtbetrag: 2699,10 EUR\nAbholung in der Werkstatt Bielefeld-Sennestadt",
abschluss.mailversand().postausgang().get(0));
}
@Test
void bestellungenWerdenFortlaufendNummeriertUndGespeichert() {
abschluss.abschliessen(new Bestellanfrage(List.of(LICHTSET), "PAKET", null, MAIL));
abschluss.abschliessen(new Bestellanfrage(List.of(BREMSE), "PAKET", null, MAIL));
assertEquals(2, abschluss.repository().alle().size());
assertEquals("CSB-1001", abschluss.repository().alle().get(0).bestellnummer());
assertEquals("CSB-1002", abschluss.repository().alle().get(1).bestellnummer());
assertEquals(15900, abschluss.repository().alle().get(1).gesamtbetrag());
}
}a) Ein Teammitglied schlägt vor, die Paketregeln in eine Klasse Paketversand zu ziehen und Kurierzustellung davon erben zu lassen, weil die Prüfung auf Räder gleich ist. Beurteile den Vorschlag mit Hilfe des Liskov Substitution Principle. Gehe auf Vorbedingung und Nachbedingung von pruefe und der Kostenberechnung ein. (4 Punkte)
Die Vererbung verletzt das LSP (1 Punkt). Vorbedingung: Für Paketversand.pruefe ist jedes Gewicht zulässig, schwere Bestellungen gehen in mehreren Paketen raus. Ein überschriebenes pruefe, das ab 25 kg ablehnt, verschärft die Vorbedingung (1 Punkt). Nachbedingung: Die Paketkosten sind ab 100 € Warenwert nach Rabatt 0 und wachsen sonst mit der Paketzahl; der Kurier verlangt immer 12,90 €, die Nachbedingung wird also geändert. Auch eine Paketzahl hat der Kurier nicht (1 Punkt). Jeder Programmteil, der mit Paketversand rechnet (Packplanung, Auswertungen), würde bei einer Kurierzustellung falsche Werte oder Ausnahmen bekommen; die Tests der Kata würden das nicht zeigen, weil sie nur über den Versandkostenrechner gehen. Besser: Komposition, die Kurierzustellung benutzt die Rad-Prüfung eines Paketversand-Objekts (1 Punkt).
b) Schreibe Tests für die neue Versandart: Pauschale samt Mailzeile, Ablehnung über 25 kg (ohne dass etwas gespeichert wird) und Ablehnung von Rädern. Nutze die Testdaten aus den vorhandenen Tests. (4 Punkte)
Bremse (133,61 €, 750 g) und Bügelschloss (62,18 €, 1300 g) kosten zusammen 195,79 €; dazu 12,90 € Kurier, 19 % Mehrwertsteuer auf 208,69 € sind 39,65 €, Gesamtbetrag 248,34 €. 20 Bügelschlösser wiegen 26 kg.
// Ergänzung in BestellabschlussTest
// ---- neue Anforderung in Stand 07: Kurierzustellung ----
@Test
void kurierKostetPauschale() {
assertArrayEquals(new int[] {19579, 0, 1290, 3965, 24834},
betraege(abschluss.abschliessen(new Bestellanfrage(List.of(BREMSE, BUEGELSCHLOSS), "KURIER", null, MAIL))));
assertEquals("An: mira.engel@example.org\nBestellung CSB-1001\nGesamtbetrag: 248,34 EUR\nVersand: Kurier (Lastenrad)",
abschluss.mailversand().postausgang().get(0));
}
@Test
void kurierUeber25KiloWirdAbgelehnt() {
assertThrows(IllegalArgumentException.class,
() -> abschluss.abschliessen(new Bestellanfrage(List.of(menge(BUEGELSCHLOSS, 20)), "KURIER", null, MAIL)));
assertEquals(0, abschluss.repository().alle().size());
}
@Test
void kurierNimmtKeineRaeder() {
assertThrows(IllegalArgumentException.class,
() -> abschluss.abschliessen(new Bestellanfrage(List.of(STADTRAD), "KURIER", null, MAIL)));Bewertung: Pauschale mit allen Beträgen (1 Punkt), Mailzeile (1 Punkt), Ablehnung über 25 kg mit Prüfung, dass nichts gespeichert ist (1 Punkt), Ablehnung von Rädern (1 Punkt).
c) Implementiere die Versandart KURIER ohne LSP-Verstoß, sodass alle alten und neuen Tests grün sind. Ziehe dazu zuerst die Paketregeln in eine eigene Klasse Paketversand und nutze sie in der neuen Klasse per Komposition. (10 Punkte)
Neue Klassen Paketversand und Kurierzustellung, geänderter Versandkostenrechner (Stand 07 der Kata):
// Kata CSB-Rad, Stand 07: neue und geänderte Klassen
// ===== Paketversand.java =====
package de.csbrad.shop;
/** Versandregeln für Pakete: Pauschale je angefangene 31,5 kg, frei ab 100 Euro Warenwert nach Rabatt. */
public class Paketversand {
static final int PAKETPAUSCHALE_CENT = 690;
static final int HOECHSTGEWICHT_PAKET_GRAMM = 31500;
static final int VERSANDKOSTENFREI_AB_CENT = 10000;
public void pruefe(Bestellung bestellung) {
if (bestellung.enthaeltRad()) {
throw new IllegalArgumentException("Räder können nicht als Paket versandt werden");
}
}
public int anzahlPakete(Bestellung bestellung) {
return Math.max(1, (bestellung.gesamtgewichtGramm() + HOECHSTGEWICHT_PAKET_GRAMM - 1) / HOECHSTGEWICHT_PAKET_GRAMM);
}
public int kosten(Bestellung bestellung) {
if (bestellung.warenwertNachRabatt() >= VERSANDKOSTENFREI_AB_CENT) {
return 0;
}
return anzahlPakete(bestellung) * PAKETPAUSCHALE_CENT;
}
}
// ===== Kurierzustellung.java =====
package de.csbrad.shop;
/**
* Neu in Stand 07: Zustellung per Lastenradkurier im Stadtgebiet, Pauschale, höchstens 25 kg, keine Räder.
* Bewusst KEINE Unterklasse von Paketversand: Sie würde die Vorbedingung verschärfen (25 kg statt beliebig vieler Pakete)
* und damit das Liskovsche Substitutionsprinzip verletzen. Gemeinsames (Rad-Prüfung) wird per Komposition genutzt.
*/
public class Kurierzustellung {
static final int KURIERPAUSCHALE_CENT = 1290;
static final int HOECHSTGEWICHT_KURIER_GRAMM = 25000;
private final Paketversand paketregeln = new Paketversand();
public void pruefe(Bestellung bestellung) {
paketregeln.pruefe(bestellung);
if (bestellung.gesamtgewichtGramm() > HOECHSTGEWICHT_KURIER_GRAMM) {
throw new IllegalArgumentException("Kurierzustellung nur bis 25 kg");
}
}
public int kosten(Bestellung bestellung) {
return KURIERPAUSCHALE_CENT;
}
}
// ===== Versandkostenrechner.java =====
package de.csbrad.shop;
import java.util.Set;
/** Kennt die Versandarten der CSB-Rad GmbH und verteilt auf die jeweiligen Regeln. */
public class Versandkostenrechner {
private static final Set<String> VERSANDARTEN = Set.of("PAKET", "SPEDITION", "ABHOLUNG", "KURIER");
private static final int SPEDITION_JE_RAD_CENT = 4900;
private final Paketversand paketversand = new Paketversand();
private final Kurierzustellung kurierzustellung = new Kurierzustellung();
public void pruefeVersandmoeglich(Bestellung bestellung) {
if (!VERSANDARTEN.contains(bestellung.versandart())) {
throw new IllegalArgumentException("Unbekannte Versandart: " + bestellung.versandart());
}
switch (bestellung.versandart()) {
case "PAKET" -> paketversand.pruefe(bestellung);
case "KURIER" -> kurierzustellung.pruefe(bestellung);
default -> { }
}
}
public int berechne(Bestellung bestellung) {
return switch (bestellung.versandart()) {
case "PAKET" -> paketversand.kosten(bestellung);
// ohne Rad wird eine Speditionspauschale berechnet
case "SPEDITION" -> Math.max(1, bestellung.anzahlRaeder()) * SPEDITION_JE_RAD_CENT;
case "KURIER" -> kurierzustellung.kosten(bestellung);
default -> 0;
};
}
}Im Mailversand kommt eine Zeile für die Versandzeile hinzu:
// Mailversand, Stand 07
private static String versandzeile(String versandart) {
return switch (versandart) {
case "PAKET" -> "Versand: Paket";
case "SPEDITION" -> "Versand: Spedition";
case "KURIER" -> "Versand: Kurier (Lastenrad)";
default -> "Abholung in der Werkstatt Bielefeld-Sennestadt";
};
}Alle 19 Tests sind grün (mvn test bzw. python -m pytest).
Bewertung: Paketversand als eigene Klasse mit unverändertem Verhalten (3 Punkte), Kurierzustellung ohne Vererbung mit Rad-Prüfung über ein Paketversand-Objekt (3 Punkte), Gewichtsgrenze und Pauschale (2 Punkte), Anschluss im Versandkostenrechner und im Mailversand (1 Punkt), Tests grün (1 Punkt).
d) Im Stand 07 unterscheidet der Versandkostenrechner die Versandarten weiterhin mit switch. Ein Teammitglied möchte eine gemeinsame abstrakte Oberklasse Versandart mit pruefe, anzahlPakete und kosten einführen, von der alle Versandarten erben. Beurteile den Vorschlag und mache einen Gegenvorschlag. (4 Punkte)
Eine gemeinsame Abstraktion ist sinnvoll, weil der switch mit jeder Versandart wächst und an mehreren Stellen steht (1 Punkt). anzahlPakete gehört aber nicht hinein: Abholung, Spedition und Kurier haben keine Pakete und müssten eine Ausnahme werfen oder einen erfundenen Wert liefern, also das LSP verletzen (1 Punkt). Gegenvorschlag: eine schlanke Schnittstelle Versandart nur mit pruefe und kosten (und ggf. der Mailzeile), die alle Versandarten vollständig erfüllen können (1 Punkt). anzahlPakete bleibt eine Methode von Paketversand. Eine Schnittstelle statt einer abstrakten Oberklasse vermeidet zusätzlich die Kopplung an geerbte Implementierung (1 Punkt). Dieser Umbau ist Thema des Artikels Erweitern statt ändern.
Aufgabe 3.2: Fahrtangebot und Mitfahrerliste durch Komposition entflechten (Profilkompetenz)
(ca. 20 min Bearbeitungszeit)
Im Mitfahrlabor darf eine Fahrt nur so viele Mitfahrende haben, wie Plätze angeboten sind, und Minderjährige dürfen nur mit Einwilligung der Erziehungsberechtigten mitfahren. Um sich Arbeit zu sparen, hat das Team die Mitfahrerliste von der Listenklasse der Standardbibliothek erben lassen und das Fahrtangebot wiederum von der Mitfahrerliste:
// Paket de.mitfahrlabor, Stand vor dem Umbau
// ===== Mitfahrer.java =====
package de.mitfahrlabor;
public record Mitfahrer(String name, boolean minderjaehrig, boolean einwilligungLiegtVor) {
}
// ===== Mitfahrerliste.java =====
package de.mitfahrlabor;
import java.util.ArrayList;
/** Bestätigte Mitfahrende einer Fahrt; erbt von ArrayList, „damit man nicht alles neu schreiben muss“. */
public class Mitfahrerliste extends ArrayList<Mitfahrer> {
private final int plaetze;
public Mitfahrerliste(int plaetze) {
this.plaetze = plaetze;
}
public void bestaetige(Mitfahrer mitfahrer) {
if (freiePlaetze() == 0) {
throw new IllegalStateException("Die Fahrt ist voll");
}
if (mitfahrer.minderjaehrig() && !mitfahrer.einwilligungLiegtVor()) {
throw new IllegalArgumentException("Einwilligung der Erziehungsberechtigten fehlt");
}
add(mitfahrer);
}
public int freiePlaetze() {
return plaetze - size();
}
}
// ===== Fahrtangebot.java =====
package de.mitfahrlabor;
/** Ein Fahrtangebot ist eine Mitfahrerliste mit Haltepunkt, Abfahrt und Kostenbeitrag. */
public class Fahrtangebot extends Mitfahrerliste {
private final String haltepunkt;
private final String abfahrt;
private final int entfernungKm;
private final int centJeKm;
public Fahrtangebot(String haltepunkt, String abfahrt, int plaetze, int entfernungKm, int centJeKm) {
super(plaetze);
this.haltepunkt = haltepunkt;
this.abfahrt = abfahrt;
this.entfernungKm = entfernungKm;
this.centJeKm = centJeKm;
}
public int kostenbeitragCent() {
return entfernungKm * centJeKm;
}
public String monitorzeile() {
return abfahrt + " ab " + haltepunkt + ": " + freiePlaetze() + " Plätze frei";
}
}Diese Tests sind grün:
package de.mitfahrlabor;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
class FahrtangebotTest {
private static final Mitfahrer AYLIN = new Mitfahrer("Aylin Kaya", false, false);
private static final Mitfahrer OKSANA = new Mitfahrer("Oksana Melnyk", true, true);
private static final Mitfahrer LINA = new Mitfahrer("Lina Hartwig", true, false);
private final Fahrtangebot fahrt = new Fahrtangebot("Gütersloh Hbf", "07:15", 2, 17, 8);
@Test
void bestaetigenBelegtPlaetze() {
fahrt.bestaetige(AYLIN);
assertEquals(1, fahrt.freiePlaetze());
assertEquals("07:15 ab Gütersloh Hbf: 1 Plätze frei", fahrt.monitorzeile());
}
@Test
void volleFahrtWirdAbgelehnt() {
fahrt.bestaetige(AYLIN);
fahrt.bestaetige(OKSANA);
assertThrows(IllegalStateException.class, () -> fahrt.bestaetige(new Mitfahrer("Ben Richter", false, false)));
}
@Test
void minderjaehrigOhneEinwilligungWirdAbgelehnt() {
assertThrows(IllegalArgumentException.class, () -> fahrt.bestaetige(LINA));
assertEquals(2, fahrt.freiePlaetze());
}
@Test
void kostenbeitragJePerson() {
assertEquals(136, fahrt.kostenbeitragCent());
}
}a) Prüfe beide Vererbungsbeziehungen mit der ist-ein-Probe. Nenne außerdem zwei Wege, auf denen Code die Platzregel oder die Einwilligungsregel umgehen kann, und beschreibe die Folge für freiePlaetze(). (5 Punkte)
„Eine Mitfahrerliste ist eine
ArrayList“ stimmt höchstens technisch; fachlich hat sie eine Liste von Personen. Die Vererbung dient nur der Wiederverwendung (1 Punkt).„Ein Fahrtangebot ist eine Mitfahrerliste“ stimmt nicht: Ein Fahrtangebot hat Mitfahrende (1 Punkt).
Umgehen: Weil alle öffentlichen Methoden der Liste geerbt sind, kann jeder Aufrufer
add/append,addAll/extend,set,removeodercleardirekt aufrufen, anbestaetigevorbei (2 Punkte für zwei Wege). Folge: Mit drei Personen auf zwei Plätzen liefertfreiePlaetze()den Wert -1, und eine minderjährige Person ohne Einwilligung steht in der Liste; die Invariante 0 ≤ freie Plätze ≤ Plätze gilt nicht mehr (1 Punkt).
b) Ein Teammitglied schlägt vor, einfach add (Python: append) zu überschreiben und dort die Prüfungen aus bestaetige aufzurufen. Beurteile den Vorschlag mit Blick auf die fragile Basisklasse und das Liskov Substitution Principle. (5 Punkte)
Fragile Basisklasse: Ob
addAll(Python:extend,insert,+=, Slice-Zuweisung) internaddbzw.appendaufruft, ist ein Implementierungsdetail der Standardbibliothek. In Java ruftArrayList.addAlldasaddnicht auf, in CPython ruftlist.extenddasappendnicht auf. Die Prüfung würde also über diese Wege weiter umgangen, und man müsste jede einzelne ändernde Methode kennen und überschreiben, auch solche, die in künftigen Versionen dazukommen (2 Punkte).LSP: Wer eine
Listbzw.listbekommt, darf erwarten, dassadddas Element anhängt (in Java liefertaddlaut Vertragtrue). Einadd, das bei voller Fahrt oder fehlender Einwilligung eine Ausnahme wirft, verschärft die Vorbedingung. Programmteile, die eineMitfahrerlisteals gewöhnliche Liste weiterreichen (Sortieren, Kopieren, Zusammenführen), würden überraschend scheitern (2 Punkte).Fazit: Der Vorschlag repariert die Symptome. Die Ursache ist, dass eine Klasse mit eigenen Regeln von einer allgemeinen Sammlung erbt; besser ist Komposition (1 Punkt).
c) Baue Mitfahrerliste und Fahrtangebot so um, dass sie nicht mehr erben, sondern Komposition und Delegation nutzen. Nach außen sollen nur fachliche Operationen sichtbar sein: bestätigen, stornieren, freie Plätze abfragen und die Bestätigten lesen, ohne die interne Liste ändern zu können. Die vorhandenen Tests müssen grün bleiben; ergänze einen Test für das Stornieren. (8 Punkte)
// Paket de.mitfahrlabor nach dem Umbau (Mitfahrer unverändert)
// ===== Mitfahrerliste.java =====
package de.mitfahrlabor;
import java.util.ArrayList;
import java.util.List;
/** Bestätigte Mitfahrende einer Fahrt; nutzt eine Liste per Komposition und schützt die Platzregel. */
public class Mitfahrerliste {
private final List<Mitfahrer> bestaetigte = new ArrayList<>();
private final int plaetze;
public Mitfahrerliste(int plaetze) {
this.plaetze = plaetze;
}
public void bestaetige(Mitfahrer mitfahrer) {
if (freiePlaetze() == 0) {
throw new IllegalStateException("Die Fahrt ist voll");
}
if (mitfahrer.minderjaehrig() && !mitfahrer.einwilligungLiegtVor()) {
throw new IllegalArgumentException("Einwilligung der Erziehungsberechtigten fehlt");
}
bestaetigte.add(mitfahrer);
}
public void storniere(Mitfahrer mitfahrer) {
bestaetigte.remove(mitfahrer);
}
public int freiePlaetze() {
return plaetze - bestaetigte.size();
}
public List<Mitfahrer> bestaetigte() {
return List.copyOf(bestaetigte);
}
}
// ===== Fahrtangebot.java =====
package de.mitfahrlabor;
import java.util.List;
/** Ein Fahrtangebot HAT eine Mitfahrerliste und reicht nur die fachlichen Operationen weiter. */
public class Fahrtangebot {
private final String haltepunkt;
private final String abfahrt;
private final int entfernungKm;
private final int centJeKm;
private final Mitfahrerliste mitfahrende;
public Fahrtangebot(String haltepunkt, String abfahrt, int plaetze, int entfernungKm, int centJeKm) {
this.haltepunkt = haltepunkt;
this.abfahrt = abfahrt;
this.entfernungKm = entfernungKm;
this.centJeKm = centJeKm;
this.mitfahrende = new Mitfahrerliste(plaetze);
}
public void bestaetige(Mitfahrer mitfahrer) {
mitfahrende.bestaetige(mitfahrer);
}
public void storniere(Mitfahrer mitfahrer) {
mitfahrende.storniere(mitfahrer);
}
public int freiePlaetze() {
return mitfahrende.freiePlaetze();
}
public List<Mitfahrer> bestaetigte() {
return mitfahrende.bestaetigte();
}
public int kostenbeitragCent() {
return entfernungKm * centJeKm;
}
public String monitorzeile() {
return abfahrt + " ab " + haltepunkt + ": " + freiePlaetze() + " Plätze frei";
}
}Ergänzter Test:
@Test
void stornierenGibtPlatzFrei() {
fahrt.bestaetige(AYLIN);
fahrt.storniere(AYLIN);
assertEquals(2, fahrt.freiePlaetze());
assertEquals(0, fahrt.bestaetigte().size());
}Alle Tests sind grün. Code, der wie vorher addAll bzw. extend am Fahrtangebot aufruft, lässt sich nicht mehr übersetzen bzw. fällt bei der Typprüfung durch.
Bewertung: interne Liste privat in Mitfahrerliste (2 Punkte), Fahrtangebot hält eine Mitfahrerliste und delegiert (2 Punkte), Stornieren (1 Punkt), Bestätigte nur als unveränderliche Kopie (1 Punkt), Tests grün und Test für Stornieren (2 Punkte).
d) Zeichne das UML-Klassendiagramm nach dem Umbau mit Attributen, Methoden und Beziehungen samt Multiplizitäten. Vermerke die Invariante der Mitfahrerliste als Notiz. (4 Punkte)
Die Beziehung zwischen Fahrtangebot und Mitfahrerliste ist eine Komposition, weil die Liste mit dem Angebot entsteht und vergeht. Die Mitfahrenden existieren unabhängig vom Angebot, deshalb eine Aggregation oder einfache Assoziation; beides ist richtig.
Bewertung: keine Generalisierung mehr, Komposition mit Multiplizitäten (1 Punkt), fachliche Methoden in beiden Klassen (1 Punkt), private Attribute (1 Punkt), Notiz mit Invariante (1 Punkt).
Links und weitere Informationen
Vererbung richtig einsetzen: Liskov Substitution Principle und Favour Composition over Inheritance
Klassen schneiden und Übungsaufgaben zum Schneiden von Klassen
Erweitern statt ändern (nächster Artikel der Reihe)
Hinweis zur Nachnutzung als Open Educational Resource (OER)
Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Übungsaufgaben zum Einsatz von Vererbung: Liskov Substitution Principle und Favour Composition over Inheritance” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/vererbung-ersetzbarkeit-uebungsaufgaben veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/design-prinzipien/clean-code. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 10.11.2026.
[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]
