MINT üben

Objektorientierte Modellierung

Zehn offene Aufgaben: Modelle erweitern, Kapselung beurteilen, Schnittstellen im Team entwerfen und Lösungsvarianten vergleichen.

Dein Fortschritt:
0 / 0 Aufgaben
3

Aufgabenblock — AFB III

Entwerfen statt nur nachvollziehen: Modelle für neue Anforderungen erweitern, Kapselungsentscheidungen beurteilen, Schnittstellen und Testfälle für ein Team festlegen und Varianten gegeneinander abwägen. 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
hat1n
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..1n
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
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.

A3
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.