MINT lernen

Übung — AFB III (Verallgemeinern und Reflektieren)

Zehn offene Aufgaben: Entwürfe mit Assoziation und Vererbung bewerten, erweitern und begründen — oft ist mehr als eine Lösung richtig.

Dein Fortschritt:
0 / 0 Aufgaben
3

Aufgabenblock — AFB III

Entwerfen statt nur nachvollziehen: Modelle für neue Anforderungen erweitern, Vererbungshierarchien und Beziehungsrichtungen bewerten, Kapselungsentscheidungen beurteilen und Entwürfe begründet verwerfen. Formuliere deine Antwort vollständig, bevor du die Lösung aufklappst — oft sind auch andere gut begründete Entwürfe richtig.

A1
Modell erweitern: Hotelzimmer mit mehreren Gästen
AFB III

Bisher kann ein Hotelzimmer nur einen Gast aufnehmen:

Zimmer
  • - nummer: Ganzzahl
  • - betten: Ganzzahl
  • - gast: Gast
  • c Zimmer(nummer: Ganzzahl, betten: Ganzzahl)
  • + einchecken(g: Gast): Wahrheitswert
  • + auschecken()
  • + istFrei(): Wahrheitswert
belegt von0..10..1
Gast
  • - name: Zeichenkette
  • c Gast(name: Zeichenkette)
  • + getName(): Zeichenkette
public boolean einchecken(Gast g) {
    if (gast == null) {
        gast = g;
        return true;
    }
    return false;
}

Neue Anforderung: In einem Zimmer dürfen so viele Gäste wohnen, wie es Betten hat. Die Rezeption möchte außerdem zu einem Gastnamen die Zimmernummer finden (Klasse Hotel mit einer Reihung aller Zimmer).

Erweitern Sie das Klassendiagramm und implementieren Sie die geänderte Klasse Zimmer sowie in Hotel die Methode findeZimmerVon(name: Zeichenkette): Ganzzahl (−1, wenn der Gast nicht im Hotel ist). Begründen Sie Ihre Entscheidung, ob Gast sein Zimmer kennen soll.

Ansatz: Aus „ein Gast“ wird „bis zu betten Gäste“: Ersetze das Attribut gast durch eine Reihung vom Typ Gast und einen Zähler belegung. Die Länge der Reihung übernimmt die Rolle von betten.
Suche: Hotel läuft über alle Zimmer (mit Null-Prüfung), jedes Zimmer prüft mit einer eigenen Methode, ob der Name bei ihm wohnt. So muss Hotel die Reihung im Zimmer nicht kennen.
Richtung: Wenn auch der Gast sein Zimmer kennt, müssen beim Ein- und Auschecken zwei Referenzen konsistent gehalten werden. Lohnt sich das hier?
Vollständige Lösung
Hotel
  • - zimmer: Reihung vom Typ Zimmer
  • c Hotel(anzahlZimmer: Ganzzahl)
  • + findeZimmerVon(name: Zeichenkette): Ganzzahl
hat1*
Zimmer
  • - nummer: Ganzzahl
  • - gaeste: Reihung vom Typ Gast
  • - belegung: Ganzzahl
  • c Zimmer(nummer: Ganzzahl, betten: Ganzzahl)
  • + getNummer(): Ganzzahl
  • + einchecken(g: Gast): Wahrheitswert
  • + auschecken()
  • + istFrei(): Wahrheitswert
  • + getBelegung(): Ganzzahl
  • + wohntHier(name: Zeichenkette): Wahrheitswert
belegt von0..1*
Gast
  • - name: Zeichenkette
  • c Gast(name: Zeichenkette)
  • + getName(): Zeichenkette
public class Zimmer {
    private int nummer;
    private Gast[] gaeste;
    private int belegung;

    public Zimmer(int nummer, int betten) {
        this.nummer = nummer;
        gaeste = new Gast[betten];
        belegung = 0;
    }

    public int getNummer() {
        return nummer;
    }

    public boolean einchecken(Gast g) {
        if (g == null || belegung == gaeste.length) {
            return false;
        }
        gaeste[belegung] = g;
        belegung = belegung + 1;
        return true;
    }

    public void auschecken() {
        for (int i = 0; i < belegung; i++) {
            gaeste[i] = null;
        }
        belegung = 0;
    }

    public boolean istFrei() {
        return belegung == 0;
    }

    public int getBelegung() {
        return belegung;
    }

    public boolean wohntHier(String name) {
        for (int i = 0; i < belegung; i++) {
            if (gaeste[i].getName().equals(name)) {
                return true;
            }
        }
        return false;
    }
}
public int findeZimmerVon(String name) {
    for (int i = 0; i < zimmer.length; i++) {
        if (zimmer[i] != null && zimmer[i].wohntHier(name)) {
            return zimmer[i].getNummer();
        }
    }
    return -1;
}

Die öffentliche Schnittstelle von Zimmer bleibt bis auf zwei neue Methoden gleich — Programmteile, die nur einchecken und istFrei nutzen, müssen nicht geändert werden. Das Attribut betten entfällt, weil gaeste.length dieselbe Information enthält (keine doppelte Datenhaltung).

Gast kennt Zimmer? Nicht nötig: Die einzige Anfrage „Wo wohnt X?“ beantwortet Hotel durch Suchen. Eine zweite Referenz müsste bei jedem Ein- und Auschecken mitgepflegt werden und könnte inkonsistent werden. Erst wenn sehr häufig vom Gast aus gefragt wird (z. B. Zimmerrechnung vom Gastprofil aus), wäre die Gegenrichtung sinnvoll.

A2
Ist ein Quadrat ein Rechteck?
AFB III

Für ein Geometrieprogramm schlägt ein Teammitglied vor, Quadrat als Unterklasse von Rechteck zu modellieren — „ein Quadrat ist schließlich ein Rechteck“. Damit ein Quadrat quadratisch bleibt, setzen die überschriebenen Setter jeweils beide Seiten.

Rechteck
  • # breite: Fließkommazahl
  • # hoehe: Fließkommazahl
  • + setBreite(b: Fließkommazahl)
  • + setHoehe(h: Fließkommazahl)
  • + flaeche(): Fließkommazahl
Quadrat
    • + setBreite(b: Fließkommazahl)
    • + setHoehe(h: Fließkommazahl)

    Eine andere Klasse enthält die Methode:

    public static boolean pruefe(Rechteck r) {
        r.setBreite(4);
        r.setHoehe(5);
        return r.flaeche() == 20;
    }

    Bewerten Sie den Entwurf. Beziehen Sie den Aufruf pruefe(new Quadrat()) ein und schlagen Sie eine Alternative vor.

    Ansatz: Verfolge pruefe(new Quadrat()): Welche Setter laufen, welche Fläche entsteht?
    Erwartung: Wer mit einer Rechteck-Variable arbeitet, erwartet, dass sich Breite und Höhe unabhängig setzen lassen.
    Alternative: Braucht man überhaupt veränderbare Seiten? Oder eine gemeinsame, allgemeinere Oberklasse?
    Vollständige Lösung

    Ablauf: Wegen dynamischer Bindung laufen die Setter des Quadrats: setBreite(4) setzt beide Seiten auf 4, setHoehe(5) beide auf 5. Die Fläche ist 25, pruefe liefert false — obwohl die Methode für jedes „echte“ Rechteck true liefert.

    Bewertung: Mathematisch ist ein Quadrat ein Rechteck, im Programm aber nicht: Ein Rechteck-Objekt verspricht, dass Breite und Höhe unabhängig veränderbar sind. Das Quadrat bricht dieses Versprechen. Jede Methode, die mit Rechteck arbeitet, kann durch ein Quadrat überrascht werden — Polymorphie wird zur Fehlerquelle. Der ist-ein-Test reicht also nicht, wenn die Unterklasse das Verhalten der Oberklasse einschränkt.

    Alternative: gemeinsame Oberklasse Form mit flaeche(); Rechteck (zwei Seiten) und Quadrat (eine Seite) als Geschwister. Oder unveränderliche Objekte ohne Setter — dann ist ein Quadrat gefahrlos ein Rechteck.

    A3
    Kapselung beurteilen: Getränkeautomat
    AFB III

    Für einen Getränkeautomaten in der Schule schlägt ein Teammitglied diese Klasse vor:

    Getraenkeautomat
    • - preis: Ganzzahl
    • - bestand: Ganzzahl
    • - guthaben: Ganzzahl
    • - einnahmen: Ganzzahl
    • c Getraenkeautomat()
    • + getPreis(): Ganzzahl
    • + setPreis(preis: Ganzzahl)
    • + getBestand(): Ganzzahl
    • + setBestand(bestand: Ganzzahl)
    • + getGuthaben(): Ganzzahl
    • + setGuthaben(guthaben: Ganzzahl)
    • + getEinnahmen(): Ganzzahl
    • + setEinnahmen(einnahmen: Ganzzahl)

    Anforderungen: Der Automat fasst höchstens 40 Flaschen, nimmt nur Münzen zu 10, 20, 50, 100 und 200 Cent an, Preise sind Vielfache von 10 Cent. Nur der Hausmeister leert die Kasse.

    Beurteilen Sie den Entwurf im Hinblick auf die Datenkapselung. Schlagen Sie eine verbesserte Schnittstelle vor und implementieren Sie sie.

    Ansatz: Alle Attribute sind privat — formal gekapselt. Frage aber bei jedem Setter: Welche ungültigen Zustände kann man von außen damit herstellen?
    Idee: Ersetze „Werte setzen“ durch Vorgänge aus der Wirklichkeit: Flaschen nachfüllen, Münze einwerfen, Getränk kaufen, Kasse leeren.
    Prüfungen: Wer prüft die Kapazität, die Münzsorte, den Preis? Genau diese Methoden.
    Vollständige Lösung

    Urteil: Der Entwurf ist nur scheinbar gekapselt. Private Attribute mit ungeprüften Settern für alles verhalten sich praktisch wie öffentliche Attribute: setGuthaben(5000) erzeugt Geld aus dem Nichts, setBestand(-3) oder setBestand(99) einen unmöglichen Bestand, setEinnahmen(0) lässt Geld verschwinden. Außerdem gehen fachliche Zusammenhänge verloren: Ein Kauf muss Bestand, Guthaben und Einnahmen gemeinsam ändern — mit einzelnen Settern ist das Sache des Aufrufers und damit fehleranfällig.

    Getraenkeautomat
    • - preis: Ganzzahl
    • - bestand: Ganzzahl
    • - guthaben: Ganzzahl
    • - einnahmen: Ganzzahl
    • - kapazitaet: Ganzzahl
    • c Getraenkeautomat(preis: Ganzzahl, kapazitaet: Ganzzahl)
    • + setPreis(preis: Ganzzahl)
    • + nachfuellen(flaschen: Ganzzahl): Ganzzahl
    • + einwerfen(muenze: Ganzzahl): Wahrheitswert
    • + kaufen(): Wahrheitswert
    • + rueckgabe(): Ganzzahl
    • + kassieren(): Ganzzahl
    • + getPreis(): Ganzzahl
    • + getBestand(): Ganzzahl
    • + getGuthaben(): Ganzzahl
    public class Getraenkeautomat {
        private int preis;
        private int bestand;
        private int guthaben;
        private int einnahmen;
        private int kapazitaet;
    
        public Getraenkeautomat(int preis, int kapazitaet) {
            this.preis = 150;
            setPreis(preis);
            this.kapazitaet = kapazitaet;
            bestand = 0;
            guthaben = 0;
            einnahmen = 0;
        }
    
        public void setPreis(int preis) {
            if (preis > 0 && preis % 10 == 0) {
                this.preis = preis;
            }
        }
    
        public int nachfuellen(int flaschen) {
            int passt = kapazitaet - bestand;
            if (flaschen > passt) {
                flaschen = passt;
            }
            if (flaschen > 0) {
                bestand = bestand + flaschen;
                return flaschen;
            }
            return 0;
        }
    
        public boolean einwerfen(int muenze) {
            if (muenze == 10 || muenze == 20 || muenze == 50 || muenze == 100 || muenze == 200) {
                guthaben = guthaben + muenze;
                return true;
            }
            return false;
        }
    
        public boolean kaufen() {
            if (bestand > 0 && guthaben >= preis) {
                bestand = bestand - 1;
                guthaben = guthaben - preis;
                einnahmen = einnahmen + preis;
                return true;
            }
            return false;
        }
    
        public int rueckgabe() {
            int betrag = guthaben;
            guthaben = 0;
            return betrag;
        }
    
        public int kassieren() {
            int betrag = einnahmen;
            einnahmen = 0;
            return betrag;
        }
    
        public int getPreis() {
            return preis;
        }
    
        public int getBestand() {
            return bestand;
        }
    
        public int getGuthaben() {
            return guthaben;
        }
    }

    Nur setPreis bleibt als Setter — mit Prüfung. Kein Getter für einnahmen: Der Betrag wird nur beim Leeren durch kassieren() herausgegeben. Dass nur der Hausmeister kassieren darf, lässt sich mit Sichtbarkeiten nicht ausdrücken — das muss die Bedienoberfläche regeln (z. B. Schlüsselschalter). Im Konstruktor wird zuerst ein Standardpreis gesetzt, damit ein ungültiger Startpreis nicht zu 0 führt.

    A4
    Ein Ladenetz für E-Autos
    AFB III

    „Ein Betreiber hat Ladesäulen mit einer Nummer und einem Preis pro kWh. Kunden haben einen Namen; Firmenkunden sind Kunden, die einen Rabatt in Prozent erhalten. Ein Ladevorgang findet an genau einer Säule für genau einen Kunden statt und speichert die geladenen kWh. Der Betreiber kennt alle Ladevorgänge und soll seinen Umsatz berechnen.“

    Entwickeln Sie ein Klassendiagramm und implementieren Sie die Kostenberechnung so, dass die Klasse Ladevorgang nicht unterscheiden muss, ob ein Firmenkunde lädt. Berechnen Sie den Umsatz für: Jo lädt 20 kWh an Säule A (0,50 €/kWh), die Zett GmbH (Rabatt 20 %) lädt 40 kWh an Säule B (0,40 €/kWh).

    €
    Klassen: Betreiber, Saeule, Kunde, Firmenkunde, Ladevorgang. Wer erbt, wer kennt wen?
    Rabatt: Gib Kunde eine Methode preisfaktor(), die 1.0 liefert, und lass Firmenkunde sie überschreiben.
    Rechnung: 20 · 0,50 · 1,0 + 40 · 0,40 · 0,8
    Vollständige Lösung
    Betreiber
    • - vorgaenge: DynArray<Ladevorgang>
    • + umsatz(): Fließkommazahl
    vorgaenge*
    Ladevorgang
    • - kwh: Fließkommazahl
    • - saeule: Saeule
    • - kunde: Kunde
    • + kosten(): Fließkommazahl
    kunde1
    Kunde
    • - name: Zeichenkette
    • + preisfaktor(): Fließkommazahl

    Dazu Ladevorgang ——saeule——> 1 Saeule (Nummer, Preis pro kWh) und Firmenkunde (- rabatt: Ganzzahl, überschreibt preisfaktor()) als Unterklasse von Kunde.

    public class Firmenkunde extends Kunde {
        private int rabatt;                       // in Prozent
        public Firmenkunde(String name, int rabatt) {
            super(name);
            this.rabatt = rabatt;
        }
        @Override
        public double preisfaktor() { return 1 - rabatt / 100.0; }
    }
    public class Ladevorgang {
        private Saeule saeule;
        private Kunde kunde;
        private double kwh;
        // Konstruktor setzt alle drei Attribute
        public double kosten() {
            return kwh * saeule.getPreisProKwh() * kunde.preisfaktor();
        }
    }

    umsatz() summiert kosten() über alle Ladevorgänge: 20 · 0,50 = 10,00 € und 40 · 0,40 · 0,8 = 12,80 €, zusammen 22,80 €. Ladevorgang kennt nur den Typ Kunde; welcher Faktor gilt, entscheidet das Objekt. Achtung: rabatt / 100 wäre bei int ganzzahlig 0 — deshalb 100.0.

    A5
    Teamaufteilung und Schnittstelle: Leihrad-App
    AFB III

    Ein Dreierteam soll für den städtischen Fahrradverleih eine App entwickeln. Anforderungen: An jeder Station stehen bis zu 12 Leihräder. Eine Kundin kann an einer Station ein freies Rad ausleihen und es an einer beliebigen Station zurückgeben. Pro Kundin ist höchstens ein Rad gleichzeitig ausgeliehen. Die Leihdauer wird in Minuten erfasst; jede angefangene Stunde kostet 2 €.

    Entwerfen Sie ein Klassendiagramm, das als Schnittstelle zwischen drei Teammitgliedern dienen kann, und legen Sie fest, wer welche Klasse implementiert. Geben Sie zu jeder Klasse einen Testfall an, der vor dem Programmieren vereinbart wird, und begründen Sie, warum die Schnittstelle vor der Implementierung feststehen muss.

    Ansatz: Kandidaten sind Station, Leihrad und Kundin (oder Kunde). Pro Person eine Klasse — dann muss genau festgelegt sein, welche Methoden die anderen aufrufen dürfen.
    Schnittstelle: Für jeden Aufruf „über die Klassengrenze“ brauchst du Name, Parameter, Rückgabetyp und das Verhalten im Fehlerfall (z. B. Station leer → null).
    Testfälle: Ein Testfall besteht aus Ausgangszustand, Aufruf und erwartetem Ergebnis — mindestens ein Grenzfall ist Pflicht.
    Vollständige Lösung
    Kundin
    • - name: Zeichenkette
    • - rad: Leihrad
    • - kostenCent: Ganzzahl
    • c Kundin(name: Zeichenkette)
    • + ausleihen(s: Station, minute: Ganzzahl): Wahrheitswert
    • + zurueckgeben(s: Station, minute: Ganzzahl): Wahrheitswert
    • + getKostenCent(): Ganzzahl
    leiht0..10..1
    Leihrad
    • - nummer: Ganzzahl
    • - startMinute: Ganzzahl
    • c Leihrad(nummer: Ganzzahl)
    • + getNummer(): Ganzzahl
    • + starten(minute: Ganzzahl)
    • + beenden(minute: Ganzzahl): Ganzzahl
    steht an0..120..1
    Station
    • - name: Zeichenkette
    • - raeder: Reihung vom Typ Leihrad
    • - anzahl: Ganzzahl
    • c Station(name: Zeichenkette)
    • + getAnzahlFrei(): Ganzzahl
    • + radEntnehmen(): Leihrad
    • + radAbstellen(r: Leihrad): Wahrheitswert

    Verhalten (Teil des Vertrags): radEntnehmen() liefert null, wenn die Station leer ist. radAbstellen liefert false bei 12 Rädern. beenden(minute) liefert die Leihdauer in Minuten. ausleihen scheitert, wenn die Kundin schon ein Rad hat oder die Station leer ist; zurueckgeben berechnet die Kosten mit 200 Cent pro angefangener Stunde.

    Aufteilung: Person A Station, Person B Leihrad, Person C Kundin (sie nutzt die Methoden der beiden anderen und trägt die Ablauflogik).

    Testfälle (vorher vereinbart): Station: leere Station, radEntnehmen() → null; Station mit 12 Rädern, radAbstellen(r) → false. Leihrad: starten(600), beenden(661) → 61. Kundin: Ausleihe um Minute 600, Rückgabe um 661 → getKostenCent() = 400 (zwei angefangene Stunden); zweites ausleihen ohne Rückgabe → false.

    Begründung: Alle drei programmieren gleichzeitig. Person C ruft radEntnehmen() auf, bevor Person A es fertig hat — das geht nur, wenn Name, Parameter, Rückgabe und Fehlerverhalten verbindlich festgelegt sind. Ändert jemand eigenmächtig eine Signatur, passt der Quelltext beim Zusammenführen nicht mehr. Die vorher vereinbarten Tests prüfen, ob sich jede Klasse an den Vertrag hält.

    A6
    Eine neue Form ergänzen
    AFB III

    Ein Zeichenprogramm verwaltet Formen polymorph: Form hat flaeche() (liefert 0), Rechteck und Kreis überschreiben sie. Die Klasse Zeichnung kennt beliebig viele Formen. Implementieren Sie eine neue Unterklasse Dreieck (Grundseite g, Höhe h) und die Methode gesamtflaeche() der Klasse Zeichnung. Berechnen Sie die Gesamtfläche einer Zeichnung aus einem Rechteck 3 × 4, einem Kreis mit r = 1 und einem Dreieck mit g = 6, h = 2 (auf zwei Nachkommastellen).

    FE
    Dreieck: Konstruktor mit zwei Parametern, flaeche() überschreiben: g * h / 2.
    Liste: Schleife über die ArrayList<Form>, jede Fläche aufsummieren — ohne Fallunterscheidung.
    Vollständige Lösung
    public class Dreieck extends Form {
        private double g;
        private double h;
        public Dreieck(double g, double h) {
            this.g = g;
            this.h = h;
        }
        @Override
        public double flaeche() {
            return g * h / 2;
        }
    }
    public double gesamtflaeche() {          // in Zeichnung
        double summe = 0;
        for (int i = 0; i < formen.size(); i++) {
            summe = summe + formen.get(i).flaeche();
        }
        return summe;
    }

    12 + π · 1² + 6 · 2 / 2 = 12 + 3,14 + 6 ≈ 21,14. Entscheidend: gesamtflaeche() musste für das Dreieck nicht geändert werden — die neue Klasse fügt sich über die überschriebene Methode ein.

    A7
    Richtung einer Beziehung: Buslinie und Haltestelle
    AFB III

    Für die Fahrgastinformation eines Verkehrsverbunds gibt es zwei Anforderungen: (1) Die App zeigt zu einer Linie alle Haltestellen in Fahrtrichtung. (2) Die Anzeigetafel an jeder Haltestelle listet alle Linien, die dort halten.

    Drei Entwürfe stehen zur Wahl:

    • A: Nur Buslinie kennt eine Reihung ihrer Haltestellen.
    • B: Nur Haltestelle kennt eine Reihung ihrer Buslinien.
    • C: Beide kennen einander.

    Erörtern Sie die drei Entwürfe im Hinblick auf beide Anforderungen, entscheiden Sie sich begründet und implementieren Sie für Ihre Entscheidung die Methode, mit der eine Haltestelle zu einer Linie hinzugefügt wird.

    Ansatz: Prüfe für jeden Entwurf, wie Anforderung (1) und (2) erfüllt werden können. Was müsste man bei A tun, um die Linien an einer Haltestelle herauszufinden?
    Konsistenz: Bei C gibt es zwei Referenzen für dieselbe Tatsache. Wie stellst du sicher, dass nie nur eine Seite eingetragen wird?
    Trick: Die Linie kann sich selbst mit this an die Haltestelle übergeben: h.linieEintragen(this);
    Vollständige Lösung

    A erfüllt (1) direkt; für (2) müsste die Haltestelle alle Linien des Verbunds durchsuchen — dazu braucht es eine zentrale Verwaltung aller Linien, und die Anzeige wird bei vielen Linien langsam. B erfüllt (2) direkt, aber (1) nicht: Die Reihenfolge der Halte einer Linie ist nirgends gespeichert. C erfüllt beide Anforderungen direkt, erkauft das aber mit doppelter Datenhaltung: Beide Seiten müssen immer zusammen geändert werden.

    Entscheidung: C, weil beide Anforderungen häufig und zeitkritisch sind (Anzeigetafel). Das Konsistenzproblem wird gelöst, indem es nur einen öffentlichen Weg gibt, einen Halt einzutragen: haltHinzufuegen in Buslinie trägt automatisch auch die Gegenrichtung ein.

    public class Buslinie {
        private String nummer;
        private Haltestelle[] halte;
        private int anzahlHalte;
    
        public Buslinie(String nummer, int maxHalte) {
            this.nummer = nummer;
            halte = new Haltestelle[maxHalte];
            anzahlHalte = 0;
        }
    
        public String getNummer() {
            return nummer;
        }
    
        public boolean haltHinzufuegen(Haltestelle h) {
            if (h == null || anzahlHalte == halte.length) {
                return false;
            }
            halte[anzahlHalte] = h;
            anzahlHalte = anzahlHalte + 1;
            h.linieEintragen(this);
            return true;
        }
    }
    public void linieEintragen(Buslinie b) {
        for (int i = 0; i < anzahlLinien; i++) {
            if (linien[i] == b) {
                return;
            }
        }
        if (anzahlLinien < linien.length) {
            linien[anzahlLinien] = b;
            anzahlLinien = anzahlLinien + 1;
        }
    }

    Die Doppelprüfung in linieEintragen verhindert, dass Linie 4 zweimal an der Tafel steht, wenn sie (z. B. als Ringlinie) eine Haltestelle zweimal anfährt. Eine Schwäche bleibt: linieEintragen ist öffentlich und könnte auch allein aufgerufen werden — das muss im Team als Regel vereinbart werden.

    A8
    Wer kennt wen? — AGs der Schule
    AFB III

    Für die AG-Verwaltung einer Schule gilt: Ein Schüler kann in beliebig vielen AGs sein, eine AG hat höchstens 20 Mitglieder. Es gibt zwei Vorschläge: (A) nur AG ——mitglieder——> 0..20 Schueler; (B) zusätzlich Schueler ——ags——> * AG, also Navigation in beide Richtungen. Diskutieren Sie beide Vorschläge im Hinblick auf die Funktionen „Mitgliederliste einer AG drucken“ und „Stundenplan eines Schülers mit allen AGs anzeigen“.

    Funktionen: Welche Richtung braucht welche Funktion?
    Konsistenz: Was muss bei Vorschlag B beim Ein- und Austreten passieren?
    Vollständige Lösung

    Vorschlag A: Die Mitgliederliste ist direkt verfügbar. Für den Stundenplan müsste man alle AGs durchsuchen, ob der Schüler darin vorkommt — bei vielen AGs aufwendig, aber machbar, sofern es eine Klasse gibt, die alle AGs kennt. Vorteil: Jede Information steht nur an einer Stelle, Widersprüche sind ausgeschlossen.

    Vorschlag B: Beide Funktionen sind direkt möglich. Nachteil: Jede Beziehung ist doppelt gespeichert. eintreten und austreten müssen beide Listen gemeinsam ändern; vergisst eine Methode eine Seite, zeigt der Stundenplan AGs, in denen der Schüler gar nicht mehr ist.

    Ergebnis: Wird der Stundenplan häufig gebraucht, ist B sinnvoll — dann mit genau einer Methode, z. B. ag.aufnehmen(s), die intern auch s.ags ergänzt. Wird er selten gebraucht, genügt A mit Suche.

    A9
    Warteschlange durch Vererbung?
    AFB III

    Ein Entwickler möchte für die Warteschlange einer Arztpraxis Arbeit sparen: public class Warteschlange extends ArrayList<Patient> — „dann habe ich add, remove und size gleich geschenkt“. Die Praxis verlangt: Neue Patienten kommen immer ans Ende, aufgerufen wird immer der vorderste. Nehmen Sie Stellung zu diesem Entwurf.

    ist-ein-Test: Ist eine Warteschlange eine beliebige Liste — mit allen Operationen?
    Geerbtes: Welche geerbten Methoden erlauben Dinge, die in der Praxis verboten sind?
    Vollständige Lösung

    Der Entwurf spart kurzfristig Code, ist aber abzulehnen. Durch die Vererbung erbt die Warteschlange alle öffentlichen Methoden der Liste, z. B. add(0, p) (vordrängeln), remove(3) (jemanden aus der Mitte entfernen) oder set(i, p). Die Regeln der Praxis lassen sich damit von außen umgehen; die Warteschlange kann ihren Zustand nicht schützen (Datenkapselung). Der ist-ein-Test scheitert: Eine Warteschlange ist keine beliebige Liste, sie hat eine Liste.

    Besser: Assoziation bzw. Attribut private ArrayList<Patient> wartende; und nur die erlaubten Methoden anstellen(p) (add), aufrufen() (remove(0)) und getAnzahl(). So entspricht die Klasse der Datenstruktur Schlange aus Kapitel 5.

    A10
    Reicht eine Reihung der Länge 100?
    AFB III

    Für Verein ——mitglieder——> * Mitglied behauptet ein Mitschüler: „Eine Reihung new Mitglied[100] reicht völlig, so viele Mitglieder hat kein Schulverein — und Reihungen sind einfacher als Listen.“ Widerlegen Sie die Behauptung, dass diese Umsetzung der Kardinalität * gleichwertig ist. Geben Sie außerdem an, wie viele Plätze der Reihung bei 7 Mitgliedern null sind.

    Ansatz: Was verspricht *, was leistet eine Reihung fester Länge?
    Gegenbeispiel: Was passiert beim 101. Mitglied — und was beim Durchlaufen der Reihung?
    Vollständige Lösung

    Gegenbeispiel: * bedeutet „beliebig viele“. Beim 101. Mitglied gibt es keinen Platz mehr; mitglieder[100] = m führt zu einer ArrayIndexOutOfBoundsException, oder das Mitglied wird abgewiesen — die Anforderung ist verletzt. Damit ist die Behauptung widerlegt.

    Außerdem ist die Reihung auch vorher nicht gleichwertig: Die Anzahl muss in einem zusätzlichen Attribut mitgezählt werden, bei 7 Mitgliedern sind 93 Plätze null, jede Schleife muss sie überspringen, und beim Austreten entstehen Lücken, die man durch Verschieben schließen muss. Eine ArrayList (im Abitur DynArray) übernimmt genau das. Eine Reihung ist nur bei einer festen Obergrenze wie 0..20 die passende Umsetzung.