Übungsaufgaben zu SOLID und Dependency Inversion
https://bildung.social/@oerinformatik/
https://oer-informatik.de/solid-uebungsaufgaben
Clean Code, SOLID, Dependency Inversion Principle, Dependency Injection, Konstruktor-Injektion, Test-Double, Mock, Stub, UML-Klassendiagramm, Übungsaufgaben, Prüfungsvorbereitung
tl/dr; (Bearbeitungszeit insgesamt 53 min, mehrere Aufgaben): Drei Übungsaufgaben zu SOLID: Du entkoppelst in der CSB-Rad-Kata den Bestellabschluss nach dem Dependency Inversion Principle und testest ihn mit Stub und Mock, findest im Erinnerungsdienst des Mitfahrlabors Verstöße gegen mehrere SOLID-Prinzipien, legst eine Reihenfolge für den Umbau fest und baust ihn mit fester Uhr im Test um, und nimmst Stellung zu der Frage, wann Dependency Injection zu viel ist [AI-Assisted]. (Zuletzt geändert am 09.11.2026)
Disclaimer: Teile des Textes stammen von einer KI. Inhalte sind nicht automatisch zuverlässig; überprüfe Fakten und zitiere vertrauenswürdige Quellen.
Diese Übungsaufgaben gehören zum Artikel SOLID im Überblick: fünf Prinzipien und die Dependency Inversion.
Hinweise zur Bearbeitung: Von FIA wird erwartet, dass ihnen Aufgaben auf Profilkompetenz-Ebene (Aufgabe 3.1 und Aufgabe 3.2) keine Probleme bereiten. Aufgaben auf Expertise-Ebene (Aufgabe 4.1) gehen darüber hinaus. 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 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: „Erläutere“ verlangt eine Erklärung mit Beispiel aus dem Code, „Ermittle“ verlangt ein vollständiges Ergebnis (hier eine Tabelle), „Lege fest und begründe“ verlangt eine Entscheidung mit Gründen, „Baue um“ und „Schreibe“ verlangen lauffähigen Code, „Zeichne“ verlangt ein UML-Klassendiagramm, „Nimm Stellung“ verlangt eine begründete eigene Position, die Gegenargumente abwägt. 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 „SOLID und Dependency Inversion“.
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/solid
- https://oer-informatik.de/dependency-injection
- https://oer-informatik.de/erweitern-statt-aendern
- https://oer-informatik.de/klassen-schneiden
- https://oer-informatik.de/vererbung-ersetzbarkeit
- https://oer-informatik.de/unit-tests
- https://oer-informatik.de/java-junit-mockito
- https://oer-informatik.de/refactoring
- https://oer-informatik.de/entwurf-und-code
- Stilbeispiel: https://oer-informatik.de/solid-uebungsaufgaben
Die Aufgaben sollen Inhalte zu diesen Schlagwörtern abdecken:
- Clean Code
- SOLID
- Dependency Inversion Principle
- Dependency Injection
- Konstruktor-Injektion
- Test-Double
- Mock
- Stub
- 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 3.1: Den Bestellabschluss der Kata entkoppeln (Profilkompetenz)
(ca. 20 min Bearbeitungszeit)
Im Onlineshop der CSB-Rad GmbH (siehe Szenario CSB-Rad) soll die Bestätigungsmail künftig über einen externen Maildienst verschickt werden. Bevor das passiert, möchte das Team den Bestellabschluss mit Unit-Tests absichern, die keine echten Mails verschicken und nichts speichern. Du startest mit Stand 08 der Code-Kata; im Begleit-Repository liegt er unter src/kata/java/stand-08 bzw. src/kata/python/stand-08. Der vollständige Code mit den Tests:
// ===== Abholung.java =====
package de.csbrad.shop;
/** Abholung in der Werkstatt: kostenlos. */
public class Abholung implements Versandart {
@Override
public void pruefe(Bestellung bestellung) {
// abholen lässt sich alles
}
@Override
public int kosten(Bestellung bestellung) {
return 0;
}
@Override
public String mailzeile() {
return "Abholung in der Werkstatt Bielefeld-Sennestadt";
}
}
// ===== 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 implements BestellungSpeichern, BestellungLesen {
private static final int ERSTE_BESTELLNUMMER = 1001;
private final List<Bestellung> bestellungen = new ArrayList<>();
private int naechsteNummer = ERSTE_BESTELLNUMMER;
@Override
public void speichere(Bestellung bestellung) {
bestellung.vergibNummer("CSB-" + naechsteNummer);
naechsteNummer = naechsteNummer + 1;
bestellungen.add(bestellung);
}
@Override
public Optional<Bestellung> findeNachNummer(String bestellnummer) {
return bestellungen.stream().filter(b -> b.bestellnummer().equals(bestellnummer)).findFirst();
}
@Override
public List<Bestellung> alle() {
return List.copyOf(bestellungen);
}
}
// ===== Bestellabschluss.java =====
package de.csbrad.shop;
/** Ablauf des Bestellabschlusses: Versandart wählen, prüfen, speichern, bestätigen. */
public class Bestellabschluss {
private final Versandarten versandarten = new Versandarten();
private final Mailversand mailversand = new Mailversand();
private final BestellRepository repository = new BestellRepository();
private final BestellungSpeichern speicher = repository;
public Bestellung abschliessen(Bestellanfrage anfrage) {
Versandart versandart = versandarten.zuKennung(anfrage.versandart());
Bestellung bestellung = new Bestellung(anfrage, versandart);
versandart.pruefe(bestellung);
speicher.speichere(bestellung);
mailversand.sendeBestaetigung(bestellung);
return bestellung;
}
public Mailversand mailversand() {
return mailversand;
}
public BestellungLesen repository() {
return repository;
}
}
// ===== 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 Versandart versandart;
private final String gutscheincode;
private final String kundenEmail;
private String bestellnummer;
public Bestellung(Bestellanfrage anfrage, Versandart versandart) {
pruefePositionen(anfrage.positionen());
this.positionen = List.copyOf(anfrage.positionen());
this.versandart = versandart;
this.gutscheincode = anfrage.gutscheincode();
this.kundenEmail = anfrage.kundenEmail();
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 versandart.kosten(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 Versandart 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;
}
}
// ===== BestellungLesen.java =====
package de.csbrad.shop;
import java.util.List;
import java.util.Optional;
/** Schlankes Interface für Auswertungen, z. B. die Bestellübersicht im Backoffice: nur lesen (ISP). */
public interface BestellungLesen {
Optional<Bestellung> findeNachNummer(String bestellnummer);
List<Bestellung> alle();
}
// ===== BestellungSpeichern.java =====
package de.csbrad.shop;
/** Schlankes Interface für den Bestellabschluss: nur speichern (ISP). */
public interface BestellungSpeichern {
/** Speichert die Bestellung und vergibt dabei die Bestellnummer. */
void speichere(Bestellung bestellung);
}
// ===== Expressversand.java =====
package de.csbrad.shop;
/**
* Neu in Stand 08: Expresspaket = Paketregeln plus Zuschlag, ohne Freigrenze.
* Neue Klasse, kein vorhandener Code geändert (außer einer Zeile in Versandarten): Open-Closed-Prinzip.
*/
public class Expressversand implements Versandart {
static final int EXPRESSZUSCHLAG_CENT = 990;
private final Paketversand paketregeln = new Paketversand();
@Override
public void pruefe(Bestellung bestellung) {
paketregeln.pruefe(bestellung);
}
@Override
public int kosten(Bestellung bestellung) {
return paketregeln.anzahlPakete(bestellung) * Paketversand.PAKETPAUSCHALE_CENT + EXPRESSZUSCHLAG_CENT;
}
@Override
public String mailzeile() {
return "Versand: Expresspaket";
}
}
// ===== 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 implements Versandart {
static final int KURIERPAUSCHALE_CENT = 1290;
static final int HOECHSTGEWICHT_KURIER_GRAMM = 25000;
private final Paketversand paketregeln = new Paketversand();
@Override
public void pruefe(Bestellung bestellung) {
paketregeln.pruefe(bestellung);
if (bestellung.gesamtgewichtGramm() > HOECHSTGEWICHT_KURIER_GRAMM) {
throw new IllegalArgumentException("Kurierzustellung nur bis 25 kg");
}
}
@Override
public int kosten(Bestellung bestellung) {
return KURIERPAUSCHALE_CENT;
}
@Override
public String mailzeile() {
return "Versand: Kurier (Lastenrad)";
}
}
// ===== 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" + bestellung.versandart().mailzeile());
}
public List<String> postausgang() {
return List.copyOf(postausgang);
}
private static String alsEuroText(int betragCent) {
return (betragCent / 100) + "," + String.format("%02d", betragCent % 100) + " EUR";
}
}
// ===== Paketversand.java =====
package de.csbrad.shop;
/** Pakete: Pauschale je angefangene 31,5 kg, frei ab 100 Euro Warenwert nach Rabatt, keine Räder. */
public class Paketversand implements Versandart {
static final int PAKETPAUSCHALE_CENT = 690;
static final int HOECHSTGEWICHT_PAKET_GRAMM = 31500;
static final int VERSANDKOSTENFREI_AB_CENT = 10000;
@Override
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);
}
@Override
public int kosten(Bestellung bestellung) {
if (bestellung.warenwertNachRabatt() >= VERSANDKOSTENFREI_AB_CENT) {
return 0;
}
return anzahlPakete(bestellung) * PAKETPAUSCHALE_CENT;
}
@Override
public String mailzeile() {
return "Versand: Paket";
}
}
// ===== 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) {
}
// ===== Speditionsversand.java =====
package de.csbrad.shop;
/** Spedition: Pauschale je Rad; ohne Rad eine Pauschale. */
public class Speditionsversand implements Versandart {
static final int SPEDITION_JE_RAD_CENT = 4900;
@Override
public void pruefe(Bestellung bestellung) {
// per Spedition geht alles
}
@Override
public int kosten(Bestellung bestellung) {
return Math.max(1, bestellung.anzahlRaeder()) * SPEDITION_JE_RAD_CENT;
}
@Override
public String mailzeile() {
return "Versand: Spedition";
}
}
// ===== Versandart.java =====
package de.csbrad.shop;
/** Strategie: jede Versandart prüft, berechnet und beschreibt sich selbst (OCP). */
public interface Versandart {
/** Wirft IllegalArgumentException, wenn die Bestellung so nicht versandt werden kann. */
void pruefe(Bestellung bestellung);
int kosten(Bestellung bestellung);
String mailzeile();
}
// ===== Versandarten.java =====
package de.csbrad.shop;
import java.util.Map;
/** Ordnet die Kennungen aus dem Shop-Formular den Versandarten zu; einzige Stelle, die neue Versandarten kennt. */
public class Versandarten {
private final Map<String, Versandart> nachKennung = Map.of(
"PAKET", new Paketversand(),
"SPEDITION", new Speditionsversand(),
"ABHOLUNG", new Abholung(),
"KURIER", new Kurierzustellung(),
"EXPRESS", new Expressversand());
public Versandart zuKennung(String kennung) {
Versandart versandart = nachKennung.get(kennung);
if (versandart == null) {
throw new IllegalArgumentException("Unbekannte Versandart: " + kennung);
}
return versandart;
}
}
// ===== BestellabschlussTest.java =====
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());
}
// ---- 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)));
}
// ---- neue Anforderung in Stand 08: Expresspaket ----
@Test
void expressOhneFreigrenze() {
assertArrayEquals(new int[] {13361, 0, 1680, 2858, 17899},
betraege(abschluss.abschliessen(new Bestellanfrage(List.of(BREMSE), "EXPRESS", null, MAIL))));
assertEquals("An: mira.engel@example.org\nBestellung CSB-1001\nGesamtbetrag: 178,99 EUR\nVersand: Expresspaket",
abschluss.mailversand().postausgang().get(0));
}
@Test
void expressJePaket() {
// 25 Bügelschlösser = 32,5 kg = 2 Pakete
assertEquals(2 * 690 + 990, abschluss.abschliessen(new Bestellanfrage(List.of(menge(BUEGELSCHLOSS, 25)), "EXPRESS", null, MAIL)).versandkosten());
}
@Test
void expressNimmtKeineRaeder() {
assertThrows(IllegalArgumentException.class,
() -> abschluss.abschliessen(new Bestellanfrage(List.of(STADTRAD), "EXPRESS", null, MAIL)));
}
}a) Erläutere zwei Gründe, warum sich der Bestellabschluss in Stand 08 nicht isoliert testen lässt, und ordne sie einem SOLID-Prinzip zu. (3 Punkte)
Erstens erzeugt der Bestellabschluss seine Abhängigkeiten selbst (new Mailversand(), new BestellRepository(), in Python im __init__). Ein Test kann deshalb keinen Stub oder Mock übergeben; jeder Test läuft durch das echte Repository und den echten Mailversand, nach dem geplanten Umbau also durch den externen Maildienst (1 Punkt). Zweitens hängt er von der konkreten Klasse Mailversand ab: Es gibt keine Abstraktion, die ein Test-Double realisieren könnte (1 Punkt). Beides verstößt gegen das Dependency Inversion Principle: Die fachliche Klasse hängt von technischen Details ab statt von Abstraktionen. Als Folge holen sich die Tests die Ergebnisse über Getter-Ketten wie abschluss.mailversand().postausgang() (Verstoß gegen das Law of Demeter) (1 Punkt).
b) Baue den Code nach dem Dependency Inversion Principle um: Der Mailversand soll eine Abstraktion sein (die bisherige Klasse heißt danach PostausgangMailversand), und der Bestellabschluss soll alle drei Abhängigkeiten über den Konstruktor bekommen und keine Getter mehr für die Tests anbieten. Passe die vorhandenen Tests an; alle 22 Tests müssen grün bleiben. (8 Punkte)
Geändert bzw. neu sind nur Mailversand, PostausgangMailversand, Bestellabschluss und der Aufbau der Tests; alle anderen Klassen bleiben, wie sie sind. Den vollständigen Stand findest Du unter src/kata/java/stand-09 bzw. src/kata/python/stand-09.
// ===== Mailversand.java =====
package de.csbrad.shop;
/** Abstraktion für den Versand der Bestätigungsmail; der Bestellabschluss kennt nur sie (DIP). */
public interface Mailversand {
void sendeBestaetigung(Bestellung bestellung);
}
// ===== PostausgangMailversand.java =====
package de.csbrad.shop;
import java.util.ArrayList;
import java.util.List;
/** Mailversand in einen Postausgang (Liste), z. B. für Tests und die Entwicklungsumgebung. */
public class PostausgangMailversand implements Mailversand {
private final List<String> postausgang = new ArrayList<>();
@Override
public void sendeBestaetigung(Bestellung bestellung) {
postausgang.add("An: " + bestellung.kundenEmail() + "\nBestellung " + bestellung.bestellnummer()
+ "\nGesamtbetrag: " + alsEuroText(bestellung.gesamtbetrag()) + "\n" + bestellung.versandart().mailzeile());
}
public List<String> postausgang() {
return List.copyOf(postausgang);
}
private static String alsEuroText(int betragCent) {
return (betragCent / 100) + "," + String.format("%02d", betragCent % 100) + " EUR";
}
}
// ===== Bestellabschluss.java =====
package de.csbrad.shop;
/**
* Ablauf des Bestellabschlusses: Versandart wählen, prüfen, speichern, bestätigen.
* Hängt nur von Abstraktionen ab; die konkreten Objekte kommen über den Konstruktor (Konstruktor-Injektion).
*/
public class Bestellabschluss {
private final Versandarten versandarten;
private final BestellungSpeichern speicher;
private final Mailversand mailversand;
public Bestellabschluss(Versandarten versandarten, BestellungSpeichern speicher, Mailversand mailversand) {
this.versandarten = versandarten;
this.speicher = speicher;
this.mailversand = mailversand;
}
public Bestellung abschliessen(Bestellanfrage anfrage) {
Versandart versandart = versandarten.zuKennung(anfrage.versandart());
Bestellung bestellung = new Bestellung(anfrage, versandart);
versandart.pruefe(bestellung);
speicher.speichere(bestellung);
mailversand.sendeBestaetigung(bestellung);
return bestellung;
}
}In den Tests ändert sich nur der Aufbau. Die Prüfungen nutzen statt der Getter-Ketten die eigenen Verweise postausgang und repository:
// Auszug aus BestellabschlussTest, Stand 09: Aufbau und eine angepasste Prüfung
private PostausgangMailversand postausgang;
private BestellRepository repository;
@BeforeEach
void aufbauen() {
postausgang = new PostausgangMailversand();
repository = new BestellRepository();
abschluss = new Bestellabschluss(new Versandarten(), repository, postausgang);
}
@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, repository.alle().size());
assertEquals("CSB-1001", repository.alle().get(0).bestellnummer());
assertEquals("CSB-1002", repository.alle().get(1).bestellnummer());
assertEquals(15900, repository.alle().get(1).gesamtbetrag());
}Bewertung: Abstraktion Mailversand mit genau der benötigten Methode und Realisierung durch PostausgangMailversand (2 Punkte), Konstruktor mit den drei Abhängigkeiten, Attributtypen sind die Abstraktionen bzw. Versandarten, keine Erzeugung im Bestellabschluss mehr (3 Punkte), Getter entfernt (1 Punkt), Tests angepasst und grün (2 Punkte). Es ist auch richtig, Versandarten ebenfalls zu injizieren, aber als konkrete Klasse (siehe Teilaufgabe e).
c) Schreibe einen isolierten Test für den Bestellabschluss mit einem handgeschriebenen Stub für den Speicher und einem handgeschriebenen Mock für den Mailversand. Der Test soll zeigen, dass nach einem erfolgreichen Abschluss genau eine Bestätigung mit der Nummer verschickt wird, die der Speicher vergeben hat, und dass bei einer abgelehnten Bestellung keine Bestätigung verschickt wird. (5 Punkte)
package de.csbrad.shop;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Testet den Bestellabschluss isoliert: Speicher als Stub, Mailversand als Mock (handgeschriebene Test-Doubles). */
class BestellabschlussIsoliertTest {
/** Stub: liefert eine feste Nummer, speichert nichts. */
static class SpeicherStub implements BestellungSpeichern {
@Override
public void speichere(Bestellung bestellung) {
bestellung.vergibNummer("CSB-TEST");
}
}
/** Mock: merkt sich die Aufrufe, damit der Test sie prüfen kann. */
static class MailversandMock implements Mailversand {
final List<Bestellung> bestaetigt = new ArrayList<>();
@Override
public void sendeBestaetigung(Bestellung bestellung) {
bestaetigt.add(bestellung);
}
}
private MailversandMock mailversand;
private Bestellabschluss abschluss;
@BeforeEach
void aufbauen() {
mailversand = new MailversandMock();
abschluss = new Bestellabschluss(new Versandarten(), new SpeicherStub(), mailversand);
}
@Test
void schicktGenauEineBestaetigungMitNummer() {
Bestellung bestellung = abschluss.abschliessen(new Bestellanfrage(List.of(BestellabschlussTest.LICHTSET), "PAKET", null, BestellabschlussTest.MAIL));
assertEquals(1, mailversand.bestaetigt.size());
assertEquals("CSB-TEST", mailversand.bestaetigt.get(0).bestellnummer());
assertEquals(bestellung, mailversand.bestaetigt.get(0));
}
@Test
void keineBestaetigungBeiAbgelehnterBestellung() {
assertThrows(IllegalArgumentException.class,
() -> abschluss.abschliessen(new Bestellanfrage(List.of(BestellabschlussTest.STADTRAD), "PAKET", null, BestellabschlussTest.MAIL)));
assertEquals(0, mailversand.bestaetigt.size());
}
}Der Stub vergibt eine feste Nummer, damit die Bestellung gültig bestätigt werden kann; er speichert nichts. Der Mock merkt sich die bestätigten Bestellungen. Weil der Stub die Nummer vergibt, zeigt die Prüfung auf CSB-TEST nebenbei, dass vor dem Bestätigen gespeichert wurde.
Bewertung: Stub realisiert BestellungSpeichern und vergibt eine Nummer (1 Punkt), Mock realisiert Mailversand und protokolliert (1 Punkt), Test für genau eine Bestätigung mit Nummer (2 Punkte), Test ohne Bestätigung bei Ablehnung (1 Punkt). Mit Mockito bzw. unittest.mock gibt es höchstens 3 Punkte, weil die Aufgabe handgeschriebene Test-Doubles verlangt.
d) Zeichne das UML-Klassendiagramm des Bestellabschluss mit seinen Abhängigkeiten und deren Realisierungen nach dem Umbau. (4 Punkte)
Bewertung: Interfaces mit Stereotyp und Operation (1 Punkt), Realisierungen durch BestellRepository und PostausgangMailversand (1 Punkt), gerichtete Assoziationen vom Bestellabschluss zu den Abstraktionen und zu Versandarten statt Kompositionen (1 Punkt), Konstruktor mit den Parametern (1 Punkt).
e) Begründe, warum Versandarten kein Interface bekommen muss, und beschreibe eine Situation, in der Du das anders entscheiden würdest. (2 Punkte)
Versandarten ist eine reine Zuordnung ohne Technik: kein Netzwerk, keine Datei, keine Uhr. Sie ist schnell und liefert immer dasselbe Ergebnis, ein Test kann sie also ohne Nachteil echt verwenden, und es gibt keine zweite Variante. Ein Interface wäre spekulative Allgemeinheit (YAGNI) (1 Punkt). Anders wäre es, wenn die Versandarten zum Beispiel aus einer Datenbank oder einem Konfigurationsdienst geladen würden oder wenn es mehrere Verkaufswege mit unterschiedlichen Versandarten gäbe; dann wäre Versandarten ein Detail, das sich ändert, und eine Abstraktion würde sich lohnen (1 Punkt).
Aufgabe 3.2: Erinnerungsdienst im Mitfahrlabor (Profilkompetenz)
(ca. 22 min Bearbeitungszeit)
In der Mitfahr-App Mitfahrlabor sollen Mitfahrende eine Stunde vor der Abfahrt erinnert werden. Jonas Wolff aus dem Projektteam hat dafür den Erinnerungsdienst geschrieben. Oksana Melnyk möchte ihn mit Unit-Tests absichern und als nächsten Kanal Erinnerungen per Push-Nachricht ausbauen. Ihr Versuch, einen Test zu schreiben, ist gescheitert: Der Test hängt von der aktuellen Uhrzeit ab und verschickt echte Mails. Der Code:
// ===== Mitfahrt.java =====
import java.time.LocalDateTime;
/** Eine bestätigte Mitfahrt: wer, wo, wann. */
public record Mitfahrt(String email, String haltepunkt, LocalDateTime abfahrt) {
}
// ===== FahrtenDatenbank.java =====
import java.util.ArrayList;
import java.util.List;
/** Zugriff auf alle Fahrten und Mitfahrten (hier im Arbeitsspeicher). */
public class FahrtenDatenbank {
private final List<Mitfahrt> bestaetigt = new ArrayList<>();
public void bieteAn(String haltepunkt, int freiePlaetze) { /* ... */ }
public void storniere(int fahrtId) { /* ... */ }
public void bestaetige(Mitfahrt mitfahrt) {
bestaetigt.add(mitfahrt);
}
public List<Mitfahrt> bestaetigteMitfahrten() {
return List.copyOf(bestaetigt);
}
}
// ===== SmtpVersand.java =====
/** Verschickt Mails über den Mailserver der Schule (hier nur angedeutet). */
public class SmtpVersand {
public void sende(String an, String text) {
System.out.println("MAIL an " + an + ": " + text);
}
}
// ===== Erinnerungsdienst.java =====
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/** Erinnert Mitfahrende eine Stunde vor der Abfahrt. */
public class Erinnerungsdienst {
private final FahrtenDatenbank datenbank = new FahrtenDatenbank();
private final SmtpVersand smtp = new SmtpVersand();
public FahrtenDatenbank datenbank() {
return datenbank;
}
public int erinnere(String kanal) {
LocalDateTime jetzt = LocalDateTime.now();
int anzahl = 0;
for (Mitfahrt m : datenbank.bestaetigteMitfahrten()) {
long minuten = Duration.between(jetzt, m.abfahrt()).toMinutes();
if (minuten > 0 && minuten <= 60) {
String text = "Erinnerung: Abfahrt um " + m.abfahrt().format(DateTimeFormatter.ofPattern("HH:mm"))
+ " am Haltepunkt " + m.haltepunkt();
switch (kanal) {
case "MAIL" -> smtp.sende(m.email(), text);
case "PUSH" -> System.out.println("PUSH an " + m.email() + ": " + text);
default -> throw new IllegalArgumentException("Unbekannter Kanal: " + kanal);
}
anzahl = anzahl + 1;
}
}
return anzahl;
}
}
// ===== ErinnerungsdienstTest.java =====
import java.time.LocalDateTime;
/** Vorher-Test (ohne Framework): hängt von der echten Uhrzeit ab und verschickt echte Mails. */
public class ErinnerungsdienstTest {
public static void main(String[] args) {
Erinnerungsdienst dienst = new Erinnerungsdienst();
dienst.datenbank().bestaetige(new Mitfahrt("aylin.kaya@dbh-bielefeld.example",
"Hauptbahnhof, ZOB Steig 3", LocalDateTime.now().plusMinutes(30)));
System.out.println("erinnert: " + dienst.erinnere("MAIL"));
}
}a) Ermittle in einer Tabelle alle Stellen, an denen der Code gegen SOLID-Prinzipien verstößt. Gib zu jeder Stelle das Prinzip und die Folge für Tests oder Erweiterungen an. (6 Punkte)
| Stelle | Prinzip | Folge |
|---|---|---|
new FahrtenDatenbank() im Attribut |
DIP | Test kann keine eigenen Mitfahrten vorgeben, nur über den Getter datenbank() die echte Datenbank befüllen |
new SmtpVersand() im Attribut |
DIP | jeder Test verschickt echte Mails |
LocalDateTime.now() bzw. datetime.now() |
DIP (versteckte Abhängigkeit von der Uhr) | Ergebnis hängt von der Uhrzeit ab, Test nicht wiederholbar |
switch/match über den Kanal, Push direkt mit Ausgabe |
OCP | jeder neue Kanal (z. B. Flurmonitor) ändert erinnere; unbekannter Kanal fällt erst zur Laufzeit auf |
erinnere wählt aus, baut den Text und stellt zu |
SRP | Änderungen am Zeitfenster, am Text und an der Zustellung treffen dieselbe Methode |
Abhängigkeit von der ganzen FahrtenDatenbank, genutzt wird nur bestaetigteMitfahrten |
ISP | ein Test-Double müsste alle Methoden der Datenbank anbieten |
Ein LSP-Verstoß liegt nicht vor, weil es keine Vererbung gibt. Der Getter datenbank(), über den der Test die Datenbank befüllt, ist eine Folge des DIP-Verstoßes.
Bewertung: je Zeile 1 Punkt (Stelle, Prinzip und Folge stimmig), höchstens 6 Punkte; die beiden DIP-Verstöße durch new dürfen in einer Zeile stehen, wenn die Uhr eigens genannt wird.
b) Lege eine Reihenfolge für den Umbau fest und begründe sie. (4 Punkte)
Uhr, Mitfahrten und Zustellung injizieren (DIP, mit ISP): Erst dann lässt sich ein wiederholbarer Test ohne echte Mails schreiben, und jeder weitere Schritt ist durch Tests abgesichert. Die Abstraktion für die Mitfahrten wird dabei gleich schlank geschnitten (
MitfahrtenLesen), weil der Stub sonst alle Methoden der Datenbank anbieten müsste.Kanal als Strategie (OCP): Der Push-Kanal ist die nächste Anforderung und ein weiterer Kanal ist absehbar; die Fallunterscheidung wächst also wirklich. Weil die Zustellung in Schritt 1 ohnehin hinter eine Abstraktion wandert, ist der Schritt klein.
Text abtrennen (SRP): Eine eigene Methode bzw. Funktion für den Text genügt; eine eigene Klasse erst, wenn der Text häufiger geändert wird oder je Kanal verschieden sein soll.
Bewertung: sinnvolle Reihenfolge mit Testbarkeit zuerst (2 Punkte), Begründung mit Änderungsbedarf bzw. YAGNI für die späteren Schritte (2 Punkte). Eine andere Reihenfolge ist richtig, wenn sie schlüssig begründet ist; wer zuerst ohne Tests die Kanäle umbaut, verliert 1 Punkt.
c) Baue den Erinnerungsdienst um, sodass er nur noch von Abstraktionen abhängt und alle Abhängigkeiten über den Konstruktor bekommt; jeder Kanal soll eine eigene Klasse sein. Die Entscheidung für Mail oder Push soll an einer einzigen Stelle außerhalb des Dienstes fallen. Schreibe einen Test mit fester Uhr: Ist es 6:30 Uhr am 10.11.2026, darf von drei Mitfahrten mit Abfahrt um 6:00, 7:15 und 7:45 Uhr nur die um 7:15 Uhr erinnert werden. (10 Punkte)
// ===== MitfahrtenLesen.java =====
import java.util.List;
/** Rolle aus Sicht des Erinnerungsdienstes: nur bestätigte Mitfahrten lesen (ISP, DIP). */
public interface MitfahrtenLesen {
List<Mitfahrt> bestaetigteMitfahrten();
}
// ===== Erinnerungskanal.java =====
/** Ein Weg, eine Erinnerung zuzustellen; neue Kanäle sind neue Klassen (OCP, DIP). */
public interface Erinnerungskanal {
void sende(Mitfahrt mitfahrt, String text);
}
// ===== MailKanal.java =====
/** Erinnerung per Mail; nutzt den vorhandenen SmtpVersand. */
public class MailKanal implements Erinnerungskanal {
private final SmtpVersand smtp;
public MailKanal(SmtpVersand smtp) {
this.smtp = smtp;
}
@Override
public void sende(Mitfahrt mitfahrt, String text) {
smtp.sende(mitfahrt.email(), text);
}
}
// ===== PushKanal.java =====
/** Erinnerung als Push-Nachricht in der App (hier nur angedeutet). */
public class PushKanal implements Erinnerungskanal {
@Override
public void sende(Mitfahrt mitfahrt, String text) {
System.out.println("PUSH an " + mitfahrt.email() + ": " + text);
}
}
// ===== FahrtenDatenbank.java =====
import java.util.ArrayList;
import java.util.List;
/** Zugriff auf alle Fahrten und Mitfahrten; erfüllt unter anderem die Rolle MitfahrtenLesen. */
public class FahrtenDatenbank implements MitfahrtenLesen {
private final List<Mitfahrt> bestaetigt = new ArrayList<>();
public void bieteAn(String haltepunkt, int freiePlaetze) { /* ... */ }
public void storniere(int fahrtId) { /* ... */ }
public void bestaetige(Mitfahrt mitfahrt) {
bestaetigt.add(mitfahrt);
}
@Override
public List<Mitfahrt> bestaetigteMitfahrten() {
return List.copyOf(bestaetigt);
}
}
// ===== Erinnerungsdienst.java =====
import java.time.Clock;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/** Erinnert Mitfahrende eine Stunde vor der Abfahrt; alle Abhängigkeiten kommen über den Konstruktor. */
public class Erinnerungsdienst {
private static final DateTimeFormatter UHRZEIT = DateTimeFormatter.ofPattern("HH:mm");
private final MitfahrtenLesen mitfahrten;
private final Erinnerungskanal kanal;
private final Clock uhr;
public Erinnerungsdienst(MitfahrtenLesen mitfahrten, Erinnerungskanal kanal, Clock uhr) {
this.mitfahrten = mitfahrten;
this.kanal = kanal;
this.uhr = uhr;
}
public int erinnere() {
LocalDateTime jetzt = LocalDateTime.now(uhr);
int anzahl = 0;
for (Mitfahrt m : mitfahrten.bestaetigteMitfahrten()) {
long minuten = Duration.between(jetzt, m.abfahrt()).toMinutes();
if (minuten > 0 && minuten <= 60) {
kanal.sende(m, erinnerungstext(m));
anzahl = anzahl + 1;
}
}
return anzahl;
}
static String erinnerungstext(Mitfahrt m) {
return "Erinnerung: Abfahrt um " + m.abfahrt().format(UHRZEIT) + " am Haltepunkt " + m.haltepunkt();
}
}
// ===== Mitfahrlabor.java =====
import java.time.Clock;
/** Kompositionswurzel: Hier wird entschieden, welche Klassen zusammenarbeiten. */
public class Mitfahrlabor {
public static void main(String[] args) {
FahrtenDatenbank datenbank = new FahrtenDatenbank();
Erinnerungskanal kanal = args.length > 0 && args[0].equals("PUSH")
? new PushKanal() : new MailKanal(new SmtpVersand());
Erinnerungsdienst dienst = new Erinnerungsdienst(datenbank, kanal, Clock.systemDefaultZone());
System.out.println("erinnert: " + dienst.erinnere());
}
}Der Test nutzt eine feste Uhr (Stub), einen Stub für die Mitfahrten und einen Mock für den Kanal. Zusätzlich prüft er die Grenze: Eine Abfahrt genau 60 Minuten später wird noch erinnert.
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
class ErinnerungsdienstTest {
private static final ZoneId ZONE = ZoneId.of("Europe/Berlin");
private static final LocalDateTime JETZT = LocalDateTime.of(2026, 11, 10, 6, 30);
/** Mock: merkt sich, was zugestellt werden sollte. */
static class KanalMock implements Erinnerungskanal {
final List<String> gesendet = new ArrayList<>();
@Override
public void sende(Mitfahrt mitfahrt, String text) {
gesendet.add(mitfahrt.email() + "|" + text);
}
}
private static Mitfahrt abfahrtUm(int stunde, int minute) {
return new Mitfahrt("aylin.kaya@dbh-bielefeld.example", "Hauptbahnhof, ZOB Steig 3",
LocalDateTime.of(2026, 11, 10, stunde, minute));
}
@Test
void erinnertNurFahrtenInDerNaechstenStunde() {
MitfahrtenLesen stub = () -> List.of(abfahrtUm(7, 15), abfahrtUm(7, 45), abfahrtUm(6, 0));
Clock festeUhr = Clock.fixed(JETZT.atZone(ZONE).toInstant(), ZONE);
KanalMock kanal = new KanalMock();
int anzahl = new Erinnerungsdienst(stub, kanal, festeUhr).erinnere();
assertEquals(1, anzahl);
assertEquals(List.of("aylin.kaya@dbh-bielefeld.example|Erinnerung: Abfahrt um 07:15 am Haltepunkt Hauptbahnhof, ZOB Steig 3"),
kanal.gesendet);
}
@Test
void genauEineStundeVorherWirdNochErinnert() {
MitfahrtenLesen stub = () -> List.of(abfahrtUm(7, 30));
Clock festeUhr = Clock.fixed(JETZT.atZone(ZONE).toInstant(), ZONE);
KanalMock kanal = new KanalMock();
assertEquals(1, new Erinnerungsdienst(stub, kanal, festeUhr).erinnere());
}
}Bewertung: Abstraktion für die Mitfahrten, schlank auf bestaetigteMitfahrten (1 Punkt), Abstraktion für den Kanal mit Mail- und Push-Klasse (2 Punkte), Uhr injiziert (Clock bzw. Funktion) (2 Punkte), Konstruktor-Injektion ohne new und ohne now() im Dienst (1 Punkt), Auswahl des Kanals in der Kompositionswurzel (1 Punkt), Test mit fester Uhr und den drei Abfahrten grün (3 Punkte).
d) Zeichne das UML-Klassendiagramm nach dem Umbau mit allen Interfaces, ihren Realisierungen und den Beziehungen des Erinnerungsdienst. (4 Punkte)
Bewertung: zwei Interfaces mit Stereotyp und Operation (1 Punkt), Realisierungen durch FahrtenDatenbank, MailKanal und PushKanal (1 Punkt), gerichtete Assoziationen des Erinnerungsdienst mit Multiplizitäten (1 Punkt), Konstruktor bzw. Uhr und SmtpVersand hinter MailKanal (1 Punkt).
Aufgabe 4.1: Wann ist Dependency Injection zu viel? (Expertise)
(ca. 11 min Bearbeitungszeit)
Nach dem Umbau des Erinnerungsdienstes diskutiert das Projektteam des Mitfahrlabors, wie es mit Abhängigkeiten künftig umgehen will.
Jonas Wolff schlägt vor: „Wir stellen die ganze App auf Spring Boot um und machen es überall gleich: Jede Klasse bekommt ein Interface, auch der Kostenbeitragsrechner, der nur ausrechnet, ob ein Kostenbeitrag höchstens 0,10 € je km und Person beträgt, und auch Wertobjekte wie Haltepunkt. Alles wird vom Container injiziert, kein einziges new mehr im Code.“
Oksana Melnyk hält dagegen: „Das ist viel zu viel. Wir bleiben bei new und schreiben keine Interfaces. Was wir im Test austauschen müssen, ersetzen wir mit patch bzw. einem Mock-Framework, das auch statische Aufrufe und Konstruktoren ersetzen kann.“
a) Nimm Stellung zu beiden Vorschlägen. Gehe dabei auf Testbarkeit, Verständlichkeit des Codes und Aufwand ein. (6 Punkte)
Zu Jonas: Ein Container nimmt die Verdrahtung ab und ist in großen Anwendungen mit vielen Klassen hilfreich; Spring macht Konstruktor-Injektion bequem (1 Punkt). „Jede Klasse bekommt ein Interface“ ist aber spekulative Allgemeinheit: Der Kostenbeitragsrechner ist reine Rechnung ohne Technik, er ist schnell und deterministisch, ein Test verwendet ihn am besten echt. Ein Interface mit genau einer Implementierung bringt dort nur Indirektion. Wertobjekte wie Haltepunkt werden ohnehin mit Daten erzeugt; sie aus einem Container zu holen ist sinnlos (1 Punkt). Der Container verschiebt Fehler in der Verdrahtung vom Übersetzen auf den Start, macht den Ablauf schwerer nachvollziehbar und bindet den Code an die Annotationen des Frameworks; für eine kleine Schul-App ist der Aufwand fraglich (1 Punkt).
Zu Oksana: Weniger Interfaces sind für stabile, fachliche Klassen richtig (KISS, YAGNI) (1 Punkt). Abhängigkeiten von Mailserver, Datenbank und Uhr mit patch oder mit Mock-Frameworks zu ersetzen, die Konstruktoren und statische Methoden abfangen, verdeckt aber das Problem: Die Abhängigkeiten bleiben unsichtbar in der Klasse, die Tests hängen von internen Details wie Modul- und Klassennamen ab und brechen bei harmlosen Umbauten, und einen anderen Kanal kann man im Betrieb weiterhin nur durch Änderung der Klasse einbauen (1 Punkt).
Eigene Position: Ein Mittelweg ist sinnvoll: Abhängigkeiten, die sich ändern oder Tests stören, bekommen eine schlanke Abstraktion und werden über den Konstruktor übergeben; stabile Fachklassen und Wertobjekte werden direkt erzeugt. Verdrahtet wird zunächst von Hand in einer Kompositionswurzel; ein Container lohnt sich erst, wenn die Verdrahtung unübersichtlich wird oder das Framework ohnehin eingesetzt wird (1 Punkt).
b) Formuliere drei Faustregeln, nach denen das Team entscheiden kann, ob eine Abhängigkeit eine Abstraktion bekommt und injiziert wird. Wende sie auf den Kostenbeitragsrechner, den Mailversand und die Uhr an. (6 Punkte)
Mögliche Faustregeln (je 1 Punkt, andere schlüssige Regeln sind ebenso richtig):
Technik und Außenwelt abstrahieren: Alles, was über die Grenzen des Programms greift (Datenbank, Dateien, Netzwerk, Mail, Uhr, Zufall), bekommt eine Abstraktion und wird injiziert.
Testbarkeit entscheidet: Wenn ein Test mit der echten Klasse langsam, nicht wiederholbar oder mit Nebenwirkungen verbunden wäre, wird injiziert; sonst darf der Test die echte Klasse verwenden.
Zweite Variante, nicht Vorrat: Eine Abstraktion für Fachlogik erst dann, wenn es eine zweite Variante wirklich gibt oder sie konkret ansteht (YAGNI, Rule of Three).
Anwendung (je 1 Punkt):
| Abhängigkeit | Entscheidung | Begründung |
|---|---|---|
Kostenbeitragsrechner |
direkt erzeugen oder als konkrete Klasse übergeben, kein Interface | reine Rechnung, schnell und deterministisch, keine zweite Variante |
| Mailversand | Abstraktion und Konstruktor-Injektion | Außenwelt mit Nebenwirkung, Tests sollen keine Mails verschicken, weitere Kanäle sind geplant |
| Uhr | injizieren (Clock bzw. Funktion), kein eigenes Interface nötig |
Ergebnis hängt von der Zeit ab, Tests brauchen eine feste Uhr; die Standardbibliothek liefert die Abstraktion schon |
Links und weitere Informationen
SOLID im Überblick: fünf Prinzipien und die Dependency Inversion
Refactoring und Vom Entwurf zum Code (nächster Schritt der Kata)
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 zu SOLID und Dependency Inversion” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/solid-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: 09.11.2026.
[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]
