Übungsaufgaben zum Refactoring: Code Smells erkennen und in sicheren Schritten umbauen

https://bildung.social/@oerinformatik/

https://oer-informatik.de/refactoring-uebungsaufgaben

Clean Code, Refactoring, Code Smell, Charakterisierungstest, Methode extrahieren, Parameterobjekt, Pfadfinderregel, 5-mal-Warum, Übungsaufgaben, Prüfungsvorbereitung

tl/dr; (Bearbeitungszeit insgesamt 81 min, mehrere Aufgaben): Vier Übungsaufgaben zum Refactoring: Du ergänzt die Charakterisierungstests der CSB-Rad-Kata und zerlegst ihren Bestellabschluss mit „Methode extrahieren“ und einem Parameterobjekt, bringst Refactoring-Schritte für die Flurmonitor-Anzeige des Mitfahrlabors in eine sichere Reihenfolge, ordnest Code Smells passenden Refactorings zu und verfolgst einen Fehler bei den freien Plätzen mit dem 5-mal-Warum bis zur Ursache [AI-Assisted]. (Zuletzt geändert am 10.11.2026)

Diese Übungsaufgaben gehören zum Artikel Refactoring: Code Smells erkennen und in sicheren Schritten umbauen.

Wortwolke zu den Übungsaufgaben: Refactoring, Code Smell, Charakterisierungstest, Methode extrahieren, Parameterobjekt, 5-mal-Warum (mit KI erstellt)

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, Aufgabe 2.2 und Aufgabe 2.3. Die Codeausschnitte stehen in Java und Python; wähle die Sprache, die Du gerade lernst, und schreib Deine Lösung in dieser Sprache. Für die Aufgaben 2.1, 2.2 und 3.1 brauchst Du eine Entwicklungsumgebung mit JUnit 5 bzw. pytest, damit Du die Tests nach jedem Schritt ausführen kannst. Achte auf die Operatoren: „Nenne“ verlangt Stichworte ohne Erklärung, „Ordne zu“ verlangt die Zuordnung mit einem Satz Begründung, „Bringe in eine Reihenfolge“ und „Begründe“ verlangen die Reihenfolge mit Begründung, „Nimm Stellung“ verlangt eine Aussage mit Begründung, „Baue um“ und „Schreibe“ verlangen lauffähigen Code, „Zeichne“ verlangt ein UML-Diagramm. 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 „Refactoring: Code Smells
erkennen und in sicheren Schritten umbauen“.

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/refactoring
- https://oer-informatik.de/einfacher-code
- https://oer-informatik.de/clean-code
- https://oer-informatik.de/methoden-gliedern
- https://oer-informatik.de/unit-tests
- Stilbeispiel: https://oer-informatik.de/refactoring-uebungsaufgaben

Die Aufgaben sollen Inhalte zu diesen Schlagwörtern abdecken:
- Clean Code
- Refactoring
- Code Smell
- Charakterisierungstest
- Methode extrahieren
- Parameterobjekt
- Pfadfinderregel
- 5-mal-Warum

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: Sicherheitsnetz für den Bestellabschluss (Kernkompetenz)

(ca. 32 min Bearbeitungszeit)

Im Onlineshop der CSB-Rad GmbH schließt die Klasse Shop Bestellungen ab. Namen, Konstanten und Kommentare sind aufgeräumt, doppelter Code und Ballast sind entfernt (Stand 03 der Code-Kata dieser Artikelreihe). Die Methode abschliessen ist aber noch rund 70 Zeilen lang, und Du sollst sie zerlegen. Für die Klasse gibt es bisher nur drei Charakterisierungstests. Den Code findest Du auch im Ordner src/kata/java/stand-03 bzw. src/kata/python/stand-03 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) {
}

// 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;
    }

    private int vergibBestellnummer() {
        int bestellnummer = naechsteBestellnummer;
        naechsteBestellnummer = naechsteBestellnummer + 1;
        return bestellnummer;
    }

    /**
     * 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(List<Position> positionen, String versandart, String gutscheincode, String kundenEmail) {
        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();
        }
        int rabatt = 0;
        if (gutscheincode != null && gutscheincode.equals(GUTSCHEINCODE_KURIER)) {
            // kaufmännisch gerundet
            rabatt = (warenwert * GUTSCHEIN_RABATT_PROZENT + 50) / 100;
        }
        int versandkosten = 0;
        String versandbezeichnung;
        if (versandart.equals("PAKET")) {
            // Räder passen in kein Paket; die Freigrenze gilt für den Warenwert nach Rabatt
            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;
            if (gesamtgewicht % HOECHSTGEWICHT_PAKET_GRAMM > 0 || anzahlPakete == 0) {
                anzahlPakete = anzahlPakete + 1;
            }
            if (warenwert - rabatt >= VERSANDKOSTENFREI_AB_CENT) {
                versandkosten = 0;
            } else {
                versandkosten = anzahlPakete * PAKETPAUSCHALE_CENT;
            }
            versandbezeichnung = "Paket";
        } else if (versandart.equals("SPEDITION")) {
            int anzahlRaeder = 0;
            for (Position position : positionen) {
                if (position.istRad()) {
                    anzahlRaeder = anzahlRaeder + position.menge();
                }
            }
            // ohne Rad wird eine Speditionspauschale berechnet
            if (anzahlRaeder == 0) {
                anzahlRaeder = 1;
            }
            versandkosten = anzahlRaeder * SPEDITION_JE_RAD_CENT;
            versandbezeichnung = "Spedition";
        } else if (versandart.equals("ABHOLUNG")) {
            versandkosten = 0;
            versandbezeichnung = "Abholung";
        } else {
            throw new IllegalArgumentException("Unbekannte Versandart: " + versandart);
        }
        if (warenwert == 0) {
            throw new IllegalArgumentException("Bestellung ohne Warenwert");
        }
        // Preise sind Nettopreise; die Mehrwertsteuer kommt auf Waren und Versand
        int nettobetrag = warenwert - rabatt + versandkosten;
        int mehrwertsteuer = (nettobetrag * MEHRWERTSTEUER_PROZENT + 50) / 100;
        int gesamtbetrag = nettobetrag + mehrwertsteuer;
        int bestellnummer = vergibBestellnummer();
        String mailtext = "An: " + kundenEmail + "\nBestellung CSB-" + bestellnummer + "\nGesamtbetrag: " + (gesamtbetrag / 100) + ","
                + String.format("%02d", gesamtbetrag % 100) + " EUR";
        if (versandart.equals("ABHOLUNG")) {
            mailtext = mailtext + "\nAbholung in der Werkstatt Bielefeld-Sennestadt";
        } else {
            mailtext = mailtext + "\nVersand: " + versandbezeichnung;
        }
        postausgang.add(mailtext);
        bestellungen.add(new Object[] {"CSB-" + bestellnummer, kundenEmail, positionen, gesamtbetrag});
        return new int[] {warenwert, rabatt, versandkosten, mehrwertsteuer, gesamtbetrag};
    }
}

// ShopCharakterisierungTest.java (src/test/java/de/csbrad/shop)
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 (Stand 03, erste Auswahl).
 * 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(List.of(LICHTSET), "PAKET", null, MAIL));
    }

    @Test
    void paketAbFreigrenzeIstVersandfrei() {
        assertArrayEquals(new int[] {13361, 0, 0, 2539, 15900}, shop.abschliessen(List.of(BREMSE), "PAKET", null, MAIL));
    }

    @Test
    void mailFuerPaketversand() {
        shop.abschliessen(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));
    }
}

a) Nenne vier Verhaltensweisen von abschliessen, die die drei vorhandenen Tests nicht absichern. (4 Punkte)

Die drei Tests decken nur den Paketversand ohne Gutschein und die Mail für den Paketversand ab. Ungesichert sind unter anderem:

  • Versandkosten der Spedition je Rad,

  • die Speditionspauschale ohne Rad,

  • die Abholung (kostenlos, eigene Mailzeile),

  • der Rabatt mit KURIER10 und die Freigrenze nach Rabatt,

  • ein unbekannter Gutscheincode (wird ignoriert),

  • die Ablehnungen: Rad im Paket, leere Bestellung, Menge 0, unbekannte Versandart; jeweils ohne Mail und ohne gespeicherte Bestellung,

  • mehr als ein Paket bei über 31,5 kg (nur ohne Freigrenze sichtbar),

  • die fortlaufende Bestellnummer und das Speichern.

Bewertung: je zutreffender Verhaltensweise 1 Punkt, höchstens 4 Punkte.

b) Schreibe für mindestens vier der Verhaltensweisen aus a) Charakterisierungstests, darunter mindestens eine Ablehnung. Ermittle die erwarteten Werte, indem Du den Code mit einer bewusst falschen Erwartung ausführst und das tatsächliche Ergebnis übernimmst. (6 Punkte)

Die Tests ergänzen die vorhandene Testklasse; Konstanten, Hilfsmethode menge bzw. replace und die Fixture stehen dort schon. Die Werte stammen aus dem Testlauf, zum Beispiel meldet assertEquals(0, ergebnis[2]) für die Spedition ohne Rad expected: <0> but was: <4900>.

@Test
void speditionJeRad() {
    assertArrayEquals(new int[] {149412, 0, 9800, 30250, 189462},
            shop.abschliessen(List.of(menge(STADTRAD, 2), menge(LICHTSET, 2)), "SPEDITION", null, MAIL));
}

@Test
void speditionOhneRadKostetEinePauschale() {
    assertArrayEquals(new int[] {7563, 0, 4900, 2368, 14831}, shop.abschliessen(List.of(LICHTSET), "SPEDITION", null, MAIL));
}

@Test
void abholungMitRabattcode() {
    assertArrayEquals(new int[] {252017, 25202, 0, 43095, 269910}, shop.abschliessen(List.of(EBIKE), "ABHOLUNG", "KURIER10", MAIL));
}

@Test
void radAlsPaketWirdAbgelehnt() {
    assertThrows(IllegalArgumentException.class, () -> shop.abschliessen(List.of(STADTRAD), "PAKET", null, MAIL));
    assertEquals(0, shop.getPostausgang().size());
    assertEquals(0, shop.getBestellungen().size());
}

@Test
void unbekannteVersandartWirdAbgelehnt() {
    assertThrows(IllegalArgumentException.class, () -> shop.abschliessen(List.of(LICHTSET), "BRIEF", null, MAIL));
}

Alle acht Tests sind gegen Stand 03 grün. Die vollständige Sammlung mit 16 Tests liegt in der Kata im Ordner tests-charakterisierung.

Bewertung: je Test mit sinnvoller Eingabe und richtigem Erwartungswert 1 Punkt (höchstens 4), Ablehnung prüft zusätzlich, dass keine Mail verschickt wurde, 1 Punkt, Tests laufen grün 1 Punkt.

c) Baue die Methode abschliessen in kleinen Schritten um: Extrahiere Methoden für Warenwert, Rabatt, Versandkosten (mit Anzahl der Pakete und Zahl der Räder), Mehrwertsteuer und Mailtext, und führe anschließend ein Parameterobjekt Bestellanfrage für die vier Parameter ein. Führe nach jedem Schritt die Tests aus. (10 Punkte)

Die Lösung entspricht Stand 04 der Kata. Die Prüfungen (Menge, Rad im Paket, Warenwert 0) bleiben bewusst an ihrer Stelle in der Reihenfolge von Stand 03, damit sich das Verhalten nicht ändert, auch nicht die Art der Ausnahme bei mehreren Fehlern.

// 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";
    }
}

// in den Tests ändert sich nur der Aufruf, zum Beispiel:
assertArrayEquals(new int[] {7563, 0, 690, 1568, 9821}, shop.abschliessen(new Bestellanfrage(List.of(LICHTSET), "PAKET", null, MAIL)));

Mit allen 16 Charakterisierungstests der Kata geprüft: mvn -q test bzw. python -m pytest, 16 von 16 grün.

Bewertung: je extrahierter Methode mit passendem Namen und richtigen Parametern 1 Punkt (Warenwert, Rabatt, Versandkosten, Pakete/Räder, Mehrwertsteuer, Mailtext: 6 Punkte), Parameterobjekt als unveränderlicher Typ (record bzw. frozen-Dataclass) 2 Punkte, Aufrufe in den Tests umgestellt 1 Punkt, Verhalten unverändert (Tests grün, Prüfungen nicht verloren) 1 Punkt. Eine andere sinnvolle Aufteilung (zum Beispiel ohne gemeinsame Rundungsmethode) ist gleichwertig.

d) Gib die Commit-Nachrichten Deiner Schritte aus c) in der Reihenfolge an. Begründe, warum Du das Parameterobjekt erst zum Schluss einführst. (3 Punkte)

  1. „Charakterisierungstests ergänzt und in den Testordner übernommen“

  2. „berechneWarenwert extrahiert“

  3. „prozentKaufmaennischGerundet extrahiert, berechneRabatt und berechneMehrwertsteuer nutzen es“

  4. „berechneAnzahlPakete und zaehleRaeder extrahiert“

  5. „berechneVersandkosten und versandbezeichnung extrahiert“

  6. „erstelleMailtext und alsEuroText extrahiert“

  7. „Parameterobjekt Bestellanfrage eingeführt, Tests auf neuen Aufruf umgestellt“

Das Parameterobjekt ändert die öffentliche Signatur und damit alle Aufrufer, auch die Tests. Kommt es zum Schluss, bleiben die Tests während aller Extraktionen unverändert und sichern sie ab; erst im letzten Schritt ändern sich Code und Tests gemeinsam, und dieser Schritt ist rein mechanisch. Außerdem siehst Du nach den Extraktionen, welche Methoden welche Werte wirklich brauchen.

Bewertung: sinnvolle Schrittfolge mit kurzen, beschreibenden Nachrichten 1 Punkt, Begründung (Signaturänderung betrifft Tests, Tests sollen während der Extraktion unverändert bleiben) 2 Punkte.

Aufgabe 2.2: Die Anzeige für die Flurmonitore umbauen (Kernkompetenz)

(ca. 14 min Bearbeitungszeit)

Die Flurmonitore der Mitfahr-App Mitfahrlabor am DBH-Berufskolleg zeigen offene Fahrtangebote anonym an: Ort, Haltepunkt, Richtung, Abfahrt und freie Plätze. Haltepunktnamen mit mehr als 20 Zeichen werden gekürzt, damit die Zeile auf den Bildschirm passt. Oksana Melnyk soll die Klasse Monitoranzeige aufräumen, bevor Svenja Krämer von der Schul-IT OWL eine zweite Bildschirmgröße einführt. Tests gibt es für die Klasse noch nicht.

public class Monitoranzeige {

    // baut eine Zeile für den Flurmonitor
    public String zeile(String o, String h, String r, int std, int min, int p) {
        String t = (std < 10 ? "0" + std : "" + std) + ":" + (min < 10 ? "0" + min : "" + min);
        String hp = h;
        if (h.length() > 20) {
            hp = h.substring(0, 17) + "...";
        }
        String pl;
        if (p == 0) {
            pl = "ausgebucht";
        } else if (p == 1) {
            pl = "1 Platz frei";
        } else {
            pl = p + " Plätze frei";
        }
        return o + " | " + hp + " | " + r + " | " + t + " | " + pl;
    }
}

Oksana hat sich die folgenden Schritte notiert, allerdings ungeordnet:

  • A Ein Parameterobjekt Monitorangebot einführen und den Aufruf umstellen.

  • B Die magische Zahl 20 und die Auslassungspunkte durch Konstanten ersetzen.

  • C Charakterisierungstests für zeile schreiben.

  • D Die Abschnitte für Uhrzeit, Haltepunkt und Platztext als Methoden alsUhrzeit, kuerzeHaltepunkt und platzText extrahieren.

  • E Parameter und lokale Variablen umbenennen.

  • F Den Kommentar über der Methode entfernen.

a) Bringe die Schritte A bis F in eine sinnvolle Reihenfolge und begründe Deine Reihenfolge. (5 Punkte)

Sinnvoll ist C – E – B – D – F – A:

  • C zuerst: Ohne Tests ist jeder Umbau ungesichert. Die Tests decken die Zweige ab: kurzer und langer Haltepunkt, 0, 1 und mehrere freie Plätze, einstellige Stunde und Minute.

  • E vor den Extraktionen: Mit sprechenden Namen lässt sich erkennen, welche Blöcke zusammengehören, und die extrahierten Methoden bekommen gleich gute Parameternamen.

  • B vor D: In kuerzeHaltepunkt soll schon die Konstante stehen. Dabei fällt auf, dass die 17 keine eigene Zahl ist, sondern 20 minus die Länge der Auslassungspunkte.

  • F nach D: Erst wenn Methoden- und Hilfsmethodennamen sagen, was passiert, ist der Kommentar überflüssig.

  • A zuletzt: Das Parameterobjekt ändert die Signatur und damit die Tests; erst wenn alles andere grün umgebaut ist, wird der Aufruf umgestellt.

Andere Reihenfolgen sind zulässig, wenn C am Anfang steht und die Begründung schlüssig ist (zum Beispiel F direkt nach E oder B nach D mit anschließender Anpassung der Methode).

Bewertung: C zuerst mit Begründung 2 Punkte, A zuletzt mit Begründung 1 Punkt, Reihenfolge von E, B, D, F schlüssig begründet 2 Punkte.

b) Führe die Schritte in Deiner Reihenfolge durch. Gib die Charakterisierungstests und die umgebaute Klasse an. (8 Punkte)

Die Charakterisierungstests (Schritt C) gegen die alte Fassung, Werte aus dem Testlauf übernommen:

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

class MonitoranzeigeTest {

    private final Monitoranzeige anzeige = new Monitoranzeige();

    @Test
    void kurzerHaltepunktMehrerePlaetze() {
        assertEquals("Bad Salzuflen | Busbahnhof | zum DBH | 07:05 | 3 Plätze frei",
                anzeige.zeile("Bad Salzuflen", "Busbahnhof", "zum DBH", 7, 5, 3));
    }

    @Test
    void langerHaltepunktWirdGekuerzt() {
        assertEquals("Gütersloh | Hauptbahnhof, ZOB... | zum DBH | 06:50 | 1 Platz frei",
                anzeige.zeile("Gütersloh", "Hauptbahnhof, ZOB Steig 3", "zum DBH", 6, 50, 1));
    }

    @Test
    void zweiterLangerHaltepunkt() {
        assertEquals("Herford | Park & Ride am Ba... | zum DBH | 07:00 | 2 Plätze frei",
                anzeige.zeile("Herford", "Park & Ride am Bahnhof", "zum DBH", 7, 0, 2));
    }

    @Test
    void ohneFreiePlaetzeAusgebucht() {
        assertEquals("Bielefeld | Bahnhof Brackwede | nach Hause | 15:30 | ausgebucht",
                anzeige.zeile("Bielefeld", "Bahnhof Brackwede", "nach Hause", 15, 30, 0));
    }
}

Die umgebaute Klasse mit Parameterobjekt (Schritte E, B, D, F, A). Die Uhrzeit nutzt die Formatierung der Standardbibliothek statt der zwei Bedingungsoperatoren (Algorithmus ersetzen); die Tests zeigen, dass das Ergebnis gleich bleibt:

// Monitorangebot.java
/** Parameterobjekt: alles, was eine Zeile auf dem Flurmonitor zeigt. */
public record Monitorangebot(String ort, String haltepunkt, String richtung, int stunde, int minute, int freiePlaetze) {
}

// Monitoranzeige.java
public class Monitoranzeige {

    private static final int HOECHSTLAENGE_HALTEPUNKT = 20;
    private static final String AUSLASSUNG = "...";

    public String zeile(Monitorangebot angebot) {
        return angebot.ort() + " | " + kuerzeHaltepunkt(angebot.haltepunkt()) + " | " + angebot.richtung() + " | "
                + alsUhrzeit(angebot.stunde(), angebot.minute()) + " | " + platzText(angebot.freiePlaetze());
    }

    private static String alsUhrzeit(int stunde, int minute) {
        return String.format("%02d:%02d", stunde, minute);
    }

    private static String kuerzeHaltepunkt(String haltepunkt) {
        if (haltepunkt.length() <= HOECHSTLAENGE_HALTEPUNKT) {
            return haltepunkt;
        }
        return haltepunkt.substring(0, HOECHSTLAENGE_HALTEPUNKT - AUSLASSUNG.length()) + AUSLASSUNG;
    }

    private static String platzText(int freiePlaetze) {
        if (freiePlaetze == 0) {
            return "ausgebucht";
        }
        if (freiePlaetze == 1) {
            return "1 Platz frei";
        }
        return freiePlaetze + " Plätze frei";
    }
}

// Aufruf in den Tests, zum Beispiel:
anzeige.zeile(new Monitorangebot("Bad Salzuflen", "Busbahnhof", "zum DBH", 7, 5, 3))

Alte und neue Fassung mit denselben vier Tests geprüft: jeweils 4 von 4 grün.

Bewertung: Charakterisierungstests decken kurzen und langen Haltepunkt sowie 0, 1 und mehrere Plätze ab 2 Punkte, sprechende Namen 1 Punkt, Konstanten (Länge aus Höchstlänge und Auslassung abgeleitet) 1 Punkt, drei extrahierte Methoden 2 Punkte, Parameterobjekt und umgestellte Tests 1 Punkt, Tests grün 1 Punkt.

c) Beschreibe, wann Du während des Umbaus committest und wie Du vorgehst, wenn nach Schritt D ein Test rot wird. (2 Punkte)

Nach jedem Schritt, sobald alle Tests grün sind, kommt ein Commit mit einer Nachricht, die den Schritt beschreibt („Methoden für Uhrzeit, Haltepunkt und Platztext extrahiert“). Wird nach Schritt D ein Test rot, wird der Schritt mit git restore . auf den letzten grünen Commit zurückgesetzt und in kleineren Teilen wiederholt, etwa erst nur platzText extrahieren. Gesucht wird nicht lange im halb umgebauten Code.

Bewertung: Commit nach jedem grünen Schritt 1 Punkt, Zurücksetzen auf den letzten grünen Stand und kleiner wiederholen 1 Punkt.

Aufgabe 2.3: Smells in der Gesuchsverwaltung zuordnen (Kernkompetenz)

(ca. 15 min Bearbeitungszeit)

Im Mitfahrlabor können Auszubildende ohne Auto ein Mitfahrgesuch einstellen: Name, Ort, Richtung, gewünschte Abfahrt und Zahl der gesuchten Plätze. Minderjährige dürfen nur mit Einwilligung der Erziehungsberechtigten teilnehmen. Jonas Wolff hat die Klasse Gesuchsverwaltung geschrieben; alle Tests sind grün. Im Code-Review markiert Oksana Melnyk fünf Stellen (A) bis (E).

import java.util.ArrayList;
import java.util.List;

public class Gesuchsverwaltung {

    private final List<String[]> gesuche = new ArrayList<>();

    // (A)
    public String erstelleGesuch(String name, String ort, String richtung, int stunde, int minute, int plaetze,
            boolean minderjaehrig, boolean einwilligung) {
        // (B)
        if (stunde < 0 || stunde > 23 || minute < 0 || minute > 59) {
            return "Ungültige Uhrzeit";
        }
        // (C)
        if (plaetze < 1 || plaetze > 4) {
            return "Ungültige Platzzahl";
        }
        // (D) prüfen, ob die Person mitfahren darf: wenn minderjährig, dann nur mit Einwilligung
        if (minderjaehrig && !einwilligung) {
            return "Einwilligung fehlt";
        }
        // (E)
        gesuche.add(new String[] {name, ort, richtung, stunde + ":" + minute, String.valueOf(plaetze)});
        return "OK";
    }

    public String verschiebeGesuch(int nummer, int stunde, int minute) {
        if (stunde < 0 || stunde > 23 || minute < 0 || minute > 59) {
            return "Ungültige Uhrzeit";
        }
        gesuche.get(nummer)[3] = stunde + ":" + minute;
        return "OK";
    }

    public String ortVonGesuch(int nummer) {
        return gesuche.get(nummer)[1];
    }
}

a) Ordne jeder der Stellen (A) bis (E) einen Code Smell zu und nenne ein Refactoring, das ihn beseitigt. Begründe die Zuordnung jeweils in einem Satz. (10 Punkte)

Code Smells und Refactorings in der Gesuchsverwaltung
Stelle Code Smell Refactoring Begründung
(A) lange Parameterliste (Long Parameter List), Datenklumpen (Data Clumps) Parameterobjekt einführen (Introduce Parameter Object), z. B. Gesuch oder Abfahrtszeit für Stunde und Minute Acht Parameter, darunter drei String und zwei boolean hintereinander, lassen sich leicht vertauschen; Stunde und Minute treten auch in verschiebeGesuch gemeinsam auf.
(B) doppelter Code (Duplicated Code) Methode extrahieren (Extract Function), z. B. istGueltigeUhrzeit(stunde, minute) Dieselbe Prüfung steht in erstelleGesuch und verschiebeGesuch; eine Änderung (etwa nur Abfahrten von 5 bis 9 Uhr) müsste an zwei Stellen erfolgen.
(C) magische Zahl (Magic Number) magische Zahl durch Konstante ersetzen (Replace Magic Literal), z. B. HOECHSTZAHL_GESUCHTE_PLAETZE Warum höchstens 4 Plätze gesucht werden dürfen, steht nirgends.
(D) erklärender Kommentar (Comments) Methode extrahieren, z. B. darfTeilnehmen(minderjaehrig, einwilligung) Der Kommentar erklärt, was die Bedingung bedeutet; ein Methodenname kann das selbst sagen.
(E) Basistyp-Besessenheit (Primitive Obsession) Basistyp durch Objekt ersetzen (Replace Primitive with Object, früher Replace Array with Object), z. B. record Gesuch bzw. Dataclass Ein Gesuch wird als String-Array gespeichert, auf die Felder wird über Indizes wie [3] zugegriffen, die Platzzahl wird zur Zeichenkette.

Bewertung: je Stelle Smell 1 Punkt, passendes Refactoring mit Begründung 1 Punkt. Gleichwertige Zuordnungen werden anerkannt, etwa bei (A) „Ganzes Objekt übergeben“ oder bei (B) „Klasse Abfahrtszeit extrahieren“.

b) Wähle zwei Deiner Refactorings aus a), die eine IDE automatisch ausführen kann. Beschreibe, worauf Du achten musst, wenn Du sie in der Python-Fassung mit der IDE ausführst. (3 Punkte)

Automatisch gehen zum Beispiel „Methode extrahieren“ für (B) und (D) sowie „Konstante extrahieren“ für (C), in IntelliJ IDEA und PyCharm über Strg+Alt+M bzw. Strg+Alt+C. In Python stehen die Typen erst zur Laufzeit fest, die IDE muss raten: Beim Umbenennen der Methode erstelle_gesuch (etwa im Zuge des Parameterobjekts) findet sie Aufrufe über getattr oder aus Templates nicht, und bei untypisierten Parametern kann sie gleichnamige Methoden anderer Klassen verwechseln. Deshalb nach dem Umbau mit mypy prüfen, nach dem alten Namen als Text suchen und die Tests ausführen.

Bewertung: zwei passende automatische Refactorings 1 Punkt, Grenze der dynamischen Typisierung beschrieben 1 Punkt, Gegenmaßnahme (Typannotationen, mypy, Textsuche, Tests) 1 Punkt.

c) Jonas soll in der Klasse nur einen Fehler beheben: Die Uhrzeit wird als „7:5“ statt „07:05“ gespeichert. Er möchte dabei gleich alle Stellen aus a) umbauen und alles in einem Commit abgeben, „nach der Pfadfinderregel“. Nimm Stellung zu seinem Vorhaben. (3 Punkte)

Die Pfadfinderregel (Robert C. Martin) verlangt, den berührten Code etwas sauberer zu hinterlassen; das spricht dafür, dass Jonas aufräumt. Er sollte aber Fehlerbehebung und Refactoring trennen (zwei Hüte): erst ein Test, der „07:05“ erwartet und rot ist, dann die Korrektur in einem eigenen Commit, dann die Refactorings in kleinen Schritten mit je einem Commit. In einem einzigen Commit könnte im Review niemand erkennen, welche Änderung das Verhalten ändert, und ein Fehler im Umbau wäre schwer zu finden. Außerdem sollte er sich auf das beschränken, was er ohnehin anfasst: Die Uhrzeitprüfung (B) und die Speicherung (E) gehören dazu, das Parameterobjekt (A) ändert alle Aufrufer und sollte mit dem Team abgesprochen werden.

Bewertung: Pfadfinderregel richtig wiedergegeben 1 Punkt, Trennung von Fehlerbehebung und Refactoring in eigene Commits begründet 1 Punkt, Begrenzung auf den berührten Code bzw. Absprache bei Signaturänderungen 1 Punkt.

Aufgabe 3.1: Freie Plätze, die es nicht gibt (Profilkompetenz)

(ca. 20 min Bearbeitungszeit)

Lukas Hofmann möchte bei Ben Richter ab Gütersloh mitfahren. Der Flurmonitor zeigt für Bens Fahrt „1 frei“, die App lehnt Lukas’ Buchung aber mit „ausgebucht“ ab. Oksana Melnyk sammelt die Fakten:

  • Bens Fahrt hat 3 Plätze. Es gibt drei Anfragen: Aylin Kaya (bestätigt), Oksana Melnyk (bestätigt) und Jonas Wolff (abgelehnt, er hatte sich im Tag vertan).

  • Die Buchung prüft Buchungsdienst.bucheMitfahrt. Die Methode stammt aus Sprint 2; damals gab es nur die Status „OFFEN“ und „BESTAETIGT“.

  • In Sprint 4 kam das Ablehnen von Anfragen dazu. Im selben Sprint entstand die Klasse Monitorfeed für die Flurmonitore; ihre Zählung der belegten Plätze wurde aus bucheMitfahrt kopiert und um die Abfrage auf „ABGELEHNT“ ergänzt.

  • Das Ticket zum Ablehnen nannte nur die Oberfläche der App. Die Tests für Buchungsdienst enthalten keine Fahrt mit abgelehnter Anfrage.

// Anfrage.java
/** Status: "OFFEN", "BESTAETIGT" oder "ABGELEHNT" */
public record Anfrage(String name, String status) {
}

// Fahrt.java
import java.util.ArrayList;
import java.util.List;

public class Fahrt {

    private final String fahrer;
    private final int plaetze;
    private final List<Anfrage> anfragen = new ArrayList<>();

    public Fahrt(String fahrer, int plaetze) {
        this.fahrer = fahrer;
        this.plaetze = plaetze;
    }

    public String fahrer() {
        return fahrer;
    }

    public int plaetze() {
        return plaetze;
    }

    public List<Anfrage> anfragen() {
        return anfragen;
    }
}

// Buchungsdienst.java
public class Buchungsdienst {

    public boolean bucheMitfahrt(Fahrt fahrt, String name) {
        int belegt = fahrt.anfragen().size();
        if (belegt >= fahrt.plaetze()) {
            return false;
        }
        fahrt.anfragen().add(new Anfrage(name, "OFFEN"));
        return true;
    }
}

// Monitorfeed.java
public class Monitorfeed {

    public String platzanzeige(Fahrt fahrt) {
        int belegt = 0;
        for (Anfrage anfrage : fahrt.anfragen()) {
            if (!anfrage.status().equals("ABGELEHNT")) {
                belegt = belegt + 1;
            }
        }
        int frei = fahrt.plaetze() - belegt;
        if (frei == 0) {
            return "ausgebucht";
        }
        return frei + " frei";
    }
}

Das Klassendiagramm zeigt den heutigen Entwurf:

UML-Klassendiagramm vorher: Klassen Fahrt mit fahrer und plaetze, Anfrage mit name und status als String, Buchungsdienst mit bucheMitfahrt und Monitorfeed mit platzanzeige; beide nutzen Fahrt, Fahrt hat 0..* Anfragen (mit KI erstellt)

a) Führe eine Ursachenanalyse mit dem 5-mal-Warum durch, ausgehend vom Symptom „App lehnt Lukas ab, Monitor zeigt einen freien Platz“. Gib an, welche Ursachen Du für die eigentlichen hältst. (5 Punkte)

  1. Warum lehnt die App Lukas ab? bucheMitfahrt zählt 3 belegte Plätze bei 3 Plätzen.

  2. Warum 3? Die Methode zählt alle Anfragen (size() bzw. len), auch die abgelehnte von Jonas.

  3. Warum zählt sie die abgelehnte mit? Sie stammt aus Sprint 2, als es noch keine Ablehnung gab, und wurde in Sprint 4 nicht angepasst.

  4. Warum wurde sie nicht angepasst, obwohl der Monitor richtig zählt? Die Regel „welche Anfrage belegt einen Platz“ steht an zwei Stellen; beim Kopieren in Monitorfeed wurde nur die Kopie ergänzt, das Original blieb, wie es war.

  5. Warum fiel das vor der Auslieferung nicht auf? Das Ticket nannte nur die Oberfläche, und es gab keinen Test für eine Buchung bei abgelehnter Anfrage.

Eigentliche Ursachen: das doppelt dargestellte Wissen über freie Plätze (Verstoß gegen DRY, Code Smell Duplicated Code) und der fehlende Testfall bzw. die fehlende Prüfung, welche Stellen eine neue Statusart betrifft. Der Status als freie Zeichenkette (Primitive Obsession) erschwert zusätzlich, alle Verwendungen zu finden.

Bewertung: Kette vom Symptom bis zur Ursache mit belegten Antworten 3 Punkte, doppelte Regel als Ursache benannt 1 Punkt, fehlender Test bzw. Prozesslücke benannt 1 Punkt.

b) Schreibe einen Test, der den Fehler zeigt. Er muss gegen den heutigen Code rot sein. (4 Punkte)

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class BuchungsdienstTest {

    private Fahrt fahrt;

    @BeforeEach
    void fahrtMitEinerAbgelehntenAnfrage() {
        fahrt = new Fahrt("Ben Richter", 3);
        fahrt.anfragen().add(new Anfrage("Aylin Kaya", "BESTAETIGT"));
        fahrt.anfragen().add(new Anfrage("Oksana Melnyk", "BESTAETIGT"));
        fahrt.anfragen().add(new Anfrage("Jonas Wolff", "ABGELEHNT"));
    }

    @Test
    void abgelehnteAnfrageBelegtKeinenPlatz() {
        assertTrue(new Buchungsdienst().bucheMitfahrt(fahrt, "Lukas Hofmann"));
    }

    @Test
    void danachIstDieFahrtAusgebucht() {
        new Buchungsdienst().bucheMitfahrt(fahrt, "Lukas Hofmann");
        assertFalse(new Buchungsdienst().bucheMitfahrt(fahrt, "Lina Hartwig"));
        assertEquals("ausgebucht", new Monitorfeed().platzanzeige(fahrt));
    }
}

Gegen den heutigen Code schlagen beide Tests fehl: expected: <true> but was: <false> für die Buchung und expected: <ausgebucht> but was: <1 frei> für den Monitor nach Lukas’ Buchung (Python: zwei AssertionError).

Bewertung: Ausgangslage mit abgelehnter Anfrage aufgebaut 1 Punkt, Erwartung „Buchung gelingt“ 2 Punkte, Test ist gegen den heutigen Code rot 1 Punkt. Der zweite Test ist nicht verlangt.

c) Behebe den Fehler so, dass die Regel für freie Plätze nur noch an einer Stelle steht. Ersetze dabei auch den Status als Zeichenkette. Gib den umgebauten Code an; Dein Test aus b) muss danach grün sein. (7 Punkte)

Zuerst die Korrektur mit eigenem Commit (Test aus b) wird grün), dann das Refactoring in kleinen Schritten: Zählung als Methode freiePlaetze nach Fahrt verschieben (Move Function), die Regel „belegt einen Platz“ in Anfrage (Extract Function), den Status als Aufzählung (Replace Primitive with Object) und die Liste der Anfragen hinter fuegeAnfrageHinzu kapseln, damit niemand mehr von außen zählt.

// Anfragestatus.java
public enum Anfragestatus {
    OFFEN, BESTAETIGT, ABGELEHNT
}

// Anfrage.java
public record Anfrage(String name, Anfragestatus status) {

    /** Offene und bestätigte Anfragen halten einen Platz frei, abgelehnte nicht. */
    public boolean belegtPlatz() {
        return status != Anfragestatus.ABGELEHNT;
    }
}

// Fahrt.java
import java.util.ArrayList;
import java.util.List;

public class Fahrt {

    private final String fahrer;
    private final int plaetze;
    private final List<Anfrage> anfragen = new ArrayList<>();

    public Fahrt(String fahrer, int plaetze) {
        this.fahrer = fahrer;
        this.plaetze = plaetze;
    }

    public String fahrer() {
        return fahrer;
    }

    public void fuegeAnfrageHinzu(Anfrage anfrage) {
        anfragen.add(anfrage);
    }

    public int freiePlaetze() {
        int belegt = 0;
        for (Anfrage anfrage : anfragen) {
            if (anfrage.belegtPlatz()) {
                belegt = belegt + 1;
            }
        }
        return plaetze - belegt;
    }
}

// Buchungsdienst.java
public class Buchungsdienst {

    public boolean bucheMitfahrt(Fahrt fahrt, String name) {
        if (fahrt.freiePlaetze() == 0) {
            return false;
        }
        fahrt.fuegeAnfrageHinzu(new Anfrage(name, Anfragestatus.OFFEN));
        return true;
    }
}

// Monitorfeed.java
public class Monitorfeed {

    public String platzanzeige(Fahrt fahrt) {
        int frei = fahrt.freiePlaetze();
        if (frei == 0) {
            return "ausgebucht";
        }
        return frei + " frei";
    }
}

In den Tests ändert sich nur der Aufbau der Fahrt (fahrt.fuegeAnfrageHinzu(new Anfrage("Jonas Wolff", Anfragestatus.ABGELEHNT)) bzw. fahrt.fuege_anfrage_hinzu(Anfrage("Jonas Wolff", Anfragestatus.ABGELEHNT))). Ein dritter Test prüft freiePlaetze() direkt. Ergebnis: 3 von 3 Tests grün (JUnit 5 und pytest), mypy im strengen Modus ohne Befund.

Bewertung: Fehler behoben, Test grün 2 Punkte, Regel nur noch an einer Stelle (Fahrt.freiePlaetze, von Buchung und Monitor genutzt) 3 Punkte, Status als Aufzählung 1 Punkt, Anfragen gekapselt 1 Punkt.

d) Zeichne das UML-Klassendiagramm nach Deinem Umbau. (4 Punkte)

UML-Klassendiagramm nachher: Fahrt mit fuegeAnfrageHinzu und freiePlaetze, Anfrage mit belegtPlatz und status vom Typ Anfragestatus, Aufzählung Anfragestatus mit OFFEN, BESTAETIGT, ABGELEHNT; Buchungsdienst und Monitorfeed nutzen Fahrt (mit KI erstellt)

Bewertung: freiePlaetze() in Fahrt und belegtPlatz() in Anfrage 1 Punkt, Aufzählung Anfragestatus mit Literalen und Typ des Attributs 1 Punkt, Assoziation Fahrt – Anfrage mit Multiplizität 1 Punkt, Abhängigkeiten von Buchungsdienst und Monitorfeed zu Fahrt 1 Punkt.

e) Nenne zwei Maßnahmen, die verhindern, dass ein solcher Fehler wieder auftritt. (2 Punkte)

  • Zu jeder neuen Statusart oder Regel gehört ein Testfall für jede Stelle, die sie nutzt; das steht in der Definition of Done.

  • Tickets nennen die fachliche Regel, nicht nur die Oberfläche; im Review wird gefragt, wo die Regel sonst noch gilt.

  • Wissen wird nicht kopiert, sondern an einer Stelle bereitgestellt (DRY); statische Analyse meldet kopierten Code.

Bewertung: je passender Maßnahme 1 Punkt, höchstens 2 Punkte.


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 Refactoring: Code Smells erkennen und in sicheren Schritten umbauen” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/refactoring-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]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: