Übungsaufgaben zu „Vom Entwurf zum Code“

https://bildung.social/@oerinformatik/

https://oer-informatik.de/entwurf-und-code-uebungsaufgaben

Clean Code, Design before Implementation, Implementation reflects Design, Modularisierung, Paketzyklus, Acyclic Dependencies Principle, YAGNI, UML-Paketdiagramm, UML-Klassendiagramm, Architekturtest, Übungsaufgaben, Prüfungsvorbereitung

tl/dr; (Bearbeitungszeit insgesamt 53 min, mehrere Aufgaben): Zwei Übungsaufgaben zum Weg vom Entwurf zum Code: Du schneidest die CSB-Rad-Kata nach einem Paketdiagramm in Pakete, findest und löst einen Abhängigkeitszyklus, zeichnest das Klassendiagramm vorher und nachher und sicherst die Paketgrenzen mit einem Architekturtest. Danach vergleichst Du in der App Let’s Meet einen Entwurf mit dem Code, bewertest die Abweichungen und nimmst Stellung, wie viel vor der nächsten Funktion entworfen werden soll [AI-Assisted]. (Zuletzt geändert am 10.11.2026)

Diese Übungsaufgaben gehören zum Artikel Vom Entwurf zum Code: Design before Implementation, Implementation reflects Design, Modularisierung und YAGNI.

Wortwolke zu den Übungsaufgaben: Paketdiagramm, Entwurf, Abhängigkeitszyklus, Design before Implementation, Implementation reflects Design, YAGNI, Sendung, Kompositionswurzel, Architekturtest, Warteliste, Paketschnitt, Klassendiagramm (mit KI erstellt)

Hinweise zur Bearbeitung: Von FIA wird erwartet, dass ihnen Aufgaben auf Profilkompetenz-Ebene (Aufgabe 3.1) keine Probleme bereiten. Aufgaben auf Expertise-Ebene (Aufgabe 4.1) gehen darüber hinaus. Die Codeausschnitte stehen in Java und Python; wähle die Sprache, die Du gerade lernst, und schreib Deine Lösung in dieser Sprache. Für Aufgabe 3.1 brauchst Du eine Entwicklungsumgebung mit JUnit 5 bzw. pytest, damit Du nach jedem Umbauschritt die Tests ausführen kannst. Achte auf die Operatoren: „Ordne zu“ verlangt eine vollständige Zuordnung, „Ermittle“ ein vollständiges Ergebnis (hier Abhängigkeiten bzw. Abweichungen), „Baue um“ und „Schreibe“ verlangen lauffähigen Code, „Zeichne“ verlangt ein UML-Diagramm, „Bewerte“ verlangt ein begründetes Urteil je Punkt, „Nimm Stellung“ verlangt eine begründete eigene Position, die Gegenargumente abwägt. Schreib Deine Lösung zuerst vollständig auf, bevor Du die Beispiellösung aufklappst.

Du möchtest eigene Aufgaben zum Üben von einem KI-Sprachmodell generieren lassen?

Kopiere den Text in ein KI-Sprachmodell Deiner Wahl und passe Kompetenzstufe, Anzahl, Bearbeitungszeit und Szenario bei Bedarf an. Wie die Aufgaben aufgebaut sind, beschreibt der Artikel Wie sind die Aufgaben aufgebaut?. Prüfe die Ergebnisse mit dem Lehrartikel: KI-Sprachmodelle machen Fehler.

Erstelle neue Übungsaufgaben zum Thema „Vom Entwurf zum Code“.

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/entwurf-und-code
- https://oer-informatik.de/solid
- https://oer-informatik.de/einfacher-code
- https://oer-informatik.de/gui_pattern_mvc
- https://oer-informatik.de/refactoring
- https://oer-informatik.de/vorgehensmodelle
- Stilbeispiel: https://oer-informatik.de/entwurf-und-code-uebungsaufgaben

Die Aufgaben sollen Inhalte zu diesen Schlagwörtern abdecken:
- Clean Code
- Design before Implementation
- Implementation reflects Design
- Modularisierung
- Paketzyklus
- Acyclic Dependencies Principle
- YAGNI
- UML-Paketdiagramm
- UML-Klassendiagramm
- Architekturtest

Kompetenzstufe: Profilkompetenz (alternativ: Basiskompetenz, Kernkompetenz, Expertise)
Anzahl: 1 Aufgabe mit Teilaufgaben
Bearbeitungszeit in Minuten ca.: <5 bis 30 min>
Szenario: eines der Szenarien aus dem Bauplan

Vorgehen:
1. Lies zuerst die verlinkten Artikel. Kannst Du keine Webseiten
   abrufen, sag mir das, statt zu raten.
2. Steht bei einer Angabe noch ein Platzhalter in <…>, frag mich
   zuerst danach. Richte Punkte und Zahl der Teilaufgaben nach der
   Bearbeitungszeit (ca. 0,9 min je Punkt).
3. Gib je Aufgabe Titel mit Kompetenzstufe, Ausgangssituation und
   Teilaufgaben mit Operator und Punktzahl aus, dazu die geschätzte
   Bearbeitungszeit.
4. Gib die Beispiellösungen mit Punkteverteilung erst aus, wenn ich
   danach frage.

Aufgabe 3.1: Die Kata nach einem Paketdiagramm schneiden (Profilkompetenz)

(ca. 37 min Bearbeitungszeit)

Im Onlineshop der CSB-Rad GmbH (siehe Szenario CSB-Rad) soll die Bestätigungsmail bald über einen externen Maildienst laufen, und die Werkstatt in Bielefeld-Sennestadt wünscht sich einen Versandkostenrechner für die Produktseite. Bisher liegen alle Klassen in einem einzigen Paket. Das Team hat deshalb einen Entwurf für die Pakete gezeichnet:

UML-Paketdiagramm, Vorgabe: Im Paket de.csbrad.shop liegen die Klasse ShopKonfiguration (Kompositionswurzel) und die Pakete infrastruktur (Technik: Speichern, Mailversand), bestellung (Fachlichkeit: Bestellung, Ablauf des Abschlusses, Verträge für Speichern und Mailversand) und versand (Versandarten und ihre Regeln, hängt von keinem anderen Paket ab); erlaubt sind die Abhängigkeiten infrastruktur zu bestellung und bestellung zu versand, ShopKonfiguration darf alle drei nutzen (mit KI erstellt)

Du startest mit Stand 09 der Code-Kata; im Begleit-Repository liegt er unter src/kata/java/stand-09 bzw. src/kata/python/stand-09. Der vollständige Code mit den Tests:

// ===== src/main/java/de/csbrad/shop/Abholung.java =====
package de.csbrad.shop;

/** Abholung in der Werkstatt: kostenlos. */
public class Abholung implements Versandart {

    @Override
    public void pruefe(Bestellung bestellung) {
        // abholen lässt sich alles
    }

    @Override
    public int kosten(Bestellung bestellung) {
        return 0;
    }

    @Override
    public String mailzeile() {
        return "Abholung in der Werkstatt Bielefeld-Sennestadt";
    }
}

// ===== src/main/java/de/csbrad/shop/BestellRepository.java =====
package de.csbrad.shop;

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

/** Speichert Bestellungen (hier im Arbeitsspeicher) und vergibt fortlaufende Bestellnummern. */
public class BestellRepository implements BestellungSpeichern, BestellungLesen {

    private static final int ERSTE_BESTELLNUMMER = 1001;

    private final List<Bestellung> bestellungen = new ArrayList<>();
    private int naechsteNummer = ERSTE_BESTELLNUMMER;

    @Override
    public void speichere(Bestellung bestellung) {
        bestellung.vergibNummer("CSB-" + naechsteNummer);
        naechsteNummer = naechsteNummer + 1;
        bestellungen.add(bestellung);
    }

    @Override
    public Optional<Bestellung> findeNachNummer(String bestellnummer) {
        return bestellungen.stream().filter(b -> b.bestellnummer().equals(bestellnummer)).findFirst();
    }

    @Override
    public List<Bestellung> alle() {
        return List.copyOf(bestellungen);
    }
}

// ===== src/main/java/de/csbrad/shop/Bestellabschluss.java =====
package de.csbrad.shop;

/**
 * Ablauf des Bestellabschlusses: Versandart wählen, prüfen, speichern, bestätigen.
 * Hängt nur von Abstraktionen ab; die konkreten Objekte kommen über den Konstruktor (Konstruktor-Injektion).
 */
public class Bestellabschluss {

    private final Versandarten versandarten;
    private final BestellungSpeichern speicher;
    private final Mailversand mailversand;

    public Bestellabschluss(Versandarten versandarten, BestellungSpeichern speicher, Mailversand mailversand) {
        this.versandarten = versandarten;
        this.speicher = speicher;
        this.mailversand = mailversand;
    }

    public Bestellung abschliessen(Bestellanfrage anfrage) {
        Versandart versandart = versandarten.zuKennung(anfrage.versandart());
        Bestellung bestellung = new Bestellung(anfrage, versandart);
        versandart.pruefe(bestellung);
        speicher.speichere(bestellung);
        mailversand.sendeBestaetigung(bestellung);
        return bestellung;
    }
}

// ===== src/main/java/de/csbrad/shop/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) {
}

// ===== src/main/java/de/csbrad/shop/Bestellung.java =====
package de.csbrad.shop;

import java.util.List;

/** Eine Bestellung kennt ihre Positionen und berechnet ihre Beträge selbst (Tell, don't ask). */
public final class Bestellung {

    private static final String GUTSCHEINCODE_KURIER = "KURIER10";
    private static final int GUTSCHEIN_RABATT_PROZENT = 10;
    private static final int MEHRWERTSTEUER_PROZENT = 19;

    private final List<Position> positionen;
    private final Versandart versandart;
    private final String gutscheincode;
    private final String kundenEmail;
    private String bestellnummer;

    public Bestellung(Bestellanfrage anfrage, Versandart versandart) {
        pruefePositionen(anfrage.positionen());
        this.positionen = List.copyOf(anfrage.positionen());
        this.versandart = versandart;
        this.gutscheincode = anfrage.gutscheincode();
        this.kundenEmail = anfrage.kundenEmail();
        if (warenwert() == 0) {
            throw new IllegalArgumentException("Bestellung ohne Warenwert");
        }
    }

    private static void pruefePositionen(List<Position> positionen) {
        if (positionen.isEmpty()) {
            throw new IllegalArgumentException("Bestellung ohne Positionen");
        }
        for (Position position : positionen) {
            if (position.menge() <= 0) {
                throw new IllegalArgumentException("Menge muss positiv sein: " + position.artikelnr());
            }
        }
    }

    public int warenwert() {
        return positionen.stream().mapToInt(p -> p.nettopreisCent() * p.menge()).sum();
    }

    public int rabatt() {
        if (GUTSCHEINCODE_KURIER.equals(gutscheincode)) {
            return prozentKaufmaennischGerundet(warenwert(), GUTSCHEIN_RABATT_PROZENT);
        }
        return 0;
    }

    public int warenwertNachRabatt() {
        return warenwert() - rabatt();
    }

    public int versandkosten() {
        return versandart.kosten(this);
    }

    public int mehrwertsteuer() {
        return prozentKaufmaennischGerundet(warenwertNachRabatt() + versandkosten(), MEHRWERTSTEUER_PROZENT);
    }

    public int gesamtbetrag() {
        return warenwertNachRabatt() + versandkosten() + mehrwertsteuer();
    }

    public int gesamtgewichtGramm() {
        return positionen.stream().mapToInt(p -> p.gewichtGramm() * p.menge()).sum();
    }

    public int anzahlRaeder() {
        return positionen.stream().filter(Position::istRad).mapToInt(Position::menge).sum();
    }

    public boolean enthaeltRad() {
        return positionen.stream().anyMatch(Position::istRad);
    }

    public Versandart versandart() {
        return versandart;
    }

    public String kundenEmail() {
        return kundenEmail;
    }

    public String bestellnummer() {
        return bestellnummer;
    }

    void vergibNummer(String nummer) {
        if (bestellnummer != null) {
            throw new IllegalStateException("Bestellung hat schon eine Nummer: " + bestellnummer);
        }
        bestellnummer = nummer;
    }

    private static int prozentKaufmaennischGerundet(int betragCent, int prozent) {
        return (betragCent * prozent + 50) / 100;
    }
}

// ===== src/main/java/de/csbrad/shop/BestellungLesen.java =====
package de.csbrad.shop;

import java.util.List;
import java.util.Optional;

/** Schlankes Interface für Auswertungen, z. B. die Bestellübersicht im Backoffice: nur lesen (ISP). */
public interface BestellungLesen {

    Optional<Bestellung> findeNachNummer(String bestellnummer);

    List<Bestellung> alle();
}

// ===== src/main/java/de/csbrad/shop/BestellungSpeichern.java =====
package de.csbrad.shop;

/** Schlankes Interface für den Bestellabschluss: nur speichern (ISP). */
public interface BestellungSpeichern {

    /** Speichert die Bestellung und vergibt dabei die Bestellnummer. */
    void speichere(Bestellung bestellung);
}

// ===== src/main/java/de/csbrad/shop/Expressversand.java =====
package de.csbrad.shop;

/**
 * Neu in Stand 08: Expresspaket = Paketregeln plus Zuschlag, ohne Freigrenze.
 * Neue Klasse, kein vorhandener Code geändert (außer einer Zeile in Versandarten): Open-Closed-Prinzip.
 */
public class Expressversand implements Versandart {

    static final int EXPRESSZUSCHLAG_CENT = 990;

    private final Paketversand paketregeln = new Paketversand();

    @Override
    public void pruefe(Bestellung bestellung) {
        paketregeln.pruefe(bestellung);
    }

    @Override
    public int kosten(Bestellung bestellung) {
        return paketregeln.anzahlPakete(bestellung) * Paketversand.PAKETPAUSCHALE_CENT + EXPRESSZUSCHLAG_CENT;
    }

    @Override
    public String mailzeile() {
        return "Versand: Expresspaket";
    }
}

// ===== src/main/java/de/csbrad/shop/Kurierzustellung.java =====
package de.csbrad.shop;

/**
 * Neu in Stand 07: Zustellung per Lastenradkurier im Stadtgebiet, Pauschale, höchstens 25 kg, keine Räder.
 * Bewusst KEINE Unterklasse von Paketversand: Sie würde die Vorbedingung verschärfen (25 kg statt beliebig vieler Pakete)
 * und damit das Liskovsche Substitutionsprinzip verletzen. Gemeinsames (Rad-Prüfung) wird per Komposition genutzt.
 */
public class Kurierzustellung implements Versandart {

    static final int KURIERPAUSCHALE_CENT = 1290;
    static final int HOECHSTGEWICHT_KURIER_GRAMM = 25000;

    private final Paketversand paketregeln = new Paketversand();

    @Override
    public void pruefe(Bestellung bestellung) {
        paketregeln.pruefe(bestellung);
        if (bestellung.gesamtgewichtGramm() > HOECHSTGEWICHT_KURIER_GRAMM) {
            throw new IllegalArgumentException("Kurierzustellung nur bis 25 kg");
        }
    }

    @Override
    public int kosten(Bestellung bestellung) {
        return KURIERPAUSCHALE_CENT;
    }

    @Override
    public String mailzeile() {
        return "Versand: Kurier (Lastenrad)";
    }
}

// ===== src/main/java/de/csbrad/shop/Mailversand.java =====
package de.csbrad.shop;

/** Abstraktion für den Versand der Bestätigungsmail; der Bestellabschluss kennt nur sie (DIP). */
public interface Mailversand {

    void sendeBestaetigung(Bestellung bestellung);
}

// ===== src/main/java/de/csbrad/shop/Paketversand.java =====
package de.csbrad.shop;

/** Pakete: Pauschale je angefangene 31,5 kg, frei ab 100 Euro Warenwert nach Rabatt, keine Räder. */
public class Paketversand implements Versandart {

    static final int PAKETPAUSCHALE_CENT = 690;
    static final int HOECHSTGEWICHT_PAKET_GRAMM = 31500;
    static final int VERSANDKOSTENFREI_AB_CENT = 10000;

    @Override
    public void pruefe(Bestellung bestellung) {
        if (bestellung.enthaeltRad()) {
            throw new IllegalArgumentException("Räder können nicht als Paket versandt werden");
        }
    }

    public int anzahlPakete(Bestellung bestellung) {
        return Math.max(1, (bestellung.gesamtgewichtGramm() + HOECHSTGEWICHT_PAKET_GRAMM - 1) / HOECHSTGEWICHT_PAKET_GRAMM);
    }

    @Override
    public int kosten(Bestellung bestellung) {
        if (bestellung.warenwertNachRabatt() >= VERSANDKOSTENFREI_AB_CENT) {
            return 0;
        }
        return anzahlPakete(bestellung) * PAKETPAUSCHALE_CENT;
    }

    @Override
    public String mailzeile() {
        return "Versand: Paket";
    }
}

// ===== src/main/java/de/csbrad/shop/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) {
}

// ===== src/main/java/de/csbrad/shop/PostausgangMailversand.java =====
package de.csbrad.shop;

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

/** Mailversand in einen Postausgang (Liste), z. B. für Tests und die Entwicklungsumgebung. */
public class PostausgangMailversand implements Mailversand {

    private final List<String> postausgang = new ArrayList<>();

    @Override
    public void sendeBestaetigung(Bestellung bestellung) {
        postausgang.add("An: " + bestellung.kundenEmail() + "\nBestellung " + bestellung.bestellnummer()
                + "\nGesamtbetrag: " + alsEuroText(bestellung.gesamtbetrag()) + "\n" + bestellung.versandart().mailzeile());
    }

    public List<String> postausgang() {
        return List.copyOf(postausgang);
    }

    private static String alsEuroText(int betragCent) {
        return (betragCent / 100) + "," + String.format("%02d", betragCent % 100) + " EUR";
    }
}

// ===== src/main/java/de/csbrad/shop/Speditionsversand.java =====
package de.csbrad.shop;

/** Spedition: Pauschale je Rad; ohne Rad eine Pauschale. */
public class Speditionsversand implements Versandart {

    static final int SPEDITION_JE_RAD_CENT = 4900;

    @Override
    public void pruefe(Bestellung bestellung) {
        // per Spedition geht alles
    }

    @Override
    public int kosten(Bestellung bestellung) {
        return Math.max(1, bestellung.anzahlRaeder()) * SPEDITION_JE_RAD_CENT;
    }

    @Override
    public String mailzeile() {
        return "Versand: Spedition";
    }
}

// ===== src/main/java/de/csbrad/shop/Versandart.java =====
package de.csbrad.shop;

/** Strategie: jede Versandart prüft, berechnet und beschreibt sich selbst (OCP). */
public interface Versandart {

    /** Wirft IllegalArgumentException, wenn die Bestellung so nicht versandt werden kann. */
    void pruefe(Bestellung bestellung);

    int kosten(Bestellung bestellung);

    String mailzeile();
}

// ===== src/main/java/de/csbrad/shop/Versandarten.java =====
package de.csbrad.shop;

import java.util.Map;

/** Ordnet die Kennungen aus dem Shop-Formular den Versandarten zu; einzige Stelle, die neue Versandarten kennt. */
public class Versandarten {

    private final Map<String, Versandart> nachKennung = Map.of(
            "PAKET", new Paketversand(),
            "SPEDITION", new Speditionsversand(),
            "ABHOLUNG", new Abholung(),
            "KURIER", new Kurierzustellung(),
            "EXPRESS", new Expressversand());

    public Versandart zuKennung(String kennung) {
        Versandart versandart = nachKennung.get(kennung);
        if (versandart == null) {
            throw new IllegalArgumentException("Unbekannte Versandart: " + kennung);
        }
        return versandart;
    }
}

// ===== src/test/java/de/csbrad/shop/BestellabschlussIsoliertTest.java =====
package de.csbrad.shop;

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

import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/** Testet den Bestellabschluss isoliert: Speicher als Stub, Mailversand als Mock (handgeschriebene Test-Doubles). */
class BestellabschlussIsoliertTest {

    /** Stub: liefert eine feste Nummer, speichert nichts. */
    static class SpeicherStub implements BestellungSpeichern {
        @Override
        public void speichere(Bestellung bestellung) {
            bestellung.vergibNummer("CSB-TEST");
        }
    }

    /** Mock: merkt sich die Aufrufe, damit der Test sie prüfen kann. */
    static class MailversandMock implements Mailversand {
        final List<Bestellung> bestaetigt = new ArrayList<>();

        @Override
        public void sendeBestaetigung(Bestellung bestellung) {
            bestaetigt.add(bestellung);
        }
    }

    private MailversandMock mailversand;
    private Bestellabschluss abschluss;

    @BeforeEach
    void aufbauen() {
        mailversand = new MailversandMock();
        abschluss = new Bestellabschluss(new Versandarten(), new SpeicherStub(), mailversand);
    }

    @Test
    void schicktGenauEineBestaetigungMitNummer() {
        Bestellung bestellung = abschluss.abschliessen(new Bestellanfrage(List.of(BestellabschlussTest.LICHTSET), "PAKET", null, BestellabschlussTest.MAIL));
        assertEquals(1, mailversand.bestaetigt.size());
        assertEquals("CSB-TEST", mailversand.bestaetigt.get(0).bestellnummer());
        assertEquals(bestellung, mailversand.bestaetigt.get(0));
    }

    @Test
    void keineBestaetigungBeiAbgelehnterBestellung() {
        assertThrows(IllegalArgumentException.class,
                () -> abschluss.abschliessen(new Bestellanfrage(List.of(BestellabschlussTest.STADTRAD), "PAKET", null, BestellabschlussTest.MAIL)));
        assertEquals(0, mailversand.bestaetigt.size());
    }
}

// ===== src/test/java/de/csbrad/shop/BestellabschlussTest.java =====
package de.csbrad.shop;

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

import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
 * Charakterisierungstests (seit Stand 04), angepasst an die Klassen aus Stand 06; Objekte werden seit Stand 09 von außen übergeben.
 * betraege(): {Warenwert, Rabatt, Versandkosten, Mehrwertsteuer, Gesamtbetrag} in Cent.
 */
class BestellabschlussTest {

    static final Position LICHTSET = new Position("LIC-SET-01", "Lichtset Nabendynamo StVZO", 7563, 1, 350, false);
    static final Position BREMSE = new Position("BRE-HYD-01", "Hydraulische Scheibenbremse (Paar)", 13361, 1, 750, false);
    static final Position BUEGELSCHLOSS = new Position("SCH-BUE-01", "Bügelschloss, gleichschließend", 6218, 1, 1300, false);
    static final Position RAHMENSCHLOSS = new Position("SCH-RAH-01", "Rahmenschloss, gleichschließend", 4118, 1, 450, false);
    static final Position STADTRAD = new Position("RAD-CITY-01", "Stadtrad „Promenade“ 7-Gang", 67143, 1, 16800, true);
    static final Position EBIKE = new Position("RAD-EB-02", "E-Bike „Kanal“ Mittelmotor 500 Wh", 252017, 1, 24500, true);
    static final String MAIL = "mira.engel@example.org";

    private Bestellabschluss abschluss;
    private PostausgangMailversand postausgang;
    private BestellRepository repository;

    @BeforeEach
    void neuerAbschluss() {
        postausgang = new PostausgangMailversand();
        repository = new BestellRepository();
        abschluss = new Bestellabschluss(new Versandarten(), repository, postausgang);
    }

    static int[] betraege(Bestellung b) {
        return new int[] {b.warenwert(), b.rabatt(), b.versandkosten(), b.mehrwertsteuer(), b.gesamtbetrag()};
    }

    static Position menge(Position p, int m) {
        return new Position(p.artikelnr(), p.bezeichnung(), p.nettopreisCent(), m, p.gewichtGramm(), p.istRad());
    }

    @Test
    void paketUnterFreigrenze() {
        assertArrayEquals(new int[] {7563, 0, 690, 1568, 9821}, betraege(abschluss.abschliessen(new Bestellanfrage(List.of(LICHTSET), "PAKET", null, MAIL))));
    }

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

    @Test
    void freigrenzeGiltNachRabatt() {
        assertArrayEquals(new int[] {10336, 1034, 690, 1898, 11890}, betraege(abschluss.abschliessen(new Bestellanfrage(List.of(BUEGELSCHLOSS, RAHMENSCHLOSS), "PAKET", "KURIER10", MAIL))));
    }

    @Test
    void ohneRabattUeberFreigrenze() {
        assertArrayEquals(new int[] {10336, 0, 0, 1964, 12300}, betraege(abschluss.abschliessen(new Bestellanfrage(List.of(BUEGELSCHLOSS, RAHMENSCHLOSS), "PAKET", null, MAIL))));
    }

    @Test
    void schwerePaketbestellungMitRabatt() {
        assertArrayEquals(new int[] {155450, 15545, 0, 26582, 166487}, betraege(abschluss.abschliessen(new Bestellanfrage(List.of(menge(BUEGELSCHLOSS, 25)), "PAKET", "KURIER10", MAIL))));
    }

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

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

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

    @Test
    void unbekannterCodeGibtKeinenRabatt() {
        assertArrayEquals(new int[] {7563, 0, 690, 1568, 9821}, betraege(abschluss.abschliessen(new Bestellanfrage(List.of(LICHTSET), "PAKET", "SOMMER20", MAIL))));
    }

    @Test
    void radAlsPaketWirdAbgelehnt() {
        assertThrows(IllegalArgumentException.class, () -> abschluss.abschliessen(new Bestellanfrage(List.of(STADTRAD), "PAKET", null, MAIL)));
        assertEquals(0, postausgang.postausgang().size());
        assertEquals(0, repository.alle().size());
    }

    @Test
    void leereBestellungWirdAbgelehnt() {
        assertThrows(IllegalArgumentException.class, () -> abschluss.abschliessen(new Bestellanfrage(List.of(), "PAKET", null, MAIL)));
        assertEquals(0, postausgang.postausgang().size());
    }

    @Test
    void mengeNullWirdAbgelehnt() {
        assertThrows(IllegalArgumentException.class, () -> abschluss.abschliessen(new Bestellanfrage(List.of(menge(LICHTSET, 0)), "PAKET", null, MAIL)));
    }

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

    @Test
    void mailFuerPaketversand() {
        abschluss.abschliessen(new Bestellanfrage(List.of(LICHTSET), "PAKET", null, MAIL));
        assertEquals("An: mira.engel@example.org\nBestellung CSB-1001\nGesamtbetrag: 98,21 EUR\nVersand: Paket", postausgang.postausgang().get(0));
    }

    @Test
    void mailFuerAbholung() {
        abschluss.abschliessen(new Bestellanfrage(List.of(EBIKE), "ABHOLUNG", "KURIER10", MAIL));
        assertEquals("An: mira.engel@example.org\nBestellung CSB-1001\nGesamtbetrag: 2699,10 EUR\nAbholung in der Werkstatt Bielefeld-Sennestadt",
                postausgang.postausgang().get(0));
    }

    @Test
    void bestellungenWerdenFortlaufendNummeriertUndGespeichert() {
        abschluss.abschliessen(new Bestellanfrage(List.of(LICHTSET), "PAKET", null, MAIL));
        abschluss.abschliessen(new Bestellanfrage(List.of(BREMSE), "PAKET", null, MAIL));
        assertEquals(2, repository.alle().size());
        assertEquals("CSB-1001", repository.alle().get(0).bestellnummer());
        assertEquals("CSB-1002", repository.alle().get(1).bestellnummer());
        assertEquals(15900, repository.alle().get(1).gesamtbetrag());
    }

    // ---- neue Anforderung in Stand 07: Kurierzustellung ----

    @Test
    void kurierKostetPauschale() {
        assertArrayEquals(new int[] {19579, 0, 1290, 3965, 24834},
                betraege(abschluss.abschliessen(new Bestellanfrage(List.of(BREMSE, BUEGELSCHLOSS), "KURIER", null, MAIL))));
        assertEquals("An: mira.engel@example.org\nBestellung CSB-1001\nGesamtbetrag: 248,34 EUR\nVersand: Kurier (Lastenrad)",
                postausgang.postausgang().get(0));
    }

    @Test
    void kurierUeber25KiloWirdAbgelehnt() {
        assertThrows(IllegalArgumentException.class,
                () -> abschluss.abschliessen(new Bestellanfrage(List.of(menge(BUEGELSCHLOSS, 20)), "KURIER", null, MAIL)));
        assertEquals(0, repository.alle().size());
    }

    @Test
    void kurierNimmtKeineRaeder() {
        assertThrows(IllegalArgumentException.class,
                () -> abschluss.abschliessen(new Bestellanfrage(List.of(STADTRAD), "KURIER", null, MAIL)));
    }

    // ---- neue Anforderung in Stand 08: Expresspaket ----

    @Test
    void expressOhneFreigrenze() {
        assertArrayEquals(new int[] {13361, 0, 1680, 2858, 17899},
                betraege(abschluss.abschliessen(new Bestellanfrage(List.of(BREMSE), "EXPRESS", null, MAIL))));
        assertEquals("An: mira.engel@example.org\nBestellung CSB-1001\nGesamtbetrag: 178,99 EUR\nVersand: Expresspaket",
                postausgang.postausgang().get(0));
    }

    @Test
    void expressJePaket() {
        // 25 Bügelschlösser = 32,5 kg = 2 Pakete
        assertEquals(2 * 690 + 990, abschluss.abschliessen(new Bestellanfrage(List.of(menge(BUEGELSCHLOSS, 25)), "EXPRESS", null, MAIL)).versandkosten());
    }

    @Test
    void expressNimmtKeineRaeder() {
        assertThrows(IllegalArgumentException.class,
                () -> abschluss.abschliessen(new Bestellanfrage(List.of(STADTRAD), "EXPRESS", null, MAIL)));
    }
}

a) Ordne alle Klassen, Records und Interfaces des Hauptcodes (ohne Tests) den drei Paketen des Entwurfs zu. Begründe Deine Zuordnung für Mailversand und PostausgangMailversand. (4 Punkte)

Zuordnung der Klassen zu den Paketen
Paket Klassen, Records und Interfaces
infrastruktur BestellRepository, PostausgangMailversand
bestellung Bestellabschluss, Bestellanfrage, Bestellung, Position, BestellungSpeichern, BestellungLesen, Mailversand
versand Versandart, Versandarten, Paketversand, Speditionsversand, Abholung, Kurierzustellung, Expressversand

Vollständige und richtige Zuordnung (3 Punkte, je Fehler 1 Punkt Abzug). Mailversand ist der Vertrag, den der Bestellabschluss braucht; nach dem Dependency Inversion Principle legt die Fachlichkeit ihn fest, deshalb liegt er in bestellung. PostausgangMailversand ist eine technische Umsetzung dieses Vertrags und wird beim Wechsel zum externen Maildienst ersetzt; sie gehört in infrastruktur (1 Punkt). In Python liegen BestellungSpeichern und BestellungLesen im Modul bestellung_ports.py; das Modul wandert als Ganzes nach bestellung.

b) Ermittle, welche Abhängigkeiten zwischen den drei Paketen entstehen, wenn Du die Klassen nur verschiebst, ohne sonst etwas zu ändern. Zeichne sie als UML-Paketdiagramm und erläutere, welche Abhängigkeit dem Entwurf widerspricht und warum sie ein Problem ist. (4 Punkte)

UML-Paketdiagramm nach dem bloßen Verschieben: infrastruktur hängt von bestellung ab, bestellung hängt von versand ab und versand von bestellung; die Notiz erklärt den Zyklus über Versandart.kosten(Bestellung) (mit KI erstellt)

Abhängigkeiten (2 Punkte): infrastruktur → bestellung (BestellRepository und PostausgangMailversand nutzen Bestellung und realisieren die Interfaces), bestellung → versand (Bestellung hält eine Versandart, Bestellabschluss nutzt Versandarten) und versand → bestellung (Versandart und alle Strategien bekommen in pruefe und kosten eine Bestellung).

Die letzte Abhängigkeit widerspricht dem Entwurf: versand soll von keinem Paket abhängen (1 Punkt). Zusammen mit bestellung → versand entsteht ein Zyklus. Die beiden Pakete lassen sich dann nur gemeinsam verwenden und testen; der Versandkostenrechner für die Produktseite müsste das ganze Paket bestellung mitnehmen. In Python kann der Zyklus schon beim Import scheitern, wenn beide Module die Namen des anderen beim Laden brauchen (1 Punkt).

c) Baue den Code so um, dass er dem Entwurf entspricht: Klassen in die Pakete verschieben, den Zyklus auflösen und eine Kompositionswurzel ShopKonfiguration (Python: Modul csbrad/konfiguration.py) einführen, die den Bestellabschluss erzeugt. Das Verhalten darf sich nicht ändern; alle Tests müssen grün bleiben. (10 Punkte)

Die Klassen werden nach Teilaufgabe a) verschoben; die Entwicklungsumgebung passt package- und import-Zeilen an (2 Punkte). In Java muss Bestellung.vergibNummer jetzt public sein, weil das Repository in einem anderen Paket liegt (1 Punkt).

Den Zyklus löst eine Abstraktion im Paket versand, die genau das beschreibt, was die Versandarten von einer Bestellung brauchen (3 Punkte); Versandart und alle Strategien nutzen nur noch sie (2 Punkte):

// ===== versand/Sendung.java =====
package de.csbrad.shop.versand;

/**
 * Was eine Versandart über eine Bestellung wissen muss. Das Paket versand definiert diese Abstraktion selbst;
 * die Bestellung implementiert sie. So hängt versand nicht von bestellung ab, und es entsteht kein Paketzyklus.
 */
public interface Sendung {

    int gesamtgewichtGramm();

    int anzahlRaeder();

    boolean enthaeltRad();

    int warenwertNachRabatt();
}

// ===== versand/Versandart.java =====
package de.csbrad.shop.versand;

/** Strategie: jede Versandart prüft, berechnet und beschreibt sich selbst (OCP). */
public interface Versandart {

    /** Wirft IllegalArgumentException, wenn die Sendung so nicht versandt werden kann. */
    void pruefe(Sendung sendung);

    int kosten(Sendung sendung);

    String mailzeile();
}

// ===== versand/Paketversand.java (ebenso die anderen Strategien: Sendung statt Bestellung) =====
package de.csbrad.shop.versand;

/** Pakete: Pauschale je angefangene 31,5 kg, frei ab 100 Euro Warenwert nach Rabatt, keine Räder. */
public class Paketversand implements Versandart {

    static final int PAKETPAUSCHALE_CENT = 690;
    static final int HOECHSTGEWICHT_PAKET_GRAMM = 31500;
    static final int VERSANDKOSTENFREI_AB_CENT = 10000;

    @Override
    public void pruefe(Sendung sendung) {
        if (sendung.enthaeltRad()) {
            throw new IllegalArgumentException("Räder können nicht als Paket versandt werden");
        }
    }

    public int anzahlPakete(Sendung sendung) {
        return Math.max(1, (sendung.gesamtgewichtGramm() + HOECHSTGEWICHT_PAKET_GRAMM - 1) / HOECHSTGEWICHT_PAKET_GRAMM);
    }

    @Override
    public int kosten(Sendung sendung) {
        if (sendung.warenwertNachRabatt() >= VERSANDKOSTENFREI_AB_CENT) {
            return 0;
        }
        return anzahlPakete(sendung) * PAKETPAUSCHALE_CENT;
    }

    @Override
    public String mailzeile() {
        return "Versand: Paket";
    }
}

// ===== bestellung/Bestellung.java (Kopf; die vier Methoden von Sendung erhalten @Override) =====
package de.csbrad.shop.bestellung;

import de.csbrad.shop.versand.Sendung;
import de.csbrad.shop.versand.Versandart;
import java.util.List;

/** Eine Bestellung kennt ihre Positionen und berechnet ihre Beträge selbst (Tell, don't ask). */
public final class Bestellung implements Sendung {
    // ... unverändert, außer: @Override an warenwertNachRabatt, gesamtgewichtGramm, anzahlRaeder, enthaeltRad
    // und public void vergibNummer(String nummer)
}

// ===== ShopKonfiguration.java =====
package de.csbrad.shop;

import de.csbrad.shop.bestellung.Bestellabschluss;
import de.csbrad.shop.infrastruktur.BestellRepository;
import de.csbrad.shop.infrastruktur.PostausgangMailversand;
import de.csbrad.shop.versand.Versandarten;

/** Kompositionswurzel: die einzige Stelle, die alle Pakete kennt und die Objekte zusammensteckt. */
public final class ShopKonfiguration {

    private ShopKonfiguration() {
    }

    public static Bestellabschluss bestellabschluss(BestellRepository repository, PostausgangMailversand mailversand) {
        return new Bestellabschluss(new Versandarten(), repository, mailversand);
    }
}

Die Tests importieren aus den neuen Paketen und erzeugen den Bestellabschluss über die Kompositionswurzel (1 Punkt); alle 24 Tests bleiben grün (1 Punkt). Den vollständigen Zielstand findest Du im Begleit-Repository unter src/kata/java/stand-10 bzw. src/kata/python/stand-10.

Andere Lösungen sind möglich, wenn der Zyklus wirklich aufgelöst ist. Nicht ausreichend ist es, in Python nur alle Importe unter if TYPE_CHECKING: zu verschieben: Dann läuft der Code, aber die Pakete hängen im Entwurf weiter gegenseitig voneinander ab (höchstens 5 Punkte).

d) Zeichne den Ausschnitt mit Bestellung, Versandart, Paketversand und Deiner neuen Abstraktion als UML-Klassendiagramm einmal vor und einmal nach dem Umbau. Zeichne die Pakete als Rahmen ein. (5 Punkte)

Vorher (2 Punkte):

UML-Klassendiagramm vorher: Bestellung im Paket bestellung hat eine gerichtete Assoziation zu genau einer Versandart im Paket versand, Versandart nutzt umgekehrt Bestellung als Parametertyp; Paketversand realisiert Versandart (mit KI erstellt)

Nachher (3 Punkte):

UML-Klassendiagramm nachher: Das Paket versand enthält die Interfaces Sendung und Versandart, Versandart nutzt Sendung; Bestellung im Paket bestellung realisiert Sendung und hat eine gerichtete Assoziation zu genau einer Versandart; Paketversand realisiert Versandart (mit KI erstellt)

Bewertung: Pakete als Rahmen, Interfaces mit «interface» und Realisierung mit gestrichelter Linie und hohlem Dreieck (je Diagramm 1 Punkt); vorher die Rückrichtung von Versandart zu Bestellung (1 Punkt); nachher die neue Abstraktion mit ihren Operationen im Paket versand, realisiert von Bestellung (1 Punkt), und alle Pfeile über die Paketgrenze von bestellung nach versand (1 Punkt). Die Operationen genügen bei den beiden Interfaces; private Hilfsmethoden gehören nicht ins Diagramm.

e) Schreibe einen Test, der fehlschlägt, sobald eine Klasse in versand etwas aus bestellung oder infrastruktur importiert oder eine Klasse in bestellung etwas aus infrastruktur. Zeige, dass er den Zustand aus Teilaufgabe b) erkennt. (4 Punkte)

// ===== src/test/java/de/csbrad/shop/PaketabhaengigkeitenTest.java =====
package de.csbrad.shop;

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

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;

/** Architekturtest: prüft die erlaubten Abhängigkeiten zwischen den Paketen anhand der import-Zeilen. */
class PaketabhaengigkeitenTest {

    private static final Path QUELLEN = Path.of("src/main/java/de/csbrad/shop");
    private static final Map<String, List<String>> VERBOTEN = Map.of(
            "versand", List.of("de.csbrad.shop.bestellung", "de.csbrad.shop.infrastruktur"),
            "bestellung", List.of("de.csbrad.shop.infrastruktur"));

    @Test
    void keineVerbotenenAbhaengigkeiten() throws IOException {
        List<String> verstoesse = new ArrayList<>();
        for (var eintrag : VERBOTEN.entrySet()) {
            verstoesse.addAll(verstoesseIn(eintrag.getKey(), eintrag.getValue()));
        }
        assertEquals(List.of(), verstoesse);
    }

    private static List<String> verstoesseIn(String paket, List<String> verbotenePakete) throws IOException {
        List<String> verstoesse = new ArrayList<>();
        try (Stream<Path> dateien = Files.list(QUELLEN.resolve(paket))) {
            for (Path datei : dateien.toList()) {
                for (String zeile : Files.readAllLines(datei)) {
                    for (String verboten : verbotenePakete) {
                        if (zeile.startsWith("import " + verboten + ".")) {
                            verstoesse.add(paket + "/" + datei.getFileName() + ": " + zeile);
                        }
                    }
                }
            }
        }
        return verstoesse;
    }
}

Verbotene Richtungen vollständig erfasst (1 Punkt), Importe korrekt ermittelt (1 Punkt), aussagekräftige Meldung mit Datei und Import (1 Punkt). Nachweis (1 Punkt): Auf den bloß verschobenen Stand aus b) angewendet, schlägt der Test fehl und nennt versand/Versandart, versand/Paketversand usw. mit dem Import von Bestellung; nach dem Umbau aus c) ist er grün (in Java sind es dann 25 Tests). Der Python-Test erkennt auch Importe unter if TYPE_CHECKING:. Bibliotheken wie ArchUnit oder import-linter sind ebenso richtig.

Aufgabe 4.1: Entwurf und Code der Warteliste vergleichen (Expertise)

(ca. 16 min Bearbeitungszeit)

Die Let’s Meet GmbH baut das Backend ihrer App neu. Eine der geplanten Funktionen sind Wartelisten für volle Aktivitäten: Sagt jemand ab, rückt die Person nach, die am längsten wartet, und bekommt eine Benachrichtigung. Yusuf Demir, Leiter der Entwicklung, hat dafür vorab einen Entwurf gezeichnet:

UML-Paketdiagramm, Vorgabe: Paket aktivitaeten mit Aktivitaet, Paket warteliste mit Warteliste und Wartelistendienst (Anwendungsfall Absage verarbeiten: sagt in der Aktivität ab, lässt nachrücken, benachrichtigt), Paket benachrichtigung mit dem Interface Benachrichtigung und der Operation nachgerueckt(benutzerId, aktivitaetId), Paket push mit PushBenachrichtigung, die Benachrichtigung realisiert; warteliste hängt von aktivitaeten und benachrichtigung ab (mit KI erstellt)

Jonas Weber, Auszubildender im zweiten Lehrjahr, hat die Funktion umgesetzt. Sein Code:

// ===== aktivitaeten/Aktivitaet.java =====
package de.letsmeet.aktivitaeten;

import de.letsmeet.warteliste.Warteliste;
import java.util.ArrayList;
import java.util.List;

/** Eine Aktivität mit begrenzter Zahl an Plätzen. */
public final class Aktivitaet {

    private final int aktivitaetId;
    private final int maxTeilnehmende;
    private final List<Integer> teilnehmende = new ArrayList<>();
    private final Warteliste warteliste = new Warteliste(this);

    public Aktivitaet(int aktivitaetId, int maxTeilnehmende) {
        this.aktivitaetId = aktivitaetId;
        this.maxTeilnehmende = maxTeilnehmende;
    }

    public void melde(int benutzerId) {
        if (istVoll()) {
            warteliste.stelleAn(benutzerId, 0);
        } else {
            teilnehmende.add(benutzerId);
        }
    }

    public void sageAb(int benutzerId) {
        teilnehmende.remove(Integer.valueOf(benutzerId));
        warteliste.rueckeNach();
    }

    public void nimmAuf(int benutzerId) {
        teilnehmende.add(benutzerId);
    }

    public boolean istVoll() {
        return teilnehmende.size() >= maxTeilnehmende;
    }

    public int aktivitaetId() {
        return aktivitaetId;
    }

    public List<Integer> teilnehmende() {
        return List.copyOf(teilnehmende);
    }
}

// ===== warteliste/Warteliste.java =====
package de.letsmeet.warteliste;

import de.letsmeet.aktivitaeten.Aktivitaet;
import de.letsmeet.push.PushBenachrichtigung;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

/** Warteliste einer vollen Aktivität: wer zuerst kommt, rückt zuerst nach. */
public class Warteliste {

    /** Ein Platz auf der Warteliste; die Priorität ist für spätere Premium-Konten gedacht. */
    record Platz(int benutzerId, int prioritaet, long eingetragenUm) {
    }

    private final Aktivitaet aktivitaet;
    private final List<Platz> plaetze = new ArrayList<>();
    private final PushBenachrichtigung push = new PushBenachrichtigung();
    private long zaehler = 0;

    public Warteliste(Aktivitaet aktivitaet) {
        this.aktivitaet = aktivitaet;
    }

    public void stelleAn(int benutzerId, int prioritaet) {
        zaehler = zaehler + 1;
        plaetze.add(new Platz(benutzerId, prioritaet, zaehler));
    }

    public void rueckeNach() {
        if (plaetze.isEmpty() || aktivitaet.istVoll()) {
            return;
        }
        plaetze.sort(Comparator.comparingInt(Platz::prioritaet).reversed()
                .thenComparingLong(Platz::eingetragenUm));
        Platz erster = plaetze.remove(0);
        aktivitaet.nimmAuf(erster.benutzerId());
        push.sende(erster.benutzerId(), "Du bist nachgerückt: Aktivität " + aktivitaet.aktivitaetId());
    }

    public int laenge() {
        return plaetze.size();
    }
}

// ===== benachrichtigung/Benachrichtigung.java =====
package de.letsmeet.benachrichtigung;

/** Vertrag aus dem Entwurf: informiert eine Person, die von der Warteliste nachgerückt ist. */
public interface Benachrichtigung {

    void nachgerueckt(int benutzerId, int aktivitaetId);
}

// ===== push/PushBenachrichtigung.java =====
package de.letsmeet.push;

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

/** Schickt Push-Nachrichten an die App (hier nur in eine Liste). */
public class PushBenachrichtigung {

    private final List<String> gesendet = new ArrayList<>();

    public void sende(int benutzerId, String text) {
        gesendet.add(benutzerId + ": " + text);
    }

    public List<String> gesendet() {
        return List.copyOf(gesendet);
    }
}

Der Code funktioniert: Melden sich bei einer Aktivität mit zwei Plätzen die Personen 1, 2 und 3 an und sagt Person 1 ab, rückt Person 3 nach.

a) Ermittle alle Stellen, an denen der Code vom Entwurf abweicht. Gib jeweils die Stelle im Code und das betroffene Prinzip an. Nenne außerdem eine Stelle, an der der Code mehr enthält als der Entwurf, ohne dass das eine Abweichung ist. (6 Punkte)

Die tatsächlichen Abhängigkeiten im Code:

UML-Paketdiagramm der tatsächlichen Abhängigkeiten im Code: aktivitaeten hängt von warteliste ab und warteliste von aktivitaeten (Zyklus), warteliste hängt von push ab; das Paket benachrichtigung wird von niemandem genutzt oder realisiert, der Wartelistendienst fehlt (mit KI erstellt)
Abweichungen des Codes vom Entwurf
Nr. Abweichung Stelle im Code betroffenes Prinzip
1 Es gibt keinen Wartelistendienst; der Ablauf „Absage verarbeiten“ steckt in Aktivitaet.sageAb. Aktivitaet.sageAb bzw. sage_ab Implementation reflects Design, SRP
2 aktivitaeten hängt von warteliste ab: Aktivitaet erzeugt eine Warteliste und ruft stelleAn und rueckeNach auf. Zusammen mit der erlaubten Richtung entsteht ein Zyklus. import in Aktivitaet, Attribut warteliste Implementation reflects Design, Acyclic Dependencies Principle
3 warteliste hängt direkt von push ab und erzeugt die konkrete PushBenachrichtigung selbst, statt das Interface Benachrichtigung zu nutzen. Attribut push in Warteliste Implementation reflects Design, Dependency Inversion Principle
4 PushBenachrichtigung realisiert Benachrichtigung nicht; die Operation heißt sende(benutzerId, text) statt nachgerueckt(benutzerId, aktivitaetId). Das Interface ist toter Code. PushBenachrichtigung, Benachrichtigung Implementation reflects Design (Vertrag nicht eingehalten)
5 Prioritäten für „spätere Premium-Konten“ und ein zweiter Parameter von stelleAn, der immer 0 ist; der Entwurf und die Anforderung kennen das nicht. Platz.prioritaet, Sortierung in rueckeNach YAGNI

Je Abweichung mit Stelle und Prinzip 1 Punkt (5 Punkte). Keine Abweichung (1 Punkt): Der Record bzw. die Dataclass Platz, der Zähler für die Reihenfolge und die Sortierung sind Innenleben der Warteliste. Der Entwurf muss sie nicht enthalten, nach Design and Implementation don’t overlap soll er es sogar nicht.

In Python fällt der Zyklus beim Ausführen nicht auf, weil warteliste.py die Aktivitaet nur unter if TYPE_CHECKING: importiert; im Entwurf bleibt er trotzdem bestehen.

b) Bewerte jede Abweichung aus a): Soll der Code an den Entwurf angepasst werden, der Entwurf an den Code, oder bleibt es, wie es ist? Begründe Deine Entscheidung und skizziere in Worten, was sich ändert. (5 Punkte)

  • 1 und 2 (Code anpassen): Ein Wartelistendienst im Paket warteliste bekommt die Aktivität und ihre Warteliste und erledigt „Absage verarbeiten“: absagen lassen, nachrücken lassen, benachrichtigen. Aktivitaet kennt dann keine Warteliste mehr, und der Zyklus ist weg. Gründe: Die Aktivität soll auch ohne Warteliste testbar und nutzbar sein, etwa für die geplanten Bewertungen nach einer Aktivität, und der Ablauf hat einen eigenen Grund für Änderungen (z. B. Fristen für das Nachrücken) (2 Punkte).

  • 3 und 4 (Code anpassen): PushBenachrichtigung realisiert Benachrichtigung mit nachgerueckt; der Wartelistendienst bekommt eine Benachrichtigung über den Konstruktor. So lässt sich das Nachrücken ohne echte Push-Nachrichten testen, und für Sabine Hoffmann, die visuelle Benachrichtigungen statt Töne braucht, kann später ein anderer Kanal hinzukommen, ohne die Warteliste zu ändern (2 Punkte).

  • 5 (Code anpassen, Prioritäten entfernen): Premium-Konten sind nicht beauftragt; die Priorität macht die Sortierung schwerer verständlich und muss mitgetestet werden. Erst wenn Clara Neumann als Product Owner Premium-Konten konkret plant, kommt die Priorität in Entwurf und Code, und zwar dann in beide (1 Punkt).

Andere Entscheidungen sind richtig, wenn sie schlüssig begründet sind, z. B. bei 4 den Entwurf an einen allgemeineren Vertrag sende(benutzerId, text) anzupassen, weil weitere Benachrichtigungen geplant sind; dann muss der Entwurf aber ausdrücklich geändert werden, nicht still.

c) In der Teambesprechung geht es um die nächste Funktion, die Freundesliste. Clara Neumann sagt: „Der Entwurf für die Warteliste hat Zeit gekostet und wurde trotzdem nicht eingehalten. Bei der Freundesliste fangen wir direkt mit dem Programmieren an und räumen später auf.“ Yusuf Demir hält dagegen: „Im Gegenteil, wir müssen gründlicher entwerfen: ein vollständiges Klassendiagramm mit allen Attributen und Methoden, bevor jemand eine Zeile schreibt.“ Nimm Stellung zu beiden Vorschlägen und schlage ein Vorgehen für die Freundesliste vor. (6 Punkte)

Zu Clara: Richtig ist, dass ein Entwurf nichts nützt, wenn ihn niemand einhält, und dass YAGNI vor Entwurf auf Vorrat warnt (1 Punkt). Ohne Entwurf fallen die Entscheidungen über Pakete und Abhängigkeiten aber nebenbei im Code, wie die Abweichungen der Warteliste zeigen: Zyklus, direkte Abhängigkeit von der Technik. „Später aufräumen“ ist teurer, weil dann schon andere Teile von der falschen Struktur abhängen, und mit mehreren Personen im Team fehlt ohne Entwurf das gemeinsame Bild (1 Punkt).

Zu Yusuf: Richtig ist, dass vor der Umsetzung entworfen werden soll (Design before Implementation) (1 Punkt). Ein vollständiges Klassendiagramm mit allen Attributen und Methoden ist aber Big Design Up Front: Es überschneidet sich mit dem Code, muss doppelt gepflegt werden und veraltet beim ersten Refactoring (Design and Implementation don’t overlap). Das Problem der Warteliste war nicht zu wenig Detail, sondern dass niemand geprüft hat, ob der Code den Entwurf umsetzt (1 Punkt).

Vorschlag (2 Punkte): Just enough design für die Freundesliste: ein Paketdiagramm mit Komponenten, Verträgen (Interfaces mit ihren Operationen) und erlaubten Abhängigkeiten, gemeinsam im Team besprochen; Datenschutzfragen (Sichtbarkeit von Profilen Minderjähriger) mit Dr. Marta Kowalczyk vorab klären, weil sie den Zuschnitt beeinflussen. Das Innenleben entscheidet die Umsetzung. Damit der Entwurf eingehalten wird: ein Architekturtest für die verbotenen Abhängigkeiten und ein kurzer Abgleich von Entwurf und Code im Code-Review; Abweichungen werden besprochen und entweder im Code oder bewusst im Entwurf korrigiert. Umgesetzt wird in kleinen Inkrementen mit Refactoring nach jedem Schritt.


Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Übungsaufgaben zu „Vom Entwurf zum Code“” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/entwurf-und-code-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: