Übungsaufgaben zum Gliedern von Methoden: Single Level of Abstraction, IOSP, eine Aufgabe je Methode und Fail fast
https://bildung.social/@oerinformatik/
https://oer-informatik.de/methoden-gliedern-uebungsaufgaben
Clean Code, Single Level of Abstraction, IOSP, Integration, Operation, Parameterobjekt, Flag-Parameter, Fail fast, Guard Clause, zyklomatische Komplexität, Übungsaufgaben, Prüfungsvorbereitung
tl/dr; (Bearbeitungszeit insgesamt 68 min, mehrere Aufgaben): Drei Übungsaufgaben zum Gliedern von Methoden: Du trennst im Bestellabschluss der CSB-Rad-Kata Integration und Operation und ziehst alle Prüfungen nach vorn, baust die Anfragebestätigung des Mitfahrlabors mit Guard Clauses, Ausnahmen und einem Parameterobjekt um und gliederst den Erinnerungsdienst so, dass er sich wie sein Aktivitätsdiagramm liest [AI-Assisted]. (Zuletzt geändert am 10.11.2026)
Diese Übungsaufgaben gehören zum Artikel Methoden gliedern: Single Level of Abstraction, IOSP, eine Aufgabe je Methode und Fail fast.
Hinweise zur Bearbeitung: Als FIA musst Du zur Teil-2-Prüfung Aufgaben auf Kernkompetenz-Ebene lösen können, das sind Aufgabe 2.1 und Aufgabe 2.2. 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 alle drei Aufgaben brauchst Du eine Entwicklungsumgebung mit JUnit 5 bzw. pytest, damit Du die Tests nach jedem Umbauschritt ausführen kannst. Achte auf die Operatoren: „Ordne zu“ verlangt die Zuordnung mit kurzer Begründung, „Nenne“ verlangt Stichworte, „Bestimme“ verlangt das Ergebnis mit Rechenweg, „Vergleiche“ verlangt Gemeinsamkeiten und Unterschiede, „Baue um“ und „Schreibe“ verlangen lauffähigen Code, „Zeichne“ verlangt ein UML-Diagramm, „Nimm Stellung“ verlangt 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 „Gliedern von Methoden: Single
Level of Abstraction, IOSP, eine Aufgabe je Methode und Fail fast“.
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/methoden-gliedern
- https://oer-informatik.de/refactoring
- https://oer-informatik.de/zyklomatische-komplexitaet
- https://oer-informatik.de/uml-aktivitaetsdiagramm
- https://oer-informatik.de/klassen-schneiden
- Stilbeispiel: https://oer-informatik.de/methoden-gliedern-uebungsaufgaben
Die Aufgaben sollen Inhalte zu diesen Schlagwörtern abdecken:
- Clean Code
- Single Level of Abstraction
- IOSP
- Integration
- Operation
- Parameterobjekt
- Flag-Parameter
- Fail fast
- Guard Clause
- zyklomatische Komplexität
Kompetenzstufe: Kernkompetenz (alternativ: Basiskompetenz, Profilkompetenz, 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: Integration und Operation im Bestellabschluss trennen (Kernkompetenz)
(ca. 30 min Bearbeitungszeit)
Im Onlineshop der CSB-Rad GmbH schließt die Klasse Shop Bestellungen ab. Nach dem letzten Refactoring ist die Methode abschliessen in benannte Hilfsmethoden zerlegt, und ein Parameterobjekt Bestellanfrage bündelt die Eingaben (Stand 04 der Code-Kata dieser Artikelreihe). 16 Tests sichern das Verhalten ab. Trotzdem mischen einige Methoden Aufrufe mit eigener Rechnung, und die Ablehnungsregeln stecken in Rechenmethoden. Den Code und die Tests findest Du auch im Ordner src/kata/java/stand-04 bzw. src/kata/python/stand-04 des Repositorys zur Reihe.
Die fachlichen Regeln (Beträge in Cent, alle Preise netto, Gewichte in Gramm):
Gutscheincode
KURIER10: 10 % Rabatt auf den Warenwert, kaufmännisch gerundet; andere Codes werden ignoriert.Paket: 6,90 € je angefangene 31,5 kg, frei ab 100,00 € Warenwert nach Rabatt; Räder können nicht als Paket versandt werden.
Spedition: 49,00 € je Rad, ohne Rad einmal 49,00 €. Abholung in der Werkstatt Bielefeld-Sennestadt: kostenlos.
Mehrwertsteuer 19 % auf Warenwert − Rabatt + Versand; abgelehnt werden Bestellungen ohne Positionen, mit Menge ≤ 0, mit Warenwert 0 und mit unbekannter Versandart.
// 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) {
}
// Shop.java
package de.csbrad.shop;
import java.util.ArrayList;
import java.util.List;
public class Shop {
private static final int ERSTE_BESTELLNUMMER = 1001;
private static final String GUTSCHEINCODE_KURIER = "KURIER10";
private static final int GUTSCHEIN_RABATT_PROZENT = 10;
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;
private static final int MEHRWERTSTEUER_PROZENT = 19;
private int naechsteBestellnummer = ERSTE_BESTELLNUMMER;
private final List<Object[]> bestellungen = new ArrayList<>();
private final List<String> postausgang = new ArrayList<>();
public List<Object[]> getBestellungen() {
return bestellungen;
}
public List<String> getPostausgang() {
return postausgang;
}
/**
* Berechnet die Beträge einer Bestellung, verschickt die Bestätigungsmail und speichert die Bestellung.
*
* @return {Warenwert, Rabatt, Versandkosten, Mehrwertsteuer, Gesamtbetrag} in Cent
*/
public int[] abschliessen(Bestellanfrage anfrage) {
int warenwert = berechneWarenwert(anfrage.positionen());
int rabatt = berechneRabatt(warenwert, anfrage.gutscheincode());
int versandkosten = berechneVersandkosten(anfrage.positionen(), anfrage.versandart(), warenwert - rabatt);
if (warenwert == 0) {
throw new IllegalArgumentException("Bestellung ohne Warenwert");
}
int nettobetrag = warenwert - rabatt + versandkosten;
int mehrwertsteuer = berechneMehrwertsteuer(nettobetrag);
int gesamtbetrag = nettobetrag + mehrwertsteuer;
int bestellnummer = vergibBestellnummer();
postausgang.add(erstelleMailtext(anfrage, bestellnummer, gesamtbetrag));
bestellungen.add(new Object[] {"CSB-" + bestellnummer, anfrage.kundenEmail(), anfrage.positionen(), gesamtbetrag});
return new int[] {warenwert, rabatt, versandkosten, mehrwertsteuer, gesamtbetrag};
}
private int berechneWarenwert(List<Position> positionen) {
int warenwert = 0;
for (Position position : positionen) {
if (position.menge() <= 0) {
throw new IllegalArgumentException("Menge muss positiv sein: " + position.artikelnr());
}
warenwert = warenwert + position.nettopreisCent() * position.menge();
}
return warenwert;
}
private int berechneRabatt(int warenwert, String gutscheincode) {
if (GUTSCHEINCODE_KURIER.equals(gutscheincode)) {
return prozentKaufmaennischGerundet(warenwert, GUTSCHEIN_RABATT_PROZENT);
}
return 0;
}
private int berechneVersandkosten(List<Position> positionen, String versandart, int warenwertNachRabatt) {
switch (versandart) {
case "PAKET":
int anzahlPakete = berechneAnzahlPakete(positionen);
if (warenwertNachRabatt >= VERSANDKOSTENFREI_AB_CENT) {
return 0;
}
return anzahlPakete * PAKETPAUSCHALE_CENT;
case "SPEDITION":
// ohne Rad wird eine Speditionspauschale berechnet
return Math.max(1, zaehleRaeder(positionen)) * SPEDITION_JE_RAD_CENT;
case "ABHOLUNG":
return 0;
default:
throw new IllegalArgumentException("Unbekannte Versandart: " + versandart);
}
}
private int berechneAnzahlPakete(List<Position> positionen) {
int gesamtgewicht = 0;
for (Position position : positionen) {
if (position.istRad()) {
throw new IllegalArgumentException("Räder können nicht als Paket versandt werden");
}
gesamtgewicht = gesamtgewicht + position.gewichtGramm() * position.menge();
}
int anzahlPakete = (gesamtgewicht + HOECHSTGEWICHT_PAKET_GRAMM - 1) / HOECHSTGEWICHT_PAKET_GRAMM;
return Math.max(1, anzahlPakete);
}
private int zaehleRaeder(List<Position> positionen) {
int anzahlRaeder = 0;
for (Position position : positionen) {
if (position.istRad()) {
anzahlRaeder = anzahlRaeder + position.menge();
}
}
return anzahlRaeder;
}
private int berechneMehrwertsteuer(int nettobetrag) {
return prozentKaufmaennischGerundet(nettobetrag, MEHRWERTSTEUER_PROZENT);
}
private static int prozentKaufmaennischGerundet(int betragCent, int prozent) {
return (betragCent * prozent + 50) / 100;
}
private int vergibBestellnummer() {
int bestellnummer = naechsteBestellnummer;
naechsteBestellnummer = naechsteBestellnummer + 1;
return bestellnummer;
}
private String erstelleMailtext(Bestellanfrage anfrage, int bestellnummer, int gesamtbetrag) {
String mailtext = "An: " + anfrage.kundenEmail() + "\nBestellung CSB-" + bestellnummer + "\nGesamtbetrag: "
+ alsEuroText(gesamtbetrag);
if (anfrage.versandart().equals("ABHOLUNG")) {
return mailtext + "\nAbholung in der Werkstatt Bielefeld-Sennestadt";
}
return mailtext + "\nVersand: " + versandbezeichnung(anfrage.versandart());
}
private static String versandbezeichnung(String versandart) {
return switch (versandart) {
case "PAKET" -> "Paket";
case "SPEDITION" -> "Spedition";
default -> "Abholung";
};
}
private static String alsEuroText(int betragCent) {
return (betragCent / 100) + "," + String.format("%02d", betragCent % 100) + " EUR";
}
}Die Tests:
// ShopCharakterisierungTest.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: halten fest, was der Code heute tut (seit Stand 04 im Testordner).
* Ergebnis-Array: {Warenwert, Rabatt, Versandkosten, Mehrwertsteuer, Gesamtbetrag} in Cent.
*/
class ShopCharakterisierungTest {
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 Shop shop;
@BeforeEach
void neuerShop() {
shop = new Shop();
}
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}, shop.abschliessen(new Bestellanfrage(List.of(LICHTSET), "PAKET", null, MAIL)));
}
@Test
void paketAbFreigrenzeIstVersandfrei() {
assertArrayEquals(new int[] {13361, 0, 0, 2539, 15900}, shop.abschliessen(new Bestellanfrage(List.of(BREMSE), "PAKET", null, MAIL)));
}
@Test
void freigrenzeGiltNachRabatt() {
assertArrayEquals(new int[] {10336, 1034, 690, 1898, 11890},
shop.abschliessen(new Bestellanfrage(List.of(BUEGELSCHLOSS, RAHMENSCHLOSS), "PAKET", "KURIER10", MAIL)));
}
@Test
void ohneRabattUeberFreigrenze() {
assertArrayEquals(new int[] {10336, 0, 0, 1964, 12300},
shop.abschliessen(new Bestellanfrage(List.of(BUEGELSCHLOSS, RAHMENSCHLOSS), "PAKET", null, MAIL)));
}
@Test
void schwerePaketbestellungMitRabatt() {
assertArrayEquals(new int[] {155450, 15545, 0, 26582, 166487},
shop.abschliessen(new Bestellanfrage(List.of(menge(BUEGELSCHLOSS, 25)), "PAKET", "KURIER10", MAIL)));
}
@Test
void speditionJeRad() {
assertArrayEquals(new int[] {149412, 0, 9800, 30250, 189462},
shop.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}, shop.abschliessen(new Bestellanfrage(List.of(LICHTSET), "SPEDITION", null, MAIL)));
}
@Test
void abholungMitRabattcode() {
assertArrayEquals(new int[] {252017, 25202, 0, 43095, 269910}, shop.abschliessen(new Bestellanfrage(List.of(EBIKE), "ABHOLUNG", "KURIER10", MAIL)));
}
@Test
void unbekannterCodeGibtKeinenRabatt() {
assertArrayEquals(new int[] {7563, 0, 690, 1568, 9821}, shop.abschliessen(new Bestellanfrage(List.of(LICHTSET), "PAKET", "SOMMER20", MAIL)));
}
@Test
void radAlsPaketWirdAbgelehnt() {
assertThrows(IllegalArgumentException.class, () -> shop.abschliessen(new Bestellanfrage(List.of(STADTRAD), "PAKET", null, MAIL)));
assertEquals(0, shop.getPostausgang().size());
assertEquals(0, shop.getBestellungen().size());
}
@Test
void leereBestellungWirdAbgelehnt() {
assertThrows(IllegalArgumentException.class, () -> shop.abschliessen(new Bestellanfrage(List.of(), "PAKET", null, MAIL)));
assertEquals(0, shop.getPostausgang().size());
}
@Test
void mengeNullWirdAbgelehnt() {
assertThrows(IllegalArgumentException.class, () -> shop.abschliessen(new Bestellanfrage(List.of(menge(LICHTSET, 0)), "PAKET", null, MAIL)));
}
@Test
void unbekannteVersandartWirdAbgelehnt() {
assertThrows(IllegalArgumentException.class, () -> shop.abschliessen(new Bestellanfrage(List.of(LICHTSET), "BRIEF", null, MAIL)));
}
@Test
void mailFuerPaketversand() {
shop.abschliessen(new Bestellanfrage(List.of(LICHTSET), "PAKET", null, MAIL));
assertEquals("An: mira.engel@example.org\nBestellung CSB-1001\nGesamtbetrag: 98,21 EUR\nVersand: Paket", shop.getPostausgang().get(0));
}
@Test
void mailFuerAbholung() {
shop.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",
shop.getPostausgang().get(0));
}
@Test
void bestellungenWerdenFortlaufendNummeriertUndGespeichert() {
shop.abschliessen(new Bestellanfrage(List.of(LICHTSET), "PAKET", null, MAIL));
shop.abschliessen(new Bestellanfrage(List.of(BREMSE), "PAKET", null, MAIL));
assertEquals(2, shop.getBestellungen().size());
assertEquals("CSB-1001", shop.getBestellungen().get(0)[0]);
assertEquals("CSB-1002", shop.getBestellungen().get(1)[0]);
assertEquals(15900, shop.getBestellungen().get(1)[3]);
}
}a) Ordne jede Methode der Klasse Shop außer den beiden Gettern einer der Arten „Integration“ (ruft nur eigene Methoden auf), „Operation“ (enthält Logik, ruft keine eigenen Methoden auf) oder „Mischung“ zu. Begründe jede Mischung in wenigen Worten. (6 Punkte)
| Methode | Art | Begründung bei Mischung |
|---|---|---|
abschliessen |
||
berechneWarenwert |
||
berechneRabatt |
||
berechneVersandkosten |
||
berechneAnzahlPakete |
||
zaehleRaeder |
||
berechneMehrwertsteuer |
||
prozentKaufmaennischGerundet |
||
vergibBestellnummer |
||
erstelleMailtext |
||
versandbezeichnung |
||
alsEuroText |
| Methode | Art | Begründung bei Mischung |
|---|---|---|
abschliessen |
Mischung | ruft Methoden auf, prüft aber selbst (if warenwert == 0), rechnet (warenwert - rabatt + versandkosten) und baut die Speicherzeile |
berechneWarenwert |
Operation | (rechnet und prüft die Mengen: eine Operation mit zwei Aufgaben) |
berechneRabatt |
Mischung | prüft den Gutscheincode und ruft prozentKaufmaennischGerundet auf |
berechneVersandkosten |
Mischung | verzweigt, vergleicht mit der Freigrenze, multipliziert und ruft berechneAnzahlPakete und zaehleRaeder auf |
berechneAnzahlPakete |
Operation | (rechnet und lehnt Räder ab: zwei Aufgaben) |
zaehleRaeder |
Operation | |
berechneMehrwertsteuer |
Integration | |
prozentKaufmaennischGerundet |
Operation | |
vergibBestellnummer |
Operation | |
erstelleMailtext |
Mischung | baut den Text selbst zusammen, verzweigt nach Abholung und ruft alsEuroText und versandbezeichnung auf |
versandbezeichnung |
Operation | |
alsEuroText |
Operation |
Bewertung: je zwei richtige Zuordnungen 1 Punkt, höchstens 6 Punkte; fehlt bei einer Mischung die Begründung, zählt die Zuordnung nur halb. Wer berechneMehrwertsteuer als Operation einordnet, weil sie nur eine Zeile hat, hat das Kriterium verwechselt: Entscheidend ist, ob eigene Methoden aufgerufen werden.
b) Nenne die Stellen, an denen Stand 04 eine Bestellung ablehnt, und erläutere an zwei Beispielen, warum diese Anordnung gegen „Fail fast“ und gegen „eine Aufgabe je Methode“ verstößt. (4 Punkte)
Stellen (1 Punkt für alle vier):
- Menge ≤ 0: in
berechneWarenwert, - Rad im Paket: in
berechneAnzahlPakete, - unbekannte Versandart: im
default-Zweig vonberechneVersandkosten, - Warenwert 0 (und damit auch „ohne Positionen“): in
abschliessen, nach der Berechnung von Rabatt und Versandkosten.
Erläuterung (je Beispiel 1,5 Punkte):
Fail fast: Die Bestellung ohne Warenwert wird erst abgelehnt, nachdem Rabatt und Versandkosten schon berechnet sind. Die Prüfung der Versandart hängt davon ab, dass die Versandkosten überhaupt berechnet werden. Wer die Reihenfolge der Rechenschritte ändert, ändert unbemerkt auch, wann und ob abgelehnt wird. Prüfungen gehören an den Anfang, vor jede Berechnung.
Eine Aufgabe je Methode:
berechneWarenwertrechnet und prüft,berechneAnzahlPaketerechnet und lehnt Räder ab. Die Namen verschweigen die zweite Aufgabe; wer den Warenwert an anderer Stelle braucht, bekommt die Prüfung mit, und wer die Ablehnungsregeln sucht, findet sie nicht.
Eine Bestellung ohne Positionen wird nur zufällig abgelehnt, weil ihr Warenwert 0 ist; eine eigene Regel dafür fehlt.
c) Baue die Klasse Shop in kleinen Schritten so um, dass abschliessen nur noch integriert: prüfen, Beträge berechnen, Nummer vergeben, Bestätigung versenden, speichern. Alle Prüfungen sollen in eigenen Methoden stehen und vor der ersten Berechnung laufen; Operationen ohne Zugriff auf den Zustand sollen in Java static bzw. in Python Funktionen auf Modulebene sein. Führe nach jedem Schritt die Tests aus; sie dürfen nicht geändert werden. (9 Punkte)
Die Beispiellösung entspricht Stand 05 der Kata (Ordner src/kata/java/stand-05 bzw. src/kata/python/stand-05); Position und Bestellanfrage bleiben unverändert, alle 16 Tests sind grün.
// Shop.java
package de.csbrad.shop;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class Shop {
private static final int ERSTE_BESTELLNUMMER = 1001;
private static final String GUTSCHEINCODE_KURIER = "KURIER10";
private static final int GUTSCHEIN_RABATT_PROZENT = 10;
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;
private static final int MEHRWERTSTEUER_PROZENT = 19;
private static final Set<String> VERSANDARTEN = Set.of("PAKET", "SPEDITION", "ABHOLUNG");
private static final int GESAMTBETRAG = 4;
private int naechsteBestellnummer = ERSTE_BESTELLNUMMER;
private final List<Object[]> bestellungen = new ArrayList<>();
private final List<String> postausgang = new ArrayList<>();
public List<Object[]> getBestellungen() {
return bestellungen;
}
public List<String> getPostausgang() {
return postausgang;
}
// ---- Integration: ruft nur auf, rechnet nicht selbst ----
/**
* Schließt eine Bestellung ab: prüfen, berechnen, Nummer vergeben, bestätigen, speichern.
*
* @return {Warenwert, Rabatt, Versandkosten, Mehrwertsteuer, Gesamtbetrag} in Cent
*/
public int[] abschliessen(Bestellanfrage anfrage) {
pruefe(anfrage);
int[] betraege = berechneBetraege(anfrage);
int bestellnummer = vergibBestellnummer();
versendeBestaetigung(anfrage, bestellnummer, betraege[GESAMTBETRAG]);
speichere(anfrage, bestellnummer, betraege[GESAMTBETRAG]);
return betraege;
}
private void pruefe(Bestellanfrage anfrage) {
pruefeMengen(anfrage.positionen());
pruefeWarenwert(berechneWarenwert(anfrage.positionen()));
pruefeVersandart(anfrage.versandart());
pruefeVersandmoeglich(anfrage.positionen(), anfrage.versandart());
}
private int[] berechneBetraege(Bestellanfrage anfrage) {
int warenwert = berechneWarenwert(anfrage.positionen());
int rabatt = berechneRabatt(warenwert, anfrage.gutscheincode());
int versandkosten = berechneVersandkosten(anfrage.positionen(), anfrage.versandart(), warenwert - rabatt);
int nettobetrag = warenwert - rabatt + versandkosten;
int mehrwertsteuer = berechneMehrwertsteuer(nettobetrag);
return new int[] {warenwert, rabatt, versandkosten, mehrwertsteuer, nettobetrag + mehrwertsteuer};
}
private int berechneVersandkosten(List<Position> positionen, String versandart, int warenwertNachRabatt) {
return switch (versandart) {
case "PAKET" -> berechnePaketkosten(berechneAnzahlPakete(positionen), warenwertNachRabatt);
case "SPEDITION" -> berechneSpeditionskosten(zaehleRaeder(positionen));
default -> 0;
};
}
// ---- Operationen: rechnen oder prüfen, rufen keine eigenen Methoden auf ----
private static void pruefeMengen(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());
}
}
}
private static void pruefeWarenwert(int warenwert) {
if (warenwert == 0) {
throw new IllegalArgumentException("Bestellung ohne Warenwert");
}
}
private static void pruefeVersandart(String versandart) {
if (!VERSANDARTEN.contains(versandart)) {
throw new IllegalArgumentException("Unbekannte Versandart: " + versandart);
}
}
private static void pruefeVersandmoeglich(List<Position> positionen, String versandart) {
boolean enthaeltRad = positionen.stream().anyMatch(Position::istRad);
if (versandart.equals("PAKET") && enthaeltRad) {
throw new IllegalArgumentException("Räder können nicht als Paket versandt werden");
}
}
private static int berechneWarenwert(List<Position> positionen) {
int warenwert = 0;
for (Position position : positionen) {
warenwert = warenwert + position.nettopreisCent() * position.menge();
}
return warenwert;
}
private static int berechneRabatt(int warenwert, String gutscheincode) {
if (GUTSCHEINCODE_KURIER.equals(gutscheincode)) {
return (warenwert * GUTSCHEIN_RABATT_PROZENT + 50) / 100;
}
return 0;
}
private static int berechneAnzahlPakete(List<Position> positionen) {
int gesamtgewicht = 0;
for (Position position : positionen) {
gesamtgewicht = gesamtgewicht + position.gewichtGramm() * position.menge();
}
return Math.max(1, (gesamtgewicht + HOECHSTGEWICHT_PAKET_GRAMM - 1) / HOECHSTGEWICHT_PAKET_GRAMM);
}
private static int berechnePaketkosten(int anzahlPakete, int warenwertNachRabatt) {
if (warenwertNachRabatt >= VERSANDKOSTENFREI_AB_CENT) {
return 0;
}
return anzahlPakete * PAKETPAUSCHALE_CENT;
}
private static int zaehleRaeder(List<Position> positionen) {
int anzahlRaeder = 0;
for (Position position : positionen) {
if (position.istRad()) {
anzahlRaeder = anzahlRaeder + position.menge();
}
}
return anzahlRaeder;
}
private static int berechneSpeditionskosten(int anzahlRaeder) {
// ohne Rad wird eine Speditionspauschale berechnet
return Math.max(1, anzahlRaeder) * SPEDITION_JE_RAD_CENT;
}
private static int berechneMehrwertsteuer(int nettobetrag) {
return (nettobetrag * MEHRWERTSTEUER_PROZENT + 50) / 100;
}
private int vergibBestellnummer() {
int bestellnummer = naechsteBestellnummer;
naechsteBestellnummer = naechsteBestellnummer + 1;
return bestellnummer;
}
private void versendeBestaetigung(Bestellanfrage anfrage, int bestellnummer, int gesamtbetrag) {
String betrag = (gesamtbetrag / 100) + "," + String.format("%02d", gesamtbetrag % 100) + " EUR";
String versandzeile = switch (anfrage.versandart()) {
case "PAKET" -> "Versand: Paket";
case "SPEDITION" -> "Versand: Spedition";
default -> "Abholung in der Werkstatt Bielefeld-Sennestadt";
};
postausgang.add("An: " + anfrage.kundenEmail() + "\nBestellung CSB-" + bestellnummer + "\nGesamtbetrag: " + betrag + "\n" + versandzeile);
}
private void speichere(Bestellanfrage anfrage, int bestellnummer, int gesamtbetrag) {
bestellungen.add(new Object[] {"CSB-" + bestellnummer, anfrage.kundenEmail(), anfrage.positionen(), gesamtbetrag});
}
}Bewertung:
abschliessenenthält nur Aufrufe in der Reihenfolge prüfen, berechnen, Nummer, Bestätigung, Speichern (2 Punkte),- eine Prüfmethode je Ablehnungsregel, aufgerufen vor der ersten Berechnung; Prüfungen aus den Rechenmethoden entfernt (3 Punkte),
- Rechenmethoden und Prüfmethoden als Operationen ohne Aufrufe eigener Methoden,
staticbzw. auf Modulebene (2 Punkte), - Tests unverändert grün (2 Punkte).
Andere Schnitte sind gleichwertig, zum Beispiel eine Operation erstelleMailtext, die versendeBestaetigung aufruft, oder die Beibehaltung von prozentKaufmaennischGerundet (dann wird berechneRabatt zu einer kleinen Mischung, siehe d).
d) In der Beispiellösung enthält berechneBetraege die Ausdrücke warenwert - rabatt und nettobetrag + mehrwertsteuer, und berechneVersandkosten wählt mit switch bzw. match nach der Versandart aus. Nimm Stellung, ob das gegen das IOSP verstößt und ob Du es ändern würdest. (3 Punkte)
Streng genommen ja: Eine Integration soll weder rechnen noch verzweigen (1 Punkt). Eine strikte Lösung bräuchte eigene Operationen wie berechneNettobetrag(warenwert, rabatt, versandkosten) und für die Auswahl nach Versandart eine Operation, die nur die Versandart einem Kennzeichen zuordnet, oder später eine eigene Klasse je Versandart. Die Ausdrücke enthalten aber keine Fachregel, die sich ändern könnte, und lassen sich auf einen Blick prüfen; eigene Methoden dafür würden das Lesen eher erschweren (viele Mini-Methoden). Ich würde sie deshalb lassen und die pragmatische Ausnahme im Team festhalten (2 Punkte für eine begründete Entscheidung, auch die Gegenposition ist mit Begründung richtig). Die Auswahl nach Versandart wird ohnehin verschwinden, wenn jede Versandart eine eigene Klasse bekommt.
Aufgabe 2.2: Anfragen im Mitfahrlabor sicher bestätigen (Kernkompetenz)
(ca. 18 min Bearbeitungszeit)
In der Mitfahr-App Mitfahrlabor bestätigen Fahrer*innen Mitfahranfragen. Dabei wird ein Platz belegt, der Kostenbeitrag berechnet (Strecke in km mal Beitrag je km, höchstens 0,10 € je km und Person) und die mitfahrende Person per Push-Nachricht oder Mail benachrichtigt. Minderjährige dürfen nur mitfahren, wenn die Einwilligung der Erziehungsberechtigten vorliegt. Oksana Melnyk aus dem Projektteam sieht sich die Methode bestaetige an, weil die Oberfläche bei Lukas Hofmann einmal „Kostenbeitrag 0,00 EUR“ angezeigt hat, obwohl seine Anfrage gar nicht bestätigt wurde.
// Fahrt.java
package de.mitfahrlabor;
import java.util.ArrayList;
import java.util.List;
public class Fahrt {
private final String fahrer;
private final int plaetze;
private final int beitragJeKmCent;
private final List<String> mitfahrende = new ArrayList<>();
public Fahrt(String fahrer, int plaetze, int beitragJeKmCent) {
this.fahrer = fahrer;
this.plaetze = plaetze;
this.beitragJeKmCent = beitragJeKmCent;
}
public String fahrer() {
return fahrer;
}
public int beitragJeKmCent() {
return beitragJeKmCent;
}
public int freiePlaetze() {
return plaetze - mitfahrende.size();
}
public void belegePlatz(String name) {
mitfahrende.add(name);
}
public List<String> mitfahrende() {
return List.copyOf(mitfahrende);
}
}
// Anfrageverwaltung.java
package de.mitfahrlabor;
import java.util.ArrayList;
import java.util.List;
public class Anfrageverwaltung {
public static final int AUSGEBUCHT = -1;
public static final int EINWILLIGUNG_FEHLT = -2;
private final List<String> nachrichten = new ArrayList<>();
public List<String> nachrichten() {
return nachrichten;
}
/** Bestätigt eine Anfrage; Rückgabe: Kostenbeitrag in Cent oder ein negativer Fehlercode. */
public int bestaetige(Fahrt fahrt, String name, boolean minderjaehrig, boolean einwilligung, int km, boolean perPush) {
if (fahrt != null) {
if (fahrt.freiePlaetze() > 0) {
if (!minderjaehrig || einwilligung) {
int beitrag = km * fahrt.beitragJeKmCent();
fahrt.belegePlatz(name);
String betrag = (beitrag / 100) + "," + String.format("%02d", beitrag % 100) + " EUR";
if (perPush) {
nachrichten.add("Push an " + name + ": Mitfahrt bei " + fahrt.fahrer() + " bestätigt, Kostenbeitrag " + betrag);
} else {
nachrichten.add("Mail an " + name + ": Deine Mitfahrt bei " + fahrt.fahrer() + " ist bestätigt. Kostenbeitrag: " + betrag);
}
return beitrag;
} else {
return EINWILLIGUNG_FEHLT;
}
} else {
return AUSGEBUCHT;
}
}
return 0;
}
}a) Nenne vier Schwachstellen der Methode bestaetige im Hinblick auf Fail fast, Verschachtelung und Parameterliste. Gib bei zwei davon an, zu welchem falschen Ergebnis sie führen können. (4 Punkte)
Je Schwachstelle 0,5 Punkte (höchstens 2), je richtig beschriebener Folge 1 Punkt (höchstens 2). Mögliche Schwachstellen:
- Fehlercodes statt Ausnahmen:
-1und-2stehen im selben Rückgabewert wie der Kostenbeitrag. Folge: Wer den Rückgabewert ungeprüft anzeigt, zeigt „-0,01 EUR“ bzw. rechnet mit einem negativen Beitrag weiter. 0bei fehlender Fahrt: Istfahrtnullbzw.None, liefert die Methode0, also einen gültigen Kostenbeitrag. Folge: Die Oberfläche meldet eine kostenlose Mitfahrt, obwohl nichts gebucht wurde (wie bei Lukas).- Strecke wird nicht geprüft: Eine Strecke von 0 oder weniger ergibt einen Beitrag von 0 oder einen negativen Beitrag, der wie ein Fehlercode aussieht; der Platz ist trotzdem belegt.
- Tiefe Verschachtelung: drei Ebenen
if-else, die Fehlerfälle stehen weit unten, weit weg von ihrer Bedingung. - Flag-Parameter
perPush: Die Methode tut zwei Dinge; am Aufrufbestaetige(fahrt, "Aylin Kaya", true, true, 19, true)ist nicht zu erkennen, welchestruewas bedeutet. - Sechs Parameter, davon drei
booleannebeneinander, die leicht vertauscht werden. - Mehrere Aufgaben in einer Methode: prüfen, Platz belegen, Beitrag berechnen, Text formatieren, Nachricht verschicken.
b) Baue die Klasse Anfrageverwaltung um: Fasse die Angaben zur Person in einem Parameterobjekt Mitfahrende zusammen, ersetze den Flag-Parameter durch zwei öffentliche Methoden bestaetigePerPush und bestaetigePerMail, prüfe alle Eingaben mit Guard Clauses, bevor ein Platz belegt wird, und melde Fehler mit Ausnahmen statt mit Fehlercodes. Die Methoden geben den Kostenbeitrag in Cent zurück. (9 Punkte)
Fahrt bleibt unverändert. Fachliche Ablehnungen (ausgebucht, Einwilligung fehlt) melden eine eigene Ausnahme, damit die Oberfläche sie von Programmierfehlern unterscheiden kann; ungültige Eingaben melden IllegalArgumentException bzw. ValueError, eine fehlende Fahrt in Java NullPointerException (in Python schließt die Typannotation Fahrt ein None aus, mypy meldet einen solchen Aufruf).
// Mitfahrende.java
package de.mitfahrlabor;
/** Parameterobjekt: wer mitfahren möchte und ob eine nötige Einwilligung vorliegt. */
public record Mitfahrende(String name, boolean minderjaehrig, boolean einwilligungLiegtVor) {
}
// BuchungAbgelehntException.java
package de.mitfahrlabor;
/** Die Anfrage darf fachlich nicht bestätigt werden (ausgebucht, Einwilligung fehlt). */
public class BuchungAbgelehntException extends RuntimeException {
public BuchungAbgelehntException(String grund) {
super(grund);
}
}
// Anfrageverwaltung.java
package de.mitfahrlabor;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class Anfrageverwaltung {
private final List<String> nachrichten = new ArrayList<>();
public List<String> nachrichten() {
return nachrichten;
}
/** Bestätigt eine Anfrage, belegt den Platz und schickt eine Push-Nachricht; Rückgabe: Kostenbeitrag in Cent. */
public int bestaetigePerPush(Fahrt fahrt, Mitfahrende person, int km) {
int beitrag = bucheEin(fahrt, person, km);
nachrichten.add(pushtext(fahrt, person, beitrag));
return beitrag;
}
/** Bestätigt eine Anfrage, belegt den Platz und schickt eine Mail; Rückgabe: Kostenbeitrag in Cent. */
public int bestaetigePerMail(Fahrt fahrt, Mitfahrende person, int km) {
int beitrag = bucheEin(fahrt, person, km);
nachrichten.add(mailtext(fahrt, person, beitrag));
return beitrag;
}
private static int bucheEin(Fahrt fahrt, Mitfahrende person, int km) {
pruefe(fahrt, person, km);
fahrt.belegePlatz(person.name());
return kostenbeitrag(fahrt, km);
}
private static void pruefe(Fahrt fahrt, Mitfahrende person, int km) {
Objects.requireNonNull(fahrt, "fahrt");
Objects.requireNonNull(person, "person");
if (km <= 0) {
throw new IllegalArgumentException("Strecke muss positiv sein: " + km);
}
if (fahrt.freiePlaetze() == 0) {
throw new BuchungAbgelehntException("Die Fahrt ist ausgebucht");
}
if (person.minderjaehrig() && !person.einwilligungLiegtVor()) {
throw new BuchungAbgelehntException("Einwilligung der Erziehungsberechtigten fehlt");
}
}
private static int kostenbeitrag(Fahrt fahrt, int km) {
return km * fahrt.beitragJeKmCent();
}
private static String pushtext(Fahrt fahrt, Mitfahrende person, int beitrag) {
return "Push an " + person.name() + ": Mitfahrt bei " + fahrt.fahrer() + " bestätigt, Kostenbeitrag " + alsEuroText(beitrag);
}
private static String mailtext(Fahrt fahrt, Mitfahrende person, int beitrag) {
return "Mail an " + person.name() + ": Deine Mitfahrt bei " + fahrt.fahrer() + " ist bestätigt. Kostenbeitrag: "
+ alsEuroText(beitrag);
}
private static String alsEuroText(int betragCent) {
return (betragCent / 100) + "," + String.format("%02d", betragCent % 100) + " EUR";
}
}Bewertung: Parameterobjekt (1 Punkt), zwei Methoden statt Flag (2 Punkte), Guard Clauses für Strecke, freie Plätze und Einwilligung vor dem Belegen (3 Punkte), Ausnahmen statt Fehlercodes, keine 0 bei fehlender Fahrt (2 Punkte), gemeinsamer Ablauf nicht doppelt geschrieben (1 Punkt). Eine Prüfung, dass der Beitrag je km höchstens 10 Cent beträgt, gehört eher in den Konstruktor von Fahrt (dort, wo der Wert entsteht) und ist eine gute Ergänzung.
c) Bestimme die zyklomatische Komplexität der alten Methode bestaetige und Deiner Prüfmethode aus b). Erläutere, warum die neue Fassung leichter zu lesen ist, obwohl sich die Zahl kaum ändert. (3 Punkte)
bestaetige(alt): vierif(fahrt != null, freie Plätze, Einwilligung,perPush), also M = 4 + 1 = 5; zählt das Werkzeug das||mit, M = 6 (1 Punkt).pruefe(neu): dreiif(Strecke, freie Plätze, Einwilligung), also M = 3 + 1 = 4; mit&&M = 5. Die übrigen Methoden haben M = 1 (1 Punkt).
Die Entscheidungen bleiben fachlich dieselben, also sinkt die Zahl kaum. Die Fehlerfälle stehen aber nicht mehr drei Ebenen tief verschachtelt, sondern nacheinander, jeder direkt neben seiner Bedingung; der Normalfall steht ohne Einrückung, und die Entscheidung Push oder Mail ist in den Methodennamen gewandert (1 Punkt).
d) Schreibe zwei Tests für Ablehnungen: Lina Hartwig (17 Jahre) fragt bei Niklas Fischer an; nimm an, dass für sie noch keine Einwilligung vorliegt, und eine Anfrage trifft auf eine Fahrt ohne freien Platz. Prüfe jeweils auch, dass kein Platz belegt und keine Nachricht verschickt wurde. (4 Punkte)
Auszug aus den Tests der Beispiellösung (alle fünf Tests grün):
// AnfrageverwaltungTest.java
package de.mitfahrlabor;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import org.junit.jupiter.api.Test;
class AnfrageverwaltungTest {
private final Anfrageverwaltung verwaltung = new Anfrageverwaltung();
@Test
void bestaetigtPerPush() {
Fahrt fahrt = new Fahrt("Ben Richter", 3, 8);
assertEquals(152, verwaltung.bestaetigePerPush(fahrt, new Mitfahrende("Aylin Kaya", true, true), 19));
assertEquals(List.of("Push an Aylin Kaya: Mitfahrt bei Ben Richter bestätigt, Kostenbeitrag 1,52 EUR"), verwaltung.nachrichten());
assertEquals(2, fahrt.freiePlaetze());
}
@Test
void bestaetigtPerMail() {
assertEquals(152, verwaltung.bestaetigePerMail(new Fahrt("Ben Richter", 3, 8), new Mitfahrende("Oksana Melnyk", false, false), 19));
assertEquals(List.of("Mail an Oksana Melnyk: Deine Mitfahrt bei Ben Richter ist bestätigt. Kostenbeitrag: 1,52 EUR"), verwaltung.nachrichten());
}
@Test
void einwilligungFehlt() {
Fahrt fahrt = new Fahrt("Niklas Fischer", 1, 10);
assertThrows(BuchungAbgelehntException.class,
() -> verwaltung.bestaetigePerPush(fahrt, new Mitfahrende("Lina Hartwig", true, false), 19));
assertEquals(1, fahrt.freiePlaetze());
assertTrue(verwaltung.nachrichten().isEmpty());
}
@Test
void ausgebucht() {
Fahrt fahrt = new Fahrt("Niklas Fischer", 1, 10);
assertEquals(190, verwaltung.bestaetigePerPush(fahrt, new Mitfahrende("Lukas Hofmann", false, false), 19));
assertThrows(BuchungAbgelehntException.class,
() -> verwaltung.bestaetigePerPush(fahrt, new Mitfahrende("Oksana Melnyk", false, false), 19));
assertEquals(1, verwaltung.nachrichten().size());
}
@Test
void ungueltigeEingaben() {
Fahrt fahrt = new Fahrt("Ben Richter", 3, 8);
Mitfahrende lukas = new Mitfahrende("Lukas Hofmann", false, false);
assertThrows(IllegalArgumentException.class, () -> verwaltung.bestaetigePerPush(fahrt, lukas, -1));
assertThrows(NullPointerException.class, () -> verwaltung.bestaetigePerPush(null, lukas, 19));
assertEquals(3, fahrt.freiePlaetze());
}
}Bewertung: je Test 1 Punkt für die erwartete Ausnahme, 1 Punkt für die Prüfung von Platz und Nachrichten. Hätte die alte Fassung einen Platz belegt, bevor sie abgelehnt hat, würde genau diese zweite Prüfung es zeigen.
Aufgabe 3.1: Den Erinnerungsdienst nach dem Entwurf gliedern (Profilkompetenz)
(ca. 20 min Bearbeitungszeit)
Das Mitfahrlabor erinnert vor gebuchten Fahrten per Mail an die Abfahrt. Oksana Melnyk hat den Ablauf im Projektteam als UML-Aktivitätsdiagramm entworfen:
Umgesetzt wurde der Ablauf in einer einzigen Methode. Die Klassen Anfrage und Fahrt sind vorgegeben:
// Anfrage.java
package de.mitfahrlabor;
/** Anfrage zu einer Fahrt; status ist "OFFEN", "BESTAETIGT" oder "ABGELEHNT". */
public record Anfrage(String name, String status) {
}
// Fahrt.java
package de.mitfahrlabor;
import java.time.LocalDateTime;
import java.util.List;
public class Fahrt {
private final String fahrer;
private final String haltepunkt;
private final LocalDateTime abfahrt;
private final List<Anfrage> anfragen;
private boolean erinnert = false;
public Fahrt(String fahrer, String haltepunkt, LocalDateTime abfahrt, List<Anfrage> anfragen) {
this.fahrer = fahrer;
this.haltepunkt = haltepunkt;
this.abfahrt = abfahrt;
this.anfragen = List.copyOf(anfragen);
}
public String fahrer() {
return fahrer;
}
public String haltepunkt() {
return haltepunkt;
}
public LocalDateTime abfahrt() {
return abfahrt;
}
public List<Anfrage> anfragen() {
return anfragen;
}
public boolean istErinnert() {
return erinnert;
}
public void markiereErinnert() {
erinnert = true;
}
}
// Erinnerungsdienst.java
package de.mitfahrlabor;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
public class Erinnerungsdienst {
private final List<String> postausgang = new ArrayList<>();
public List<String> postausgang() {
return postausgang;
}
public void sendeErinnerungen(List<Fahrt> fahrten, LocalDateTime jetzt) {
for (Fahrt fahrt : fahrten) {
if (!fahrt.istErinnert() && fahrt.abfahrt().isAfter(jetzt) && fahrt.abfahrt().isBefore(jetzt.plusHours(12))) {
fahrt.markiereErinnert();
String uhrzeit = String.format("%02d:%02d", fahrt.abfahrt().getHour(), fahrt.abfahrt().getMinute());
for (Anfrage anfrage : fahrt.anfragen()) {
if (!anfrage.status().equals("ABGELEHNT")) {
postausgang.add("An: " + anfrage.name() + "\nErinnerung: Abfahrt um " + uhrzeit + " ab " + fahrt.haltepunkt());
}
}
}
}
}
}Ben Richter fährt am 17.11.2026 um 07:05 Uhr ab „Hauptbahnhof, ZOB Steig 3“ in Gütersloh. Aylin Kayas Anfrage ist bestätigt, Oksana Melnyks Anfrage ist noch offen, Lukas Hofmanns Anfrage wurde abgelehnt. Der Dienst läuft am 16.11.2026 um 20:00 Uhr.
a) Vergleiche den Code mit dem Aktivitätsdiagramm: Ordne jeder Aktion des Diagramms die Codestelle zu, die sie umsetzt, und nenne zwei Stellen, an denen der Code vom Entwurf abweicht. Gib an, welche Mails im Beispiel verschickt werden. (5 Punkte)
| Aktion im Diagramm | Codestelle |
|---|---|
| fällige Fahrten auswählen | Bedingung im äußeren if (nicht erinnert, Abfahrt nach jetzt und vor jetzt + 12 Stunden), verteilt auf Schleife und if |
| Empfänger ermitteln | innere Schleife über anfragen mit dem if auf den Status |
| Erinnerungstext erstellen | Variable uhrzeit und die Verkettung im add bzw. append |
| Erinnerung an alle Empfänger senden | postausgang.add(...) in der inneren Schleife |
| Fahrt als erinnert markieren | markiereErinnert() bzw. fahrt.erinnert = True |
Bewertung: Zuordnung 2 Punkte.
Abweichungen (je 1 Punkt, zwei verlangt):
- Der Fahrer wird nicht erinnert; laut Entwurf gehört er zu den Empfängern.
- Erinnert werden alle Anfragen, die nicht abgelehnt sind, also auch offene; laut Entwurf nur bestätigte.
- Die Fahrt wird als erinnert markiert, bevor die Mails verschickt sind; laut Entwurf danach. Bricht der Versand ab, gilt die Fahrt trotzdem als erinnert.
Im Beispiel gehen zwei Mails hinaus: an Aylin Kaya und an Oksana Melnyk, jeweils „Erinnerung: Abfahrt um 07:05 ab Hauptbahnhof, ZOB Steig 3“; Ben Richter bekommt keine (1 Punkt). Die Abfahrt liegt 11 Stunden und 5 Minuten nach dem Lauf, also im Zeitfenster.
b) Gliedere den Erinnerungsdienst so, dass sich die öffentliche Methode wie das Aktivitätsdiagramm liest: Jede Aktion wird eine Methode mit sprechendem Namen, integrierende und operierende Methoden sind getrennt. Behebe dabei die Abweichungen aus a). Gib zusätzlich einen Test an, der das korrigierte Verhalten für das Beispiel prüft. (10 Punkte)
Anfrage und Fahrt bleiben unverändert.
// Erinnerungsdienst.java
package de.mitfahrlabor;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
public class Erinnerungsdienst {
private static final int VORLAUF_STUNDEN = 12;
private final List<String> postausgang = new ArrayList<>();
public List<String> postausgang() {
return postausgang;
}
// ---- Integration: liest sich wie das Aktivitätsdiagramm ----
public void sendeErinnerungen(List<Fahrt> fahrten, LocalDateTime jetzt) {
for (Fahrt fahrt : faelligeFahrten(fahrten, jetzt)) {
erinnere(fahrt);
}
}
private void erinnere(Fahrt fahrt) {
versende(empfaenger(fahrt), erinnerungstext(fahrt));
fahrt.markiereErinnert();
}
// ---- Operationen ----
private static List<Fahrt> faelligeFahrten(List<Fahrt> fahrten, LocalDateTime jetzt) {
LocalDateTime grenze = jetzt.plusHours(VORLAUF_STUNDEN);
return fahrten.stream()
.filter(fahrt -> !fahrt.istErinnert())
.filter(fahrt -> fahrt.abfahrt().isAfter(jetzt) && fahrt.abfahrt().isBefore(grenze))
.toList();
}
private static List<String> empfaenger(Fahrt fahrt) {
List<String> empfaenger = new ArrayList<>();
empfaenger.add(fahrt.fahrer());
for (Anfrage anfrage : fahrt.anfragen()) {
if (anfrage.status().equals("BESTAETIGT")) {
empfaenger.add(anfrage.name());
}
}
return empfaenger;
}
private static String erinnerungstext(Fahrt fahrt) {
String uhrzeit = String.format("%02d:%02d", fahrt.abfahrt().getHour(), fahrt.abfahrt().getMinute());
return "Erinnerung: Abfahrt um " + uhrzeit + " ab " + fahrt.haltepunkt();
}
private void versende(List<String> empfaenger, String text) {
for (String name : empfaenger) {
postausgang.add("An: " + name + "\n" + text);
}
}
}Tests (darunter zwei, die schon für die alte Fassung galten: nur einmal erinnern, spätere Fahrt noch nicht):
// ErinnerungsdienstTest.java
package de.mitfahrlabor;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.LocalDateTime;
import java.util.List;
import org.junit.jupiter.api.Test;
class ErinnerungsdienstTest {
static final LocalDateTime JETZT = LocalDateTime.of(2026, 11, 16, 20, 0);
static final String ZOB = "Hauptbahnhof, ZOB Steig 3";
static Fahrt bensFahrt() {
return new Fahrt("Ben Richter", ZOB, LocalDateTime.of(2026, 11, 17, 7, 5), List.of(
new Anfrage("Aylin Kaya", "BESTAETIGT"),
new Anfrage("Oksana Melnyk", "OFFEN"),
new Anfrage("Lukas Hofmann", "ABGELEHNT")));
}
@Test
void erinnertNurEinmal() {
Erinnerungsdienst dienst = new Erinnerungsdienst();
List<Fahrt> fahrten = List.of(bensFahrt());
dienst.sendeErinnerungen(fahrten, JETZT);
int anzahl = dienst.postausgang().size();
dienst.sendeErinnerungen(fahrten, JETZT.plusHours(1));
assertEquals(anzahl, dienst.postausgang().size());
}
@Test
void spaetereFahrtNochNicht() {
Erinnerungsdienst dienst = new Erinnerungsdienst();
dienst.sendeErinnerungen(List.of(bensFahrt()), JETZT.minusHours(1));
assertEquals(List.of(), dienst.postausgang());
}
@Test
void fahrerUndBestaetigteMitfahrende() {
Erinnerungsdienst dienst = new Erinnerungsdienst();
dienst.sendeErinnerungen(List.of(bensFahrt()), JETZT);
assertEquals(List.of(
"An: Ben Richter\nErinnerung: Abfahrt um 07:05 ab " + ZOB,
"An: Aylin Kaya\nErinnerung: Abfahrt um 07:05 ab " + ZOB), dienst.postausgang());
}
}Bewertung: öffentliche Methode als Integration, die die Aktionen in der Reihenfolge des Diagramms aufruft (2 Punkte), je Aktion eine Methode mit sprechendem Namen (2 Punkte), Operationen ohne Aufrufe eigener Methoden und ohne Zustand static bzw. auf Modulebene (2 Punkte), Abweichungen behoben (Fahrer, nur bestätigte, Markieren nach dem Senden) (2 Punkte), Test mit erwarteten Mails an Ben Richter und Aylin Kaya (2 Punkte).
c) Zeichne das UML-Klassendiagramm der Klassen Erinnerungsdienst, Fahrt und Anfrage nach Deinem Umbau. Kennzeichne die statischen Operationen. (4 Punkte)
Bewertung: Klasse Erinnerungsdienst mit allen Methoden, Sichtbarkeiten und Parametern (2 Punkte), statische Operationen unterstrichen (1 Punkt), Beziehungen mit Multiplizitäten (1 Punkt). In Python entsprechen die statischen Operationen den Funktionen auf Modulebene; im Diagramm werden sie wie in Java als statische Methoden der Klasse dargestellt oder in einer Notiz genannt.
d) Thomas Becker, der Projektleiter, sieht sich die neue Fassung an: „Jetzt brauche ich sieben Methoden, um zu verstehen, was vorher in einer stand. Und streng nach IOSP dürfte sendeErinnerungen nicht einmal eine Schleife enthalten.“ Nimm Stellung zu beiden Einwänden. (3 Punkte)
Sieben Methoden: Wer nur wissen will, was passiert, liest jetzt zwei kurze Methoden, die sich wie das Diagramm lesen; die Einzelheiten muss er nur öffnen, wenn er eine Regel ändert. Die Abweichungen aus a) waren in der langen Methode nicht aufgefallen, in der gegliederten Fassung steht jede Regel unter ihrem Namen. Der Einwand ist berechtigt, wo eine Methode nicht mehr sagt als ihr Inhalt; dann wäre ein Zusammenlegen (Inline) besser (1,5 Punkte).
Schleife in der Integration: Streng genommen ist das richtig. Eine Schleife, die nur für jedes Element eine andere Methode aufruft, enthält aber keine Fachlogik; sie entspricht der Schleife im Aktivitätsdiagramm. Sie in eine eigene Methode zu verschieben oder durch
forEachzu verstecken, würde nichts lesbarer machen. Pragmatisch erlaubt man solche Schleifen in Integrationen (1,5 Punkte).
Links und weitere Informationen
Methoden gliedern: Single Level of Abstraction, IOSP, eine Aufgabe je Methode und Fail fast
Klassen schneiden (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 Gliedern von Methoden: Single Level of Abstraction, IOSP, eine Aufgabe je Methode und Fail fast” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/methoden-gliedern-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]
