Übungsaufgaben zu lesbarem Code: Konventionen, Namen, Kommentare und Command-Query-Separation

https://bildung.social/@oerinformatik/

https://oer-informatik.de/lesbarer-code-uebungsaufgaben

Clean Code, Lesbarkeit, Namenskonvention, Magic Number, Kommentar, Prinzip der geringsten Überraschung, Command-Query-Separation, Übungsaufgaben, Prüfungsvorbereitung

tl/dr; (ca. 72 min Lese- und Bearbeitungszeit): Vier Übungsaufgaben zu lesbarem Code: Du verbesserst Namen und Konventionen in der Kostenbeitragsberechnung der Mitfahr-App Mitfahrlabor, bewertest Kommentare in der Anzeige der Flurmonitore, machst den Bestellabschluss der CSB-Rad-Kata lesbar und baust einen Getter mit Seiteneffekt nach Command-Query-Separation um [AI-Assisted]. (Zuletzt geändert am 09.11.2026)

Disclaimer: Teile des Textes stammen von einer KI. Inhalte sind nicht automatisch zuverlässig; überprüfe Fakten und zitiere vertrauenswürdige Quellen.

Diese Übungsaufgaben gehören zum Artikel Lesbarer Code: Konventionen, sprechende Namen, Kommentare und das Prinzip der geringsten Überraschung.

Wortwolke zu den Übungsaufgaben: Namenskonvention, Magic Number, Kommentar, Command-Query-Separation, Getter (mit KI erstellt)

Hinweise zur Bearbeitung: Bis zur Teil-1-Prüfung der IT-Berufe solltest Du die Basiskompetenz beherrschen, also Aufgabe 1.1 und Aufgabe 1.2. Als FIA musst Du zur Teil-2-Prüfung Aufgaben auf Kernkompetenz-Ebene lösen können, das sind Aufgabe 2.1 und Aufgabe 2.2. Die Codeausschnitte stehen in Java und Python; wähle die Sprache, die Du gerade lernst, und schreib Deine Lösung in dieser Sprache. Achte auf die Operatoren: „Nenne“ verlangt Stichworte ohne Erklärung, „Beurteile“ und „Erläutere“ verlangen eine Aussage mit Begründung am Code, „Schreibe“ und „Baue um“ verlangen lauffähigen Code. 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 „Lesbarer Code: Konventionen,
Namen, Kommentare und Command-Query-Separation“.

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

Die Aufgaben sollen Inhalte zu diesen Schlagwörtern abdecken:
- Clean Code
- Lesbarkeit
- Namenskonvention
- Magic Number
- Kommentar
- Prinzip der geringsten Überraschung
- Command-Query-Separation

Kompetenzstufe: Basiskompetenz (alternativ: Kernkompetenz, 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 1.1: Kostenbeitrag im Mitfahrlabor (Basiskompetenz)

In der Mitfahr-App Mitfahrlabor des DBH-Berufskollegs geben Fahrer*innen beim Anbieten einer Fahrt an, wie viel Cent je Kilometer und Person sie als Kostenbeitrag verlangen möchten. Nach den Regeln der App sind höchstens 0,10 € je km und Person erlaubt; ein höherer Wunsch wird auf diesen Höchstsatz begrenzt. Jonas Wolff aus dem Projektteam hat die Berechnung geschrieben. Sie liefert die Summe der Kostenbeiträge aller Mitfahrenden einer Fahrt; Beträge speichert die App in Cent. Der Parameter f ist die Strecke in km, p die Zahl der Mitfahrenden und k der gewünschte Satz in Cent je km und Person.

public class kostenrechner {

    public static final int maxSatz = 10;

    // berechnet den Kostenbeitrag in Euro
    public int kb(int f, int p, int k) {
        int x = k;
        if (x > 10) {
            x = 10;
        }
        return f * x * p;
    }
}

a) Nenne vier Stellen, an denen der Ausschnitt gegen die Konventionen der Sprache oder gegen die Regeln für sprechende Namen verstößt, und gib jeweils die verletzte Regel an. (4 Punkte)

Verstöße in der Kostenbeitragsberechnung
Stelle verletzte Regel
Klassenname kostenrechner Klassen werden in UpperCamelCase (Python CapWords) geschrieben: Kostenrechner bzw. besser Kostenbeitragsrechner
Konstante maxSatz Konstanten in UPPER_SNAKE_CASE; außerdem fehlt die Einheit im Namen
Methodenname kb (Python KB) rätselhafte Abkürzung, Methoden sollen ein Verb enthalten; in Python zusätzlich snake_case verlangt
Parameter f, p, k und Variable x Ein-Buchstaben-Namen verraten weder Bedeutung noch Einheit
Zahl 10 in der Bedingung magische Zahl, obwohl es die Konstante schon gibt: Wert steht doppelt im Code

Bewertung: je Stelle mit passender Regel 1 Punkt, höchstens 4 Punkte. Auch der falsche Kommentar ist als Stelle zulässig (siehe b).

b) Ben Richter fährt 18 km von Gütersloh zur Schule, nimmt drei Personen mit und möchte 12 Cent je km und Person verlangen. Berechne, was die Methode liefert, und beurteile den Kommentar über der Methode. (3 Punkte)

Der Wunschsatz von 12 Cent wird auf 10 Cent begrenzt: 18 km · 10 Cent · 3 Personen = 540. Die Methode liefert also 540 Cent, das sind 5,40 € für alle drei zusammen (1,80 € je Person).

Der Kommentar „berechnet den Kostenbeitrag in Euro“ ist falsch: Der Wert ist in Cent. Wer ihm glaubt, zeigt 540 € an. Er verschweigt außerdem, dass es die Summe aller Mitfahrenden und nicht der Beitrag je Person ist. Ein falscher Kommentar ist schädlicher als keiner; die Einheit gehört besser in den Methodennamen.

Bewertung: Rechnung mit Einheit 1 Punkt, falsche Einheit im Kommentar erkannt 1 Punkt, Begründung (irreführend, Summe statt je Person oder Einheit in den Namen) 1 Punkt.

c) Schreibe die Klasse in Java oder Python so neu, dass sie die Konventionen einhält, sprechende Namen mit Einheiten verwendet, die Konstante nutzt und einen passenden Dokumentationskommentar hat. Das Verhalten soll gleich bleiben. (6 Punkte)

public class Kostenbeitragsrechner {

    /** Höchstsatz nach den Regeln des Mitfahrlabors: 0,10 Euro je km und Person. */
    public static final int HOECHSTSATZ_CENT_JE_KM_UND_PERSON = 10;

    /**
     * Berechnet die Summe der Kostenbeiträge aller Mitfahrenden einer Fahrt.
     * Ein höherer Wunschsatz wird auf den Höchstsatz begrenzt.
     *
     * @param streckeKm Länge der Fahrt in Kilometern
     * @param anzahlMitfahrende Zahl der Mitfahrenden ohne die fahrende Person
     * @param wunschsatzCentJeKm gewünschter Satz in Cent je km und Person
     * @return Summe der Kostenbeiträge in Cent
     */
    public int berechneKostenbeitraegeCent(int streckeKm, int anzahlMitfahrende, int wunschsatzCentJeKm) {
        int satzCentJeKm = Math.min(wunschsatzCentJeKm, HOECHSTSATZ_CENT_JE_KM_UND_PERSON);
        return streckeKm * satzCentJeKm * anzahlMitfahrende;
    }

    public static void main(String[] args) {
        System.out.println(new Kostenbeitragsrechner().berechneKostenbeitraegeCent(18, 3, 12));  // 540
    }
}

Statt Math.min bzw. min ist auch die if-Abfrage mit der Konstanten richtig. Andere sprechende Namen sind gleichwertig, wenn sie Bedeutung und Einheit nennen.

Bewertung: Klassen- und Methodenname nach Konvention und sprechend 1 Punkt, Parameter mit Einheit 2 Punkte, Konstante korrekt benannt und genutzt (keine 10 mehr im Code) 1 Punkt, Dokumentationskommentar mit Einheit der Rückgabe 1 Punkt, Verhalten unverändert 1 Punkt.

Aufgabe 1.2: Kommentare in der Monitoranzeige (Basiskompetenz)

Die Flurmonitore des Mitfahrlabors zeigen offene Fahrtangebote anonym an: nur Haltepunkt, Richtung, Abfahrtszeit und freie Plätze. Oksana Melnyk aus dem Projektteam soll die Klasse, die eine Anzeigezeile erzeugt, im Review durchsehen. Die Kommentare sind mit (K1) bis (K6) markiert.

public class Monitorzeile {

    private int anzahlAngezeigt = 0;

    /** (K1) Liefert die Anzeigezeile eines Angebots für die Flurmonitore, ohne personenbezogene Daten. */
    public String erzeuge(String haltepunkt, String richtung, String abfahrt, int freiePlaetze) {
        // (K2) Haltepunkt statt Wohnadresse: Die Monitore hängen öffentlich im Foyer (Vorgabe Datenschutz)
        // (K3) alte Version: return haltepunkt + " " + abfahrt + " " + fahrerName;
        // (K4) z = Zeit
        String z = abfahrt;
        // (K5) Zähler um eins erhöhen
        anzahlAngezeigt = anzahlAngezeigt + 1;
        // (K6) geändert am 03.10.2026 von J. Wolff
        return haltepunkt + " -> " + richtung + ", " + z + " Uhr, " + freiePlaetze + " frei";
    }

    public int getAnzahlAngezeigt() {
        return anzahlAngezeigt;
    }
}

a) Beurteile jeden der sechs Kommentare: Ist er sinnvoll? Begründe Deine Entscheidung und gib an, was mit dem Kommentar geschehen soll (behalten, löschen, durch besseren Code ersetzen). (6 Punkte)

Bewertung der Kommentare in der Klasse Monitorzeile
Kommentar sinnvoll? Begründung und Vorschlag
(K1)
(K2)
(K3)
(K4)
(K5)
(K6)
Bewertung der Kommentare in der Klasse Monitorzeile (Beispiellösung)
Kommentar sinnvoll? Begründung und Vorschlag
(K1) ja Dokumentationskommentar einer öffentlichen Methode; nennt die wichtige Zusicherung „ohne personenbezogene Daten“. Behalten, ggf. um Parameter ergänzen (siehe c).
(K2) ja erklärt das Warum (Datenschutz-Vorgabe), das sich nicht aus dem Code ergibt; hält spätere Änderungen wie „Adresse ist doch praktischer“ auf. Behalten.
(K3) nein auskommentierter Code; die alte Fassung steht in der Versionsverwaltung. Löschen (sie enthielt sogar den Namen der fahrenden Person).
(K4) nein ersetzt einen fehlenden Namen. Variable in abfahrtszeit umbenennen oder abfahrt direkt verwenden, Kommentar löschen.
(K5) nein erzählt den Code nach, sagt nichts Neues. Löschen.
(K6) nein Änderungsprotokoll im Code; wer was wann geändert hat, zeigt Git (git log, git blame). Löschen.

Bewertung: je Kommentar mit stimmiger Begründung 1 Punkt.

b) Erläutere zwei Gründe, warum ein falscher oder veralteter Kommentar schädlicher sein kann als gar kein Kommentar. (2 Punkte)

  • Lesende vertrauen dem Kommentar und bauen auf einer falschen Annahme weiter, z. B. rechnen sie mit Euro statt Cent. Ohne Kommentar hätten sie den Code genauer gelesen.
  • Kommentare werden weder vom Compiler noch von Tests geprüft. Sie veralten unbemerkt, wenn der Code geändert wird, und widersprechen ihm dann; im Zweifel weiß niemand, ob Code oder Kommentar recht hat.

Bewertung: je erläutertem Grund 1 Punkt.

c) Schreibe den Kopf der Methode erzeuge mit einem vollständigen Dokumentationskommentar (Javadoc oder Docstring), der alle Parameter und die Rückgabe beschreibt. Den Rumpf darfst Du mit einer Zeile andeuten. (3 Punkte)

public class MonitorzeileDoku {

    /**
     * Erzeugt die Anzeigezeile eines Fahrtangebots für die Flurmonitore.
     * Die Zeile enthält keine personenbezogenen Daten.
     *
     * @param haltepunkt öffentlicher Haltepunkt, an dem die Fahrt beginnt
     * @param richtung Ziel bzw. Richtung der Fahrt
     * @param abfahrt Abfahrtszeit im Format HH:MM
     * @param freiePlaetze Zahl der noch freien Plätze
     * @return Anzeigezeile, z. B. "Gütersloh Hbf -> DBH-Berufskolleg, 07:05 Uhr, 2 frei"
     */
    public String erzeuge(String haltepunkt, String richtung, String abfahrt, int freiePlaetze) {
        return haltepunkt + " -> " + richtung + ", " + abfahrt + " Uhr, " + freiePlaetze + " frei";
    }
}

In Python ist auch eine Methode mit self richtig; der Aufbau des Docstrings (hier nach dem Google-Stil mit Args: und Returns:) darf abweichen, wenn alle Angaben enthalten sind.

Bewertung: Kurzbeschreibung 1 Punkt, alle vier Parameter 1 Punkt, Rückgabe 1 Punkt.

Aufgabe 2.1: Den Bestellabschluss lesbar machen (Kernkompetenz)

Im Onlineshop der CSB-Rad GmbH schließt die Klasse Shop Bestellungen ab: Sie berechnet Warenwert, Rabatt für den Gutscheincode KURIER10, Versandkosten (Paket, Spedition, Abholung) und Mehrwertsteuer, vergibt eine Bestellnummer ab CSB-1001, legt eine Bestätigungsmail in den Postausgang und speichert die Bestellung. Beträge sind Nettopreise in Cent, Gewichte in Gramm. Ein Paket darf höchstens 31,5 kg wiegen und kostet 6,90 €; ab 100 € Warenwert nach Rabatt ist der Paketversand kostenlos. Die Spedition kostet 49 € je Rad. Der Code funktioniert, ist aber schwer zu lesen. Es ist Stand 01 der Code-Kata dieser Artikelreihe; Du findest ihn samt Charakterisierungstests im Ordner src/kata/java/stand-01 bzw. src/kata/python/stand-01 des Repositorys zur Reihe.

// vollständiger Stand aus src/kata/java/stand-01: Pos.java
package de.csbrad.shop;

// Position einer Bestellung
public record Pos(String nr, String b, int p, int m, int g, boolean r) {
}
// vollständiger Stand aus src/kata/java/stand-01: Shop.java
package de.csbrad.shop;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Shop {

    private int nr = 1001;
    private final List<Object[]> liste = new ArrayList<>();
    private final List<String> mails = new ArrayList<>();
    private final Map<Integer, Integer> cache = new HashMap<>();

    // liefert die Bestellnummer
    public int getNr() {
        return nr++;
    }

    public List<Object[]> getListe() {
        return liste;
    }

    public List<String> getMails() {
        return mails;
    }

    // schliesst die Bestellung ab
    public int[] abschliessen(List<Pos> l, String v, String c, String m, boolean x) {
        int s = 0;
        for (Pos p : l) {
            if (p.m() <= 0) {
                throw new IllegalArgumentException("Fehler");
            }
            s = s + p.p() * p.m();
        }
        // Rabatt
        int r = 0;
        if (c != null && c.equals("KURIER10")) {
            int s2 = 0;
            for (Pos p : l) {
                s2 = s2 + p.p() * p.m();
            }
            r = (s2 * 10 + 50) / 100;
        }
        int vk = 0;
        String t;
        if (v.equals("PAKET")) {
            // Versand ist ab 100 Euro immer kostenlos
            int g = 0;
            for (Pos p : l) {
                if (p.r()) {
                    throw new IllegalArgumentException("Fehler");
                }
                g = g + p.g() * p.m();
            }
            int n = g / 31500;
            if (g % 31500 > 0 || n == 0) {
                n = n + 1;
            }
            if (s - r >= 10000) {
                vk = 0;
            } else {
                vk = n * 690;
            }
            t = "Paket";
        } else if (v.equals("SPEDITION")) {
            int a = 0;
            for (Pos p : l) {
                if (p.r()) {
                    a = a + p.m();
                }
            }
            if (a == 0) {
                a = 1;
            }
            vk = a * 4900;
            t = "Spedition";
        } else if (v.equals("ABHOLUNG")) {
            vk = 0;
            t = "Abholung";
        } else {
            throw new IllegalArgumentException("Fehler");
        }
        if (s == 0) {
            throw new IllegalArgumentException("Fehler");
        }
        // Preise sind brutto
        int n = s - r + vk;
        // Cache, damit die MwSt nicht jedes Mal neu berechnet wird (schneller!)
        int mw;
        if (cache.containsKey(n)) {
            mw = cache.get(n);
        } else {
            mw = (n * 19 + 50) / 100;
            cache.put(n, mw);
        }
        int z = n + mw;
        int b = getNr();
        String txt;
        if (v.equals("ABHOLUNG")) {
            txt = "An: " + m + "\nBestellung CSB-" + b + "\nGesamtbetrag: " + (z / 100) + "," + String.format("%02d", z % 100) + " EUR";
            txt = txt + "\nAbholung in der Werkstatt Bielefeld-Sennestadt";
        } else {
            txt = "An: " + m + "\nBestellung CSB-" + b + "\nGesamtbetrag: " + (z / 100) + "," + String.format("%02d", z % 100) + " EUR";
            txt = txt + "\nVersand: " + t;
        }
        mails.add(txt);
        liste.add(new Object[] {"CSB-" + b, m, l, z});
        return new int[] {s, r, vk, mw, z};
    }
}

a) Erstelle eine Tabelle aller magischen Zahlen und Zeichenketten in abschliessen und Shop, die einen Namen bekommen sollten, mit ihrer Bedeutung und einem passenden Konstantennamen. Nenne mindestens sechs. Gib außerdem eine Zahl an, die keinen eigenen Konstantennamen braucht, und begründe das kurz. (6 Punkte)

Magische Werte in Stand 01
Wert Bedeutung Konstante
1001 erste vergebene Bestellnummer ERSTE_BESTELLNUMMER
"KURIER10" einziger gültiger Gutscheincode GUTSCHEINCODE_KURIER
10 Rabatt des Gutscheins in Prozent GUTSCHEIN_RABATT_PROZENT
690 Pauschale je Paket in Cent PAKETPAUSCHALE_CENT
31500 Höchstgewicht eines Pakets in Gramm HOECHSTGEWICHT_PAKET_GRAMM
10000 Versandkostenfreigrenze in Cent (Warenwert nach Rabatt) VERSANDKOSTENFREI_AB_CENT
4900 Speditionskosten je Rad in Cent SPEDITION_JE_RAD_CENT
19 Mehrwertsteuersatz in Prozent MEHRWERTSTEUER_PROZENT

Keine eigene Konstante brauchen z. B. die 0 als Startwert einer Summe, + 1 beim Hochzählen und die 100 beim Rechnen mit Prozent bzw. beim Umrechnen von Cent in Euro: Ihre Bedeutung ergibt sich aus dem Zusammenhang. Die 50 in (… + 50) / 100 sorgt für kaufmännisches Runden; hier hilft ein kurzer Kommentar oder später eine eigene Methode mehr als ein Konstantenname. Auch die Versandarten "PAKET", "SPEDITION" und "ABHOLUNG" sind als Antwort richtig; für sie passt eine Aufzählung (Enum) besser als drei einzelne Konstanten.

Bewertung: je zwei richtige Zeilen mit Bedeutung und Namen 1 Punkt (höchstens 4 Punkte), Wert ohne Konstante mit Begründung 2 Punkte.

b) Gib für den Record bzw. die Datenklasse Pos, ihre sechs Felder und die fünf Parameter von abschliessen sprechende Namen an. Erkläre an zwei Beispielen, welche Regel für sprechende Namen der alte Name verletzt. (5 Punkte)

Neue Namen für Pos und die Parameter von abschliessen
alt neu (Java / Python)
Pos Position (Python-Modul position.py)
nr, b artikelnr, bezeichnung
p, m, g nettopreisCent / nettopreis_cent, menge, gewichtGramm / gewicht_gramm
r istRad / ist_rad
l, v, c positionen, versandart, gutscheincode
m, x kundenEmail / kunden_email, express

Beispiele für verletzte Regeln: p und g legen die Absicht nicht offen und verschweigen die Einheit (Cent, Gramm); m ist irreführend, weil es in Pos die Menge und in abschliessen die E-Mail-Adresse bezeichnet; r ist für einen Wahrheitswert nicht als Frage formuliert; Pos ist eine Abkürzung, die man erst entschlüsseln muss.

Bewertung: Namen für Record und Felder 2 Punkte, Namen für die Parameter 1 Punkt, zwei erklärte Regelverstöße je 1 Punkt.

c) Beurteile die fünf Kommentare in Shop (ohne den Kommentar zum Cache) und gib jeweils an, was mit ihnen geschehen soll. (5 Punkte)

  • liefert die Bestellnummer: wiederholt den Namen und verschweigt, dass der Zähler steigt; entfällt mit dem Umbau von getNr() (siehe d).
  • schliesst die Bestellung ab: wiederholt den Namen; ersetzen durch einen Dokumentationskommentar, der Nebenwirkungen (Mail, Speichern) und die Reihenfolge der Rückgabewerte nennt.
  • Rabatt: ersetzt einen Namen; mit der Variable rabatt überflüssig, löschen.
  • Versand ist ab 100 Euro immer kostenlos: falsch, gilt nur für Pakete und für den Warenwert nach Rabatt; ersetzen durch den Warum-Kommentar „Räder passen in kein Paket; die Freigrenze gilt für den Warenwert nach Rabatt“ und die Konstante VERSANDKOSTENFREI_AB_CENT.
  • Preise sind brutto: falsch, die Preise sind netto; ersetzen durch „Preise sind Nettopreise; die Mehrwertsteuer kommt auf Waren und Versand“.

Bewertung: je Kommentar mit Begründung und Vorschlag 1 Punkt.

d) Erläutere, warum getNr() gegen das Prinzip der geringsten Überraschung und gegen Command-Query-Separation verstößt. Beschreibe eine Situation, in der dadurch eine Bestellnummer verloren geht. (4 Punkte)

Ein Getter liest nach allgemeiner Erwartung nur einen Wert. getNr() erhöht aber bei jedem Aufruf den Zähler: Der Name verspricht eine Abfrage, die Methode ist zugleich ein Kommando (POLA). Nach Command-Query-Separation soll eine Methode entweder etwas liefern oder etwas ändern, nicht beides; getNr() tut beides.

Situation: Ein Kollege möchte im Adminbereich anzeigen, welche Nummer die nächste Bestellung bekommt, und ruft dafür getNr() auf. Die Anzeige zeigt 1001, der Zähler steht danach auf 1002. Die nächste echte Bestellung bekommt CSB-1002, die Nummer CSB-1001 fehlt in der Buchhaltung. Dasselbe passiert, wenn ein Debugger oder eine Log-Ausgabe den Getter aufruft.

Bewertung: POLA erläutert 1 Punkt, CQS erläutert 1 Punkt, Situation mit Folge 2 Punkte.

e) Baue Pos und Shop so um, dass Deine Ergebnisse aus a) bis d) umgesetzt sind. Ersetze getNr() durch eine Methode, die nicht überrascht, und benenne getListe() und getMails() nach ihrem Inhalt um. Das Verhalten darf sich nicht ändern: Führe die Charakterisierungstests vor und nach dem Umbau aus (Java: Ordner tests-charakterisierung/de nach src/test/java kopieren, mvn test; Python: python -m pytest tests-charakterisierung). Benenne mit der Umbenennen-Funktion Deiner Entwicklungsumgebung um, damit die Tests die neuen Namen mitbekommen. Begründe, warum Deine neue Methode zur Nummernvergabe privat ist und warum sie trotz Command-Query-Separation Wert und Änderung verbinden darf. (6 Punkte)

Die Beispiellösung entspricht Stand 02 der Kata (src/kata/java/stand-02 bzw. src/kata/python/stand-02); die Charakterisierungstests laufen dort in beiden Sprachen mit 16 von 16 Tests grün.

// vollständiger Stand aus src/kata/java/stand-02: 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) {
}
// vollständiger Stand aus src/kata/java/stand-02: Shop.java
package de.csbrad.shop;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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<>();
    private final Map<Integer, Integer> mehrwertsteuerCache = new HashMap<>();

    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, boolean express) {
        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)) {
            int warenwertFuerRabatt = 0;
            for (Position position : positionen) {
                warenwertFuerRabatt = warenwertFuerRabatt + position.nettopreisCent() * position.menge();
            }
            // kaufmännisch gerundet
            rabatt = (warenwertFuerRabatt * 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;
        if (mehrwertsteuerCache.containsKey(nettobetrag)) {
            mehrwertsteuer = mehrwertsteuerCache.get(nettobetrag);
        } else {
            mehrwertsteuer = (nettobetrag * MEHRWERTSTEUER_PROZENT + 50) / 100;
            mehrwertsteuerCache.put(nettobetrag, mehrwertsteuer);
        }
        int gesamtbetrag = nettobetrag + mehrwertsteuer;
        int bestellnummer = vergibBestellnummer();
        String mailtext;
        if (versandart.equals("ABHOLUNG")) {
            mailtext = "An: " + kundenEmail + "\nBestellung CSB-" + bestellnummer + "\nGesamtbetrag: " + (gesamtbetrag / 100) + ","
                    + String.format("%02d", gesamtbetrag % 100) + " EUR";
            mailtext = mailtext + "\nAbholung in der Werkstatt Bielefeld-Sennestadt";
        } else {
            mailtext = "An: " + kundenEmail + "\nBestellung CSB-" + bestellnummer + "\nGesamtbetrag: " + (gesamtbetrag / 100) + ","
                    + String.format("%02d", gesamtbetrag % 100) + " EUR";
            mailtext = mailtext + "\nVersand: " + versandbezeichnung;
        }
        postausgang.add(mailtext);
        bestellungen.add(new Object[] {"CSB-" + bestellnummer, kundenEmail, positionen, gesamtbetrag});
        return new int[] {warenwert, rabatt, versandkosten, mehrwertsteuer, gesamtbetrag};
    }
}

Begründung: vergibBestellnummer() ist privat, weil nur abschliessen Nummern vergeben darf; niemand kann sie von außen versehentlich aufrufen. Sie liefert die Nummer und erhöht den Zähler in einem Schritt, weil eine getrennte Abfrage und ein getrenntes Kommando vergessen oder bei gleichzeitigen Bestellungen verschränkt werden könnten; dann würde eine Nummer doppelt vergeben. Das Verb „vergib“ kündigt die Änderung an, sodass niemand überrascht wird (wie bei next() oder pop()).

Noch nicht umgebaut, weil es nicht zur Lesbarkeit gehört: die doppelte Warenwert-Schleife, der doppelt aufgebaute Mailkopf, der Mehrwertsteuer-Cache und der ungenutzte Parameter express. Wer diese Stellen ebenfalls verändert hat, hat nicht falsch gearbeitet, solange die Tests grün bleiben; sie sind Thema des Artikels Einfach bleiben.

Bewertung: Konstanten und Namen umgesetzt 2 Punkte, Kommentare bereinigt 1 Punkt, Nummernvergabe ohne Getter und Getter umbenannt 1 Punkt, Tests grün 1 Punkt, Begründung 1 Punkt.

Aufgabe 2.2: Freie Plätze im Mitfahrlabor (Kernkompetenz)

Im Mitfahrlabor bietet Ben Richter für Montag eine Fahrt ab Gütersloh mit drei freien Plätzen an. Die Klasse Fahrtangebot wird an zwei Stellen benutzt: Die Buchung prüft mit getFreiePlaetze(), ob noch ein Platz frei ist. Die Flurmonitore rufen getFreiePlaetze() auf, um die Zahl der freien Plätze anzuzeigen, und aktualisieren ihre Anzeige jede Minute.

public class Fahrtangebot {

    private final String haltepunkt;
    private int freiePlaetze;

    public Fahrtangebot(String haltepunkt, int plaetze) {
        this.haltepunkt = haltepunkt;
        this.freiePlaetze = plaetze;
    }

    public String getHaltepunkt() {
        return haltepunkt;
    }

    /** Liefert die freien Plätze. */
    public int getFreiePlaetze() {
        if (freiePlaetze > 0) {
            freiePlaetze = freiePlaetze - 1;  // Platz für die anfragende Person sichern
            return freiePlaetze + 1;
        }
        return -1;
    }
}

a) Beschreibe, was die Flurmonitore bei den ersten vier Aktualisierungen anzeigen, wenn in dieser Zeit niemand bucht, und welche Folge das für Aylin Kaya hat, die danach eine Mitfahrt buchen möchte. (3 Punkte)

Jeder Aufruf verbraucht einen Platz: Die Monitore zeigen nacheinander 3, 2, 1 und dann -1. Nach der dritten Aktualisierung stehen intern null freie Plätze, obwohl niemand gebucht hat. Aylins Buchung scheitert, weil getFreiePlaetze() jetzt -1 liefert, und auf dem Monitor steht eine unsinnige negative Platzzahl. Ben fährt allein, obwohl drei Personen hätten mitfahren können.

Bewertung: Anzeigefolge 1 Punkt, Plätze ohne Buchung verbraucht 1 Punkt, Folge für Aylin 1 Punkt.

b) Erläutere am Code, gegen welche zwei Prinzipien getFreiePlaetze() verstößt. Gehe dabei auch auf den Rückgabewert -1 ein. (4 Punkte)

  • Prinzip der geringsten Überraschung: Ein Getter soll nur lesen. getFreiePlaetze() reserviert nebenbei einen Platz; der Dokumentationskommentar „Liefert die freien Plätze“ verschweigt das sogar. Auch -1 überrascht: Erwartet wird eine Platzzahl, 0 wäre die natürliche Antwort auf „wie viele sind frei?“. Wer -1 nicht kennt, zeigt es an oder rechnet damit weiter.
  • Command-Query-Separation: Die Methode ist zugleich Abfrage (liefert eine Zahl) und Kommando (verringert den Zustand). Deshalb verändert jede Abfrage durch die Monitore das Angebot.

Bewertung: POLA mit Bezug zum Getter 1 Punkt, Rückgabewert -1 1 Punkt, CQS 1 Punkt, Folge (jede Abfrage ändert den Zustand) 1 Punkt.

c) Baue Fahrtangebot nach Command-Query-Separation um: getFreiePlaetze() soll nur noch abfragen und nie einen negativen Wert liefern, ein neues Kommando reservierePlatz() verringert die freien Plätze und wirft eine Ausnahme, wenn keiner mehr frei ist. (6 Punkte)

public class Fahrtangebot {

    private final String haltepunkt;
    private int freiePlaetze;

    public Fahrtangebot(String haltepunkt, int plaetze) {
        this.haltepunkt = haltepunkt;
        this.freiePlaetze = plaetze;
    }

    public String getHaltepunkt() {
        return haltepunkt;
    }

    /** Abfrage: liefert die Zahl der freien Plätze (0, wenn das Angebot voll ist) und ändert nichts. */
    public int getFreiePlaetze() {
        return freiePlaetze;
    }

    /**
     * Kommando: reserviert einen Platz für eine Mitfahrt.
     *
     * @throws IllegalStateException wenn kein Platz mehr frei ist
     */
    public void reservierePlatz() {
        if (freiePlaetze == 0) {
            throw new IllegalStateException("Kein freier Platz ab " + haltepunkt);
        }
        freiePlaetze = freiePlaetze - 1;
    }
}

Die Buchung ruft danach zuerst die Abfrage und dann das Kommando auf, z. B. if (angebot.getFreiePlaetze() > 0) { angebot.reservierePlatz(); … }. Statt IllegalStateException bzw. ValueError ist eine eigene Ausnahmeklasse ebenso richtig. In Python ist auch eine @property freie_plaetze idiomatisch.

Bewertung: Abfrage ohne Seiteneffekt und ohne -1 2 Punkte, Kommando ohne Rückgabewert 2 Punkte, Ausnahme bei vollem Angebot 1 Punkt, passende Dokumentationskommentare 1 Punkt.

d) Schreibe einen Unit-Test mit JUnit 5 oder pytest für Deine Klasse aus c), der zeigt, dass zweimaliges Abfragen nichts ändert, dass eine Reservierung genau einen Platz verbraucht und dass bei vollem Angebot eine Ausnahme kommt. (4 Punkte)

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

import org.junit.jupiter.api.Test;

class FahrtangebotTest {

    @Test
    void abfragenAendertNichts() {
        Fahrtangebot angebot = new Fahrtangebot("Gütersloh Hbf", 3);
        assertEquals(3, angebot.getFreiePlaetze());
        assertEquals(3, angebot.getFreiePlaetze());
    }

    @Test
    void reservierungVerbrauchtGenauEinenPlatz() {
        Fahrtangebot angebot = new Fahrtangebot("Gütersloh Hbf", 3);
        angebot.reservierePlatz();
        assertEquals(2, angebot.getFreiePlaetze());
    }

    @Test
    void vollesAngebotLehntReservierungAb() {
        Fahrtangebot angebot = new Fahrtangebot("Gütersloh Hbf", 1);
        angebot.reservierePlatz();
        assertThrows(IllegalStateException.class, angebot::reservierePlatz);
        assertEquals(0, angebot.getFreiePlaetze());
    }
}

Der erste Test wäre mit der alten Klasse rot geworden: Der zweite Aufruf hätte 2 statt 3 geliefert. Ein solcher Test hält den Fehler dauerhaft fern (Regressionstest).

Bewertung: je Testfall 1 Punkt, sinnvolle Testnamen und Aufbau (Arrange – Act – Assert) 1 Punkt.


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 lesbarem Code: Konventionen, Namen, Kommentare und Command-Query-Separation” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/lesbarer-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: 09.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: