Block 08 · Objektorientierung & Entwurfsmuster – Lösungen

Lösungssatz zu 08_OOP_Entwurfsmuster.html · Pseudocode im IHK-Stil, dazu lauffähige Java-Dateien im Ordner java\08_OOP_Entwurfsmuster\ (Start mit java Aufgabe_OOP_NN_k.java)
Hinweise zur Bewertung Wie in den IHK-Lösungshinweisen gilt: Andere sach- und fachgerechte Lösungen (andere Schleifenart, Java- oder C#-Syntax, Struktogramm, andere sinnvolle Bezeichner) werden mit voller Punktzahl bewertet. Syntaxfehler kosten in der Regel keine Punkte, Logikfehler schon. Bei Klassendiagrammen zählen die geforderten Elemente; die Anordnung der Kästen ist frei. Alle Java-Ausgaben unten stammen aus einem echten Programmlauf (JDK 21). Punkte gesamt: 178.

Lösungen

OOP-01 · Grundkonzepte: Klasse, Objekt, Kapselung

Aufgabe OOP-01.1 · 6 Punkte

a) Klasse und Objekt (2 P)

Die Klasse Paketfach ist der Bauplan (Datentyp): Sie legt fest, welche Attribute (Fachnummer, Größe, Inhalt, Abholcode) und welche Methoden (einlegen, entnehmen, istFrei …) jedes Paketfach hat. Ein Objekt ist ein konkretes Paketfach, das zur Laufzeit mit new erzeugt wird und eigene Attributwerte hat, z. B. Fach 7 der Station Hafenstraße, Größe "M", belegt mit Sendung KT-1001. Für die Station gibt es also eine Klasse, aber 24 Paketfach-Objekte.

Bewertung: 1 P Klasse als Bauplan/Typ mit Attributen und Methoden, 1 P Objekt als konkrete Instanz mit eigenem Zustand; ohne Bezug zum Beispiel höchstens 1 P.

b) Datenkapselung (3 P)

Die Attribute eines Paketfachs (fachNr, groesse, inhalt, abholcode) werden nach außen verborgen (Sichtbarkeit private). Andere Objekte, z. B. die Steuerung der Paketstation, können den Zustand nur über die öffentlichen Methoden lesen oder ändern (getGroesse(), istFrei(), einlegen, entnehmen). Niemand kann von außen direkt inhalt überschreiben oder den Abholcode auslesen.

Vorteile (zwei davon):

Bewertung: 1 P für das Prinzip (private Attribute, Zugriff nur über Methoden), je 1 P pro erläutertem Vorteil (max. 2).

c) Aufgabe eines Konstruktors (1 P)

Der Konstruktor wird beim Erzeugen eines Objekts (new) automatisch aufgerufen und initialisiert das neue Objekt, d. h. er setzt die Anfangswerte der Attribute.

Bewertung: 1 P.
Aufgabe OOP-01.2 · 10 Punkte

a) Ausgaben (6 P)

Nr.AusgabeBegründung
(1)1000 | Ahrens | 4.0 kge3 = e1 kopiert nur die Referenz. e3.setGewicht(4.0) ändert also dasselbe Objekt, auf das e1 zeigt.
(2)1001 | Bode | 1.0 kgZweiter Konstruktor ruft this("Bode", 1.0) auf. setGewicht(40.0) wird abgelehnt (über 31,5 kg), das Gewicht bleibt 1.0.
(3)truee1 und e3 verweisen auf dasselbe Objekt.
(4)falsee4 ist ein eigenes Objekt (Nummer 1002) – andere Identität.
(5)trueequals vergleicht Empfänger und Gewicht: beide "Ahrens" und 4.0.
(6)4Es wurden vier Objekte erzeugt (1000 bis 1003). Auch e5 zählt: Die Nummer wird vergeben, bevor das ungültige Gewicht 0.0 abgelehnt wird.

Ausgabe · Aufgabe_OOP_01_2.java

1000 | Ahrens | 4.0 kg
1001 | Bode | 1.0 kg
true
false
true
4
Bewertung: je richtige Ausgabe 1 P. Bei (1) ist der Kern das Aliasing (4.0 statt 2.5), bei (6) das Mitzählen von e5 (3 ist falsch). Die genaue Darstellung der Zahl (4.0 oder 4) ist unerheblich.

b) == gegenüber equals (2 P)

== vergleicht bei Objekten die Referenzen (Identität): e1 und e4 sind zwei verschiedene Objekte im Speicher, daher false. Die Klasse Etikett überschreibt equals so, dass inhaltlich verglichen wird (Empfänger und Gewicht) – beide Objekte haben gleiche Werte, daher true.

Bewertung: 1 P für == als Identitäts-/Referenzvergleich, 1 P für equals als inhaltlichen Vergleich (überschrieben).

c) static (2 P)

naechsteNr gehört als statisches Attribut zur Klasse und existiert nur einmal für alle Etiketten. Jeder Konstruktoraufruf liest und erhöht denselben Zähler – so entstehen fortlaufende, eindeutige Nummern, und die statische Methode getAnzahl() kann ohne Objekt aufgerufen werden.

Als normales Attribut hätte jedes Objekt eine eigene Kopie mit dem Startwert 1000: Alle Etiketten bekämen die Nummer 1000, die Nummern wären nicht mehr eindeutig. Außerdem dürfte die statische Methode getAnzahl() nicht mehr auf das Attribut zugreifen (Compilerfehler).

Bewertung: 1 P Begründung (ein Zähler für alle Objekte), 1 P Folge ohne static (jedes Objekt beginnt bei 1000, keine eindeutigen Nummern).

Datei: java\08_OOP_Entwurfsmuster\Aufgabe_OOP_01_2.java (enthält zusätzlich ein Beispiel für die NullPointerException)

Aufgabe OOP-01.3 · 16 Punkte

a) Klassendiagramm (7 P)

Paketfach- anzahlFaecher : integer- fachNr : integer {final}- groesse : string {final}- inhalt : Sendung- abholcode : string+ Paketfach(fachNr : integer, groesse : string)+ Paketfach(fachNr : integer)+ getAnzahlFaecher() : integer+ getFachNr() : integer+ getGroesse() : string+ istFrei() : boolean+ einlegen(s : Sendung, code : string) : boolean+ entnehmen(code : string) : Sendung- rang(groesse : string) : integerSendunginhalt0..11
Lösung OOP-01.3a: Klasse Paketfach mit statischem Zähler (unterstrichen) und gerichteter Assoziation zu Sendung
Bewertung: Attribute mit Datentyp und - (2 P); statisches Attribut anzahlFaecher unterstrichen oder mit {static} (1 P); beide Konstruktoren (1 P); Methoden getFachNr, getGroesse, istFrei, einlegen, entnehmen mit vollständiger Signatur (1 P); getAnzahlFaecher statisch und rang privat (1 P); Beziehung zu Sendung (1 P).
Ebenfalls richtig: inhalt nur als Rollenname an der Assoziation statt im Attributfach; Multiplizität 1 statt 0..1 (der Belegsatz kennt nur 1 und *); Aggregation (ein Fach „enthält“ eine Sendung). Keine Komposition: Die Sendung existiert unabhängig vom Fach weiter. {final} ist nicht gefordert.

Erklärung: Die Anzahl aller Fächer soll ohne Paketfach-Objekt abrufbar sein – das verlangt ein statisches Attribut und eine statische Methode. Fachnummer und Größe sind nach der Erzeugung unveränderlich, also gibt es nur Getter und keine Setter. rang ist eine interne Hilfsmethode und deshalb privat.

b) Konstruktoren (3 P)

konstruktor Paketfach(fachNr : integer, groesse : string)
  this.fachNr := fachNr
  this.groesse := groesse
  inhalt := null
  abholcode := null
  anzahlFaecher := anzahlFaecher + 1
Ende konstruktor

konstruktor Paketfach(fachNr : integer)
  this(fachNr, "M")
Ende konstruktor
Bewertung: Zuweisung von Fachnummer und Größe mit this (1 P), Zähler erhöhen (1 P), zweiter Konstruktor ruft this(fachNr, "M") auf (1 P). Die Zuweisung von null ist optional (Standardwert). Wer im zweiten Konstruktor den Code kopiert statt this(…) zu verwenden, verliert den dritten Punkt.

c) einlegen (3 P)

methode einlegen(s : Sendung, code : string) : boolean
  wenn istFrei() und rang(s.getGroesse()) <= rang(groesse) dann
    inhalt := s
    abholcode := code
    rückgabe true
  Ende wenn
  rückgabe false
Ende methode
Bewertung: Prüfung „Fach frei“ (1 P), Größenvergleich über rang mit <= (1 P), Sendung und Code speichern und passende Rückgaben (1 P). istFrei() darf durch inhalt = null ersetzt werden.

d) entnehmen (3 P)

methode entnehmen(code : string) : Sendung
  wenn nicht istFrei() und abholcode = code dann
    s : Sendung := inhalt
    inhalt := null
    abholcode := null
    rückgabe s
  Ende wenn
  rückgabe null
Ende methode
Bewertung: Prüfung auf belegtes Fach und richtigen Code (1 P), Sendung zwischenspeichern und Fach leeren (1 P), Rückgabe der Sendung bzw. null (1 P).

Typischer Fehler: erst inhalt := null und danach rückgabe inhalt – dann wird immer null zurückgegeben. Deshalb die Sendung vorher in einer lokalen Variablen merken. Ohne die Prüfung „Fach belegt“ würde bei leerem Fach null = code verglichen; in Java wäre abholcode.equals(code) dann sogar eine NullPointerException.

Java · Aufgabe_OOP_01_3.java (Klasse Paketfach)

class Paketfach {
    private static int anzahlFaecher = 0;
    private final int fachNr;
    private final String groesse;
    private Sendung inhalt;
    private String abholcode;

    public Paketfach(int fachNr, String groesse) {
        this.fachNr = fachNr;
        this.groesse = groesse;
        this.inhalt = null;
        this.abholcode = null;
        anzahlFaecher = anzahlFaecher + 1;
    }

    public Paketfach(int fachNr) {
        this(fachNr, "M");
    }

    public static int getAnzahlFaecher() {
        return anzahlFaecher;
    }

    public int getFachNr() {
        return fachNr;
    }

    public String getGroesse() {
        return groesse;
    }

    public boolean istFrei() {
        return inhalt == null;
    }

    public boolean einlegen(Sendung s, String code) {
        if (s != null && istFrei() && rang(s.getGroesse()) <= rang(groesse)) {
            inhalt = s;
            abholcode = code;
            return true;
        }
        return false;
    }

    public Sendung entnehmen(String code) {
        if (!istFrei() && abholcode.equals(code)) {
            Sendung s = inhalt;
            inhalt = null;
            abholcode = null;
            return s;
        }
        return null;
    }

    private int rang(String groesse) {
        if (groesse.equals("S")) {
            return 1;
        } else if (groesse.equals("M")) {
            return 2;
        } else if (groesse.equals("L")) {
            return 3;
        }
        return 0;
    }
}

Ausgabe des Testprogramms

Erzeugte Fächer: 3
Fach 2 hat die Größe M
f1.einlegen(KT-1001 [M]) -> false  (Sendung zu groß für S)
f2.einlegen(KT-1001 [M]) -> true
f2.einlegen(KT-1003 [S]) -> false  (Fach belegt)
f3.einlegen(KT-1002 [L]) -> true
f1.einlegen(KT-1003 [S]) -> true
f2.entnehmen("000000") -> null  (falscher Code)
f2.entnehmen("482913") -> KT-1001
f2.istFrei() -> true
f2.entnehmen("482913") -> null  (Fach leer)

OOP-02 · Vererbung, abstrakte Klassen und Interfaces

Aufgabe OOP-02.1 · 6 Punkte

a) Zuordnung (3 P)

Aussagetrifft zu aufBegründung
1. Mit new direkt Objekte erzeugenkeinesWeder abstrakte Klassen noch Interfaces sind instanziierbar.
2. Attribute mit Werten je Objektabstrakte KlasseInterfaces haben nur Konstanten.
3. Eine Klasse kann mehrere davon erweitern/implementierenInterfaceNur eine Oberklasse (extends), beliebig viele Interfaces (implements).
4. Als Datentyp verwendbarbeidez. B. List<Fahrzeug> oder Kuehlbar k
5. Konstruktor, Aufruf mit super(…)abstrakte KlasseInterfaces haben keinen Konstruktor.
6. Methoden ohne Rumpf, die implementiert werden müssenbeideabstrakte Methoden bzw. Interface-Methoden
Bewertung: je richtige Zeile ½ P.

b) Abstrakte Klasse oder Interface? (3 P)

Entscheidung: Interface Druckbar.

Bewertung: 1 P Entscheidung, je 1 P pro stichhaltigem Argument (max. 2). Die Entscheidung für eine abstrakte Klasse ist wegen der bestehenden Vererbung von Abholschein falsch (0 P für die Entscheidung).
Aufgabe OOP-02.2 · 10 Punkte

a) Ausgaben (4 P)

Fahrzeug LR-07 angelegt
Lastenrad LR-07 bereit
Fahrzeug KT-02 angelegt
Kuehlung auf 20 Grad
Bewertung: je richtige Zeile in richtiger Reihenfolge 1 P.

Erklärung: Jeder Unterklassen-Konstruktor ruft als erste Anweisung super(…) auf. Der Konstruktor von Fahrzeug läuft deshalb vollständig durch (erste Zeile), erst danach der Rest des Unterklassen-Konstruktors. Beim Kühltransporter wird temperatur erst nach dem super-Aufruf auf 20 gesetzt.

b) Fehler in der Klasse EScooter (5 P)

ZeileFehlerKorrektur
1EScooter ist nicht abstrakt, implementiert aber die abstrakte Methode kostenProKm() nicht.public double kostenProKm() { return 0.03; } ergänzen
2/3Kein Aufruf des Oberklassen-Konstruktors. Das automatische super() scheitert, weil Fahrzeug keinen parameterlosen Konstruktor hat.erste Anweisung super(kennung, 35); (Zeile 3 entfällt)
8info() ist in Fahrzeug final und darf nicht überschrieben werden.Methode entfernen oder unter anderem Namen anlegen, z. B. beschreibung() (ohne @Override)
9kennung ist in Fahrzeug private und in der Unterklasse nicht sichtbar.getKennung() verwenden (oder Attribut protected machen)
14Fahrzeug ist abstrakt und kann nicht mit new instanziiert werden.eine konkrete Unterklasse erzeugen, z. B. Fahrzeug reserve = new Lastenrad("RES-1");
Bewertung: je Fehler 1 P (½ P Fehler, ½ P Korrektur). Kleine Abweichungen bei der Zeilenangabe (z. B. 7 statt 8) sind unerheblich.

Meldungen von javac 21 für die fehlerhafte Fassung (gekürzt)

Fehler: Fahrzeug ist abstrakt, kann nicht instanziiert werden
Fehler: EScooter ist nicht abstrakt und setzt die abstrakte Methode kostenProKm() in Fahrzeug nicht außer Kraft
Fehler: Konstruktor Fahrzeug in Klasse Fahrzeug kann nicht auf die angegebenen Typen angewendet werden.
        Erforderlich: String,int  Ermittelt: keine Argumente
Fehler: info() in EScooter kann nicht info() in Fahrzeug außer Kraft setzen. Außer Kraft gesetzte Methode ist final
Fehler: kennung hat private-Zugriff in Fahrzeug
5 Fehler

Java · korrigierte Klasse (Aufgabe_OOP_02_2.java)

class EScooter extends Fahrzeug {
    public EScooter(String kennung) {
        super(kennung, 35);                       // Fehler 1 behoben
        System.out.println("E-Scooter bereit");
    }

    @Override
    public double kostenProKm() {                 // Fehler 2 behoben
        return 0.03;
    }

    public String beschreibung() {                // Fehler 3 behoben (info() ist final)
        return "E-Scooter " + getKennung();       // Fehler 4 behoben (kennung ist private)
    }
}
// Fehler 5: new Fahrzeug(...) ist nicht erlaubt -> konkrete Unterklasse instanziieren

c) Ein Objekt, zwei Typen (1 P)

Kuehltransporter erbt von Fahrzeug (ein Kühltransporter ist ein Fahrzeug) und realisiert Kuehlbar. Ein Objekt einer Klasse ist mit allen Oberklassen und allen implementierten Interfaces typkompatibel (Upcast). Über die Fahrzeug-Variable sind dann nur die Fahrzeug-Methoden aufrufbar, über die Kuehlbar-Variable nur kuehlen.

Bewertung: 1 P für Vererbung und Realisierung als Grund.

Ausgabe des gesamten Testprogramms

--- a) Konstruktorverkettung ---
Fahrzeug LR-07 angelegt
Lastenrad LR-07 bereit
Fahrzeug KT-02 angelegt
Kuehlung auf 20 Grad
--- b) korrigierte Klasse EScooter ---
Fahrzeug ES-11 angelegt
E-Scooter bereit
ES-11 (35 km), Kosten/km: 0.03
E-Scooter ES-11
Fahrzeug RES-1 angelegt
Lastenrad RES-1 bereit
RES-1 (60 km)
--- c) ein Objekt, zwei Typen ---
Fahrzeug KT-03 angelegt
Kuehlung auf 20 Grad
KT-03 (220 km) / kuehlen(-18): true
f1 instanceof Kuehlbar: false, f2 instanceof Kuehlbar: true
Aufgabe OOP-02.3 · 22 Punkte

a) Nachteile der bisherigen Umsetzung (4 P)

Bewertung: je erläutertem Nachteil 2 P (max. 4). Nur genannt, ohne Erläuterung: je 1 P.

b) Klassendiagramm (9 P)

Postsendung{abstract}- sendungsNr : string- gewicht : double+ Postsendung(sendungsNr : string, gewicht : double)+ getSendungsNr() : string+ getGewicht() : double+ berechnePorto() : double {abstract}+ maxGewicht() : double {abstract}+ istStationsfaehig() : boolean {abstract}«interface»Versicherbar+ berechneVersicherung(warenwert : double) : doubleBrief+ Brief(sendungsNr : string, gewicht : double)+ berechnePorto() : double+ maxGewicht() : double+ istStationsfaehig() : booleanPaket+ Paket(sendungsNr : string, gewicht : double)+ berechnePorto() : double+ maxGewicht() : double+ istStationsfaehig() : boolean+ berechneVersicherung(warenwert : double) : doubleExpresssendung+ Expresssendung(sendungsNr : string, gewicht : double)+ berechnePorto() : double+ maxGewicht() : double+ istStationsfaehig() : boolean+ berechneVersicherung(warenwert : double) : double
Lösung OOP-02.3b: abstrakte Oberklasse, drei Unterklassen, Interface Versicherbar nur für versicherbare Sendungsarten
Bewertung: Postsendung abstrakt gekennzeichnet und ohne typ (1 P); gemeinsame Attribute, Konstruktor und Getter nur in der Oberklasse (1 P); drei abstrakte Methoden kursiv bzw. {abstract} (1 P); drei Unterklassen mit Vererbungspfeil (Dreieck an Postsendung) (2 P); überschreibende Methoden in allen Unterklassen (1 P); Konstruktoren der Unterklassen (1 P); Interface Versicherbar mit Methode (1 P); Realisierung nur durch Paket und Expresssendung, gestrichelt mit Dreieck (1 P).
Ebenfalls richtig: Attribute # statt - mit Gettern; istStationsfaehig() als normale Methode in Postsendung (Rückgabe false), die nur Paket überschreibt. Nicht richtig: berechneVersicherung zusätzlich in Postsendung – dann hätte auch Brief diese Methode, und das Interface wäre überflüssig.

c) Klasse Paket (5 P)

klasse Paket erbt von Postsendung implementiert Versicherbar

  konstruktor Paket(sendungsNr : string, gewicht : double)
    super(sendungsNr, gewicht)
  Ende konstruktor

  methode berechnePorto() : double
    wenn getGewicht() <= 5.0 dann
      rückgabe 5.49
    Ende wenn
    rückgabe 8.99
  Ende methode

  methode maxGewicht() : double
    rückgabe 31.5
  Ende methode

  methode istStationsfaehig() : boolean
    rückgabe true
  Ende methode

  methode berechneVersicherung(warenwert : double) : double
    betrag : double := warenwert * 0.01
    wenn betrag < 2.00 dann
      betrag := 2.00
    Ende wenn
    rückgabe betrag
  Ende methode
Ende klasse
Bewertung: Klassenkopf mit Vererbung und Interface (½ P); Konstruktor mit super (1 P); berechnePorto mit Grenze „bis einschließlich 5 kg“ (1 P); maxGewicht und istStationsfaehig (1 P); berechneVersicherung mit Mindestbetrag 2,00 (1½ P). Gleichwertig: Zugriff auf ein protected-Attribut gewicht statt getGewicht().

Java · Aufgabe_OOP_02_3.java (Klasse Paket)

class Paket extends Postsendung implements Versicherbar {
    public Paket(String sendungsNr, double gewicht) {
        super(sendungsNr, gewicht);
    }

    @Override
    public double berechnePorto() {
        if (getGewicht() <= 5.0) {
            return 5.49;
        }
        return 8.99;
    }

    @Override
    public double maxGewicht() {
        return 31.5;
    }

    @Override
    public boolean istStationsfaehig() {
        return true;
    }

    @Override
    public double berechneVersicherung(double warenwert) {
        double betrag = warenwert * 0.01;
        if (betrag < 2.00) {
            betrag = 2.00;
        }
        return betrag;
    }
}

Ausgabe des Testprogramms (alle Sendungsarten, polymorph über List<Postsendung>)

Nr    Art             Gewicht  Porto   max.kg  Station
B-1   Brief            0,02    0,95     1,0  nein
B-2   Brief            0,40    1,80     1,0  nein
P-1   Paket            3,00    5,49    31,5  ja
P-2   Paket           12,00    8,99    31,5  ja
E-1   Expresssendung   2,50   14,90    20,0  nein
Versicherung (nur Objekte, die Versicherbar realisieren):
  P-1  Warenwert  150,00 -> Versicherung  2,00
  P-2  Warenwert  600,00 -> Versicherung  6,00
  E-1  Warenwert  400,00 -> Versicherung  2,00
Gegenprobe mit der bisherigen Klasse (Fallunterscheidung nach typ):
  E-1 alt: Porto 14,90, Versicherung(400) 2,00

d) Eine Oberklasse, aber mehrere Interfaces (4 P)

Java erlaubt bei Klassen nur Einfachvererbung. Würde eine Klasse von zwei Klassen erben, die beide eine Methode mit gleicher Signatur implementieren oder gleichnamige Attribute haben, wäre nicht eindeutig, welche Implementierung bzw. welches Attribut gilt; erben beide Klassen außerdem von einer gemeinsamen Oberklasse, wären deren Attribute doppelt vorhanden (Diamond-Problem).

Ein Interface enthält dagegen keine Instanzattribute und (in der klassischen Form) nur Methodensignaturen ohne Implementierung. Paket implementiert berechneVersicherung selbst – es gibt genau eine Implementierung und damit keinen Konflikt. Deshalb darf eine Klasse eine Oberklasse haben (Postsendung, „ist ein“) und zusätzlich beliebig viele Interfaces realisieren (Versicherbar, „kann versichert werden“).

Bewertung: 2 P für die Begründung des Verbots (Mehrdeutigkeit/Diamond-Problem), 2 P dafür, warum Interfaces unproblematisch sind (kein Zustand, keine konkurrierende Implementierung).

OOP-03 · Polymorphie, Überladen/Überschreiben, Generics

Aufgabe OOP-03.1 · 6 Punkte

a) Polymorphie (3 P)

Polymorphie (Vielgestaltigkeit) bedeutet, dass eine Variable vom Typ einer Oberklasse oder eines Interfaces auf Objekte verschiedener Unterklassen verweisen kann. Derselbe Methodenaufruf führt je nach Objekt zu unterschiedlichem Verhalten, weil die Unterklassen die Methode überschreiben. Welche Implementierung ausgeführt wird, wird erst zur Laufzeit anhand der tatsächlichen Klasse des Objekts entschieden (dynamische bzw. späte Bindung). Beispiel: Zustellung z := new Expresszustellung() – der Aufruf z.gebuehr() führt die Methode von Expresszustellung aus.

Bewertung: 1 P Oberklassen-Referenz auf Objekte verschiedener Unterklassen, 1 P gleicher Aufruf – unterschiedliches Verhalten durch Überschreiben, 1 P Entscheidung zur Laufzeit/dynamische Bindung. Ohne Bezug zu den Klassen aus Anlage 7 höchstens 2 P. Auch akzeptiert: Hinweis auf statische Polymorphie durch Überladen als Ergänzung.

b) Überladen oder Überschreiben? (3 P)

Nr.ErgebnisBegründung
1Überladengleiche Klasse, gleicher Name, unterschiedliche Parameterliste (integer bzw. string)
2ÜberschreibenUnterklasse definiert die geerbte Methode mit gleicher Signatur neu und ersetzt ihre Implementierung
3Überladenandere Parameterliste (zusätzlicher Parameter). Die geerbte planeTour(km) bleibt im Kuehltransporter unverändert erhalten; es kommt nur eine weitere Variante hinzu.
Bewertung: je Zeile 1 P (½ P Ergebnis, ½ P Begründung). Zeile 3 ist die Fangfrage: Obwohl die Methoden in Ober- und Unterklasse stehen, liegt kein Überschreiben vor.
Aufgabe OOP-03.2 · 10 Punkte

a) Ausgaben (6 P)

Nr.AusgabeBegründung
(1)Expressb zeigt auf eine Expresszustellung → deren überschriebene beschreibung() (dynamische Bindung).
(2)Express: 10.0 EURquittung() ist geerbt; darin werden beschreibung() und gebuehr() dynamisch gebunden → Express-Versionen; gebuehr() = super.gebuehr() + 6.00 = 4.00 + 6.00.
(3)Nacht-Express: 10.0 EURNachtzustellung überschreibt nur beschreibung(): "Nacht-" + super.beschreibung() (= "Express"). gebuehr() wird von Expresszustellung geerbt.
(4)Zustellung: 4.0 EURreines Zustellung-Objekt
(5)StandardbuchungÜberladen wird beim Übersetzen nach dem statischen Typ des Arguments ausgewählt: b ist als Zustellung deklariert → buche(Zustellung).
(6)Expressbuchungstatischer Typ von d ist Expresszustellung → die spezifischere Variante buche(Expresszustellung).

Ausgabe · Aufgabe_OOP_03_2.java

Express
Express: 10.0 EUR
Nacht-Express: 10.0 EUR
Zustellung: 4.0 EUR
Standardbuchung
Expressbuchung
Bewertung: je richtige Ausgabe 1 P. Schreibweise des Betrags (10.0, 10,00) ist unerheblich. Bei (5) ist „Expressbuchung“ der typische Fehler.

b) Aufruf von zeitfenster() über b (2 P)

Der Compiler prüft anhand des statischen Typs, welche Methoden aufgerufen werden dürfen. b ist als Zustellung deklariert, und Zustellung hat keine Methode zeitfenster() – dass das Objekt zur Laufzeit eine Expresszustellung ist, spielt für den Compiler keine Rolle. Lösung: Typ prüfen und explizit umwandeln (Downcast):

if (b instanceof Expresszustellung) {
    Expresszustellung e = (Expresszustellung) b;
    System.out.println(e.zeitfenster());
}
Bewertung: 1 P Begründung (statischer Typ), 1 P Anweisungen mit instanceof und Cast. Ohne instanceof-Prüfung ½ P. Auch akzeptiert: Variable gleich als Expresszustellung deklarieren.

c) Downcast auf ein Objekt der Oberklasse (2 P)

Übersetzen: Der Compiler akzeptiert die Anweisung, weil Expresszustellung eine Unterklasse von Zustellung ist und ein Downcast grundsätzlich möglich ist. Ausführen: a verweist auf ein reines Zustellung-Objekt, das keine Expresszustellung ist. Die Umwandlung schlägt fehl, es wird eine ClassCastException ausgelöst (ohne Behandlung bricht das Programm ab). Vermeiden lässt sich das mit einer Prüfung durch instanceof.

ClassCastException: class Zustellung cannot be cast to class Expresszustellung (Zustellung and Expresszustellung are in unnamed module of loader 'app')
a instanceof Expresszustellung: false
Bewertung: 1 P Übersetzen erlaubt, 1 P ClassCastException zur Laufzeit.

Datei: java\08_OOP_Entwurfsmuster\Aufgabe_OOP_03_2.java – Die Datei enthält die Klassen aus Anlage 7, den Programmausschnitt der Aufgabe sowie b) und c) zum Ausprobieren.

Aufgabe OOP-03.3 · 18 Punkte

a) Generische Klasse (3 P)

Eine generische Klasse hat einen Typparameter (hier T), der als Platzhalter für den Datentyp der Elemente steht. Der konkrete Typ wird erst bei der Verwendung festgelegt, z. B. Ablage<Lagerposten> oder später Ablage<Rollcontainer>; die Klasse wird nur einmal programmiert.

Vorteil gegenüber Object: Typsicherheit beim Übersetzen – in eine Ablage<Lagerposten> lässt der Compiler nur Lagerposten hinein, beim Herauslesen ist kein Cast nötig, und Typfehler fallen nicht erst zur Laufzeit (ClassCastException) auf. (Auch akzeptiert: Wiederverwendbarkeit ohne Codekopie, bessere Lesbarkeit.)

Bewertung: 2 P Erläuterung (Typparameter, Festlegung bei Verwendung), 1 P Vorteil.

b) Konstruktor und ablegen (4 P)

konstruktor Ablage(bezeichnung : string, kapazitaet : integer)
  this.bezeichnung := bezeichnung
  this.kapazitaet := kapazitaet
  elemente := new List<T>()
Ende konstruktor

methode ablegen(element : T) : boolean
  wenn elemente.size() < kapazitaet und nicht elemente.contains(element) dann
    elemente.add(element)
    rückgabe true
  Ende wenn
  rückgabe false
Ende methode
Bewertung: Konstruktor mit Zuweisungen und Erzeugen der leeren Liste (1½ P); Kapazitätsprüfung mit < (1 P); Prüfung mit contains (½ P); add und beide Rückgaben (1 P). Typischer Fehler: Liste im Konstruktor nicht erzeugen – dann führt das erste ablegen zu einer NullPointerException.

c) faelligePosten (4 P)

methode faelligePosten(ablage : Ablage<Lagerposten>, heute : Datum) : List<Lagerposten>
  ergebnis : List<Lagerposten> := new List<Lagerposten>()
  posten : List<Lagerposten> := ablage.getElemente()
  zähle i von 0 bis posten.size() - 1
    p : Lagerposten := posten.get(i)
    wenn p.istFaellig(heute) dann
      ergebnis.add(p)
    Ende wenn
  Ende zähle
  rückgabe ergebnis
Ende methode
Bewertung: neue Ergebnisliste (1 P); Durchlauf über getElemente() mit richtigen Grenzen (1 P); Aufruf von istFaellig(heute) ohne Fallunterscheidung nach der Klasse (1 P); add und Rückgabe (1 P). Eine Lösung, die mit instanceof die drei Regeln selbst nachprogrammiert, verfehlt den Sinn der Aufgabe (höchstens 2 P).

Testdaten: R-101 (Eingang 20.09. + 14 Tage = 04.10. ≤ 10.10.), Z-201 (freigegeben) und L-301 (Abholtermin = heute) sind fällig; R-102 erst ab 15.10., Z-202 nicht freigegeben, L-302 erst am 12.10.

d) Mechanismus (3 P)

Polymorphie durch dynamische (späte) Bindung. Lagerposten deklariert istFaellig als abstrakte Methode, und jede Unterklasse (Retoure, Zollposten, Lagerauftrag) überschreibt sie mit ihrer eigenen Regel. Die Variable p hat den statischen Typ Lagerposten; welche Implementierung ausgeführt wird, entscheidet die Laufzeitumgebung erst beim Aufruf anhand der tatsächlichen Klasse des Objekts (dynamischer Typ).

Bewertung: 1 P Begriff (Polymorphie/dynamische Bindung), 1 P Überschreiben der abstrakten Methode in den Unterklassen, 1 P Entscheidung zur Laufzeit nach dem tatsächlichen Objekt.

e) maximum (4 P)

methode maximum(v : Vergleicher<T>) : T
  wenn elemente.size() = 0 dann
    rückgabe null
  Ende wenn
  max : T := elemente.get(0)
  zähle i von 1 bis elemente.size() - 1
    wenn v.vergleiche(elemente.get(i), max) > 0 dann
      max := elemente.get(i)
    Ende wenn
  Ende zähle
  rückgabe max
Ende methode
Bewertung: leere Ablage → null (1 P); Startwert = erstes Element (1 P); Vergleich über v.vergleiche in richtiger Richtung (> 0 heißt „größer als das bisherige Maximum“) (1 P); Ersetzen und Rückgabe (1 P). Ein Startwert wie 0 oder null ist falsch, weil T ein beliebiger Typ ist und mit null nicht verglichen werden kann.

Java · Aufgabe_OOP_03_3.java (Ablage<T>, Vergleicher<T>, Lagerverwaltung)

class Ablage<T> {
    private String bezeichnung;
    private int kapazitaet;
    private List<T> elemente;

    public Ablage(String bezeichnung, int kapazitaet) {
        this.bezeichnung = bezeichnung;
        this.kapazitaet = kapazitaet;
        this.elemente = new ArrayList<>();
    }

    public boolean ablegen(T element) {
        if (elemente.size() < kapazitaet && !elemente.contains(element)) {
            elemente.add(element);
            return true;
        }
        return false;
    }

    public int anzahl() {
        return elemente.size();
    }

    public List<T> getElemente() {
        return elemente;
    }

    public T maximum(Vergleicher<T> v) {
        if (elemente.size() == 0) {
            return null;
        }
        T max = elemente.get(0);
        for (int i = 1; i < elemente.size(); i++) {
            if (v.vergleiche(elemente.get(i), max) > 0) {
                max = elemente.get(i);
            }
        }
        return max;
    }
}

interface Vergleicher<T> {
    int vergleiche(T a, T b);
}

class Lagerverwaltung {
    public List<Lagerposten> faelligePosten(Ablage<Lagerposten> ablage, Datum heute) {
        List<Lagerposten> ergebnis = new ArrayList<>();
        List<Lagerposten> posten = ablage.getElemente();
        for (int i = 0; i < posten.size(); i++) {
            Lagerposten p = posten.get(i);
            if (p.istFaellig(heute)) {          // dynamische Bindung
                ergebnis.add(p);
            }
        }
        return ergebnis;
    }
}

Ausgabe des Testprogramms (Testdaten aus Anlage 8, heute = 10.10.2026)

ablegen(R-101) -> true
ablegen(R-102) -> true
ablegen(Z-201) -> true
ablegen(Z-202) -> true
ablegen(L-301) -> true
ablegen(L-302) -> true
ablegen(R-101) erneut -> false  (schon enthalten)
ablegen(R-103) -> false  (Kapazität 6 erreicht)
anzahl() = 6
c) faelligePosten(regalA, 10.10.2026): R-101 Z-201 L-301 
e) maximum(GewichtVergleicher) = L-301 (18.0 kg)
Ablage<String>.maximum(nach Länge) = "Zoll ruft zurück"
maximum einer leeren Ablage = null

Die letzten Zeilen zeigen die Wiederverwendung: dieselbe Klasse als Ablage<String>, der Vergleicher als Lambda-Ausdruck, und null bei leerer Ablage.

OOP-04 · Entwurfsmuster: Grundlagen, Kategorien, Erzeugungsmuster

Aufgabe OOP-04.1 · 6 Punkte

a) Vorteile von Entwurfsmustern (4 P)

Bewertung: je beschriebenem Vorteil 2 P (max. 4). Nur ein Schlagwort ohne Beschreibung: 1 P.

b) Kategorien (2 P)

EntwurfsmusterKategorie
SingletonErzeugungsmuster
FassadeStrukturmuster
StrategieVerhaltensmuster
FabrikmethodeErzeugungsmuster
Bewertung: je richtige Zuordnung ½ P.
Aufgabe OOP-04.2 · 10 Punkte

a) Zweck des Singleton-Musters (2 P)

Das Singleton-Muster stellt sicher, dass es von der Klasse Tarifkonfiguration genau ein Objekt gibt, und bietet über die statische Methode getInstanz() einen globalen Zugriffspunkt darauf. So wird die Datei tarife.properties nur einmal gelesen, und alle Programmteile verwenden garantiert dieselben Tarifwerte.

Bewertung: 1 P genau eine Instanz, 1 P globaler Zugriff bzw. Nutzen im Beispiel (einmal laden, einheitliche Werte).

b) Vier Fehler (6 P)

ZeileFehlerKorrektur
2instanz ist ein Instanzattribut – die einzige Instanz muss aber zur Klasse gehören, damit die statische Methode darauf zugreifen kann.private static Tarifkonfiguration instanz = null;
6Der Konstruktor ist public – jede Klasse kann mit new beliebig viele Objekte erzeugen.private Tarifkonfiguration() { … }
11getInstanz() ist nicht statisch – man bräuchte bereits ein Objekt, um an das Objekt zu kommen.public static Tarifkonfiguration getInstanz()
12Bei jedem Aufruf wird ein neues Objekt erzeugt und die Datei erneut gelesen.if (instanz == null) {
  instanz = new Tarifkonfiguration();
}
Bewertung: je Fehler 1½ P (½ P Fehler mit Zeile, 1 P Korrektur). In Pseudocode genügt z. B. „- instanz : Tarifkonfiguration {static}“ bzw. „wenn instanz = null dann instanz := new Tarifkonfiguration()“.
Tarifkonfiguration- instanz : Tarifkonfiguration- grundpreisPaket : double- kgZuschlag : double- Tarifkonfiguration()+ getInstanz() : Tarifkonfiguration+ getGrundpreisPaket() : double+ getKgZuschlag() : double- ladeWert(schluessel : string) : double
Korrigierte Klasse als Klassendiagramm: privater Konstruktor, statische Elemente unterstrichen

Java · Aufgabe_OOP_04_2.java (korrigierte Klasse, threadsicher)

class Tarifkonfiguration {
    private static Tarifkonfiguration instanz = null;      // Fehler 1: static
    private double grundpreisPaket;
    private double kgZuschlag;

    private Tarifkonfiguration() {                         // Fehler 2: private
        grundpreisPaket = ladeWert("paket.grundpreis");
        kgZuschlag = ladeWert("paket.kgzuschlag");
    }

    public static synchronized Tarifkonfiguration getInstanz() {   // Fehler 3: static
        if (instanz == null) {                                     // Fehler 4: nur einmal erzeugen
            instanz = new Tarifkonfiguration();
        }
        return instanz;
    }

    public double getGrundpreisPaket() {
        return grundpreisPaket;
    }

    public double getKgZuschlag() {
        return kgZuschlag;
    }

    // ladeWert(...) wie bisher
}

c) Mehrere Threads (2 P)

Problem: Rufen zwei Threads getInstanz() gleichzeitig zum ersten Mal auf, können beide die Bedingung instanz == null als erfüllt sehen, bevor einer von ihnen das neue Objekt zugewiesen hat (Race Condition). Dann werden zwei Objekte erzeugt, die Datei wird zweimal gelesen, und Programmteile arbeiten eventuell mit unterschiedlichen Instanzen – das Singleton ist verletzt.

Lösung (eine genügt): getInstanz() als synchronized deklarieren (nur ein Thread gleichzeitig in der Methode); oder die Instanz gleich beim Laden der Klasse erzeugen (private static final Tarifkonfiguration INSTANZ = new Tarifkonfiguration();); oder in Java ein enum mit genau einem Wert verwenden.

Bewertung: 1 P Problem (gleichzeitige Prüfung auf null → mehrere Instanzen), 1 P Lösung.

Ausgabe des Testprogramms (10 Threads rufen gleichzeitig getInstanz() auf)

t1 == t2: true
Grundpreis Paket: 4,20 EUR, Zuschlag je kg: 0,35 EUR
Verschiedene Instanzen bei 10 Threads: 1
Anzahl Ladevorgänge der Datei: 1
Aufgabe OOP-04.3 · 16 Punkte

a) Vorteile der Fabrikmethode (3 P)

Bewertung: je erläutertem Vorteil 1½ P (max. 3).

b) Klassendiagramm (7 P)

Zustellservice+ informiereEmpfaenger(k : Kunde, text : string) : doubleBenachrichtigung{abstract}# ziel : string+ Benachrichtigung(ziel : string)+ senden(text : string) : boolean {abstract}+ kostenProNachricht() : double {abstract}+ erzeuge(kanal : string, ziel : string) : Benachrichtigung«use»EmailBenachrichtigung+ EmailBenachrichtigung(ziel : string)+ senden(text : string) : boolean+ kostenProNachricht() : doubleSmsBenachrichtigung+ SmsBenachrichtigung(ziel : string)+ senden(text : string) : boolean+ kostenProNachricht() : doublePushBenachrichtigung+ PushBenachrichtigung(ziel : string)+ senden(text : string) : boolean+ kostenProNachricht() : double
Lösung OOP-04.3b: abstrakte Klasse mit statischer Fabrikmethode (unterstrichen), drei Unterklassen, Zustellservice nutzt nur die Abstraktion
Bewertung: Benachrichtigung abstrakt mit # ziel : string und Konstruktor (1½ P); abstrakte Methoden senden und kostenProNachricht gekennzeichnet (1 P); statische Fabrikmethode mit beiden Parametern und Rückgabetyp Benachrichtigung, unterstrichen oder {static} (1½ P); drei Unterklassen mit Vererbungspfeil (1 P); Konstruktor und überschreibende Methoden in den Unterklassen (1 P); Zustellservice mit Methode und Beziehung zu Benachrichtigung (Abhängigkeit oder gerichtete Assoziation) (1 P). Zusätzliche Abhängigkeiten «create» von der Fabrikmethode zu den Unterklassen sind richtig, aber nicht gefordert.

c) Fabrikmethode (4 P)

methode erzeuge(kanal : string, ziel : string) : Benachrichtigung   {static}
  wenn kanal = "EMAIL" dann
    rückgabe new EmailBenachrichtigung(ziel)
  sonst wenn kanal = "SMS" dann
    rückgabe new SmsBenachrichtigung(ziel)
  sonst wenn kanal = "PUSH" dann
    rückgabe new PushBenachrichtigung(ziel)
  Ende wenn
  rückgabe null      // unbekannter Kanal
Ende methode

// Verwendung im Zustellservice – keine Fallunterscheidung mehr:
methode informiereEmpfaenger(k : Kunde, text : string) : double
  b : Benachrichtigung := Benachrichtigung.erzeuge(k.getKanal(), k.getKontakt())
  wenn b = null dann
    rückgabe 0.0
  Ende wenn
  b.senden(text)
  rückgabe b.kostenProNachricht()
Ende methode
Bewertung: je Kanal 1 P (Vergleich, Erzeugung mit ziel, Rückgabe), 1 P für null bei unbekanntem Kanal. Die Methode informiereEmpfaenger ist nicht gefordert und dient nur der Veranschaulichung.

Java · Aufgabe_OOP_04_3.java

abstract class Benachrichtigung {
    protected String ziel;

    public Benachrichtigung(String ziel) {
        this.ziel = ziel;
    }

    public abstract boolean senden(String text);

    public abstract double kostenProNachricht();

    public static Benachrichtigung erzeuge(String kanal, String ziel) {
        if (kanal.equals("EMAIL")) {
            return new EmailBenachrichtigung(ziel);
        } else if (kanal.equals("SMS")) {
            return new SmsBenachrichtigung(ziel);
        } else if (kanal.equals("PUSH")) {
            return new PushBenachrichtigung(ziel);
        }
        return null;
    }
}

class Zustellservice {
    /** Liefert die Kosten der Benachrichtigung (0, wenn keine gesendet wurde). */
    public double informiereEmpfaenger(Kunde k, String text) {
        Benachrichtigung b = Benachrichtigung.erzeuge(k.getKanal(), k.getKontakt());
        if (b == null) {
            System.out.println("  Kanal " + k.getKanal() + " von " + k.getName() + " unbekannt - keine Nachricht");
            return 0.0;
        }
        b.senden(text);
        return b.kostenProNachricht();
    }
}

Ausgabe (vier Kunden, der Kanal "FAX" ist unbekannt)

  E-Mail an ahrens@example.org: 183 Zeichen
  SMS an +49 151 0000001: 160 Zeichen (gekürzt auf max. 160)
  Push an Gerät geraet-7f3a: 183 Zeichen
  Kanal FAX von Dahl unbekannt - keine Nachricht
Kosten der Benachrichtigungen: 0,07 EUR

d) Builder (2 P)

Ein Konstruktor mit allen Angaben hätte viele, überwiegend optionale Parameter: Aufrufe wie new Etikett("KT-1", null, 0.0, null, "Garage", null) sind unlesbar und fehleranfällig (Reihenfolge, viele null-Werte), oder man bräuchte sehr viele überladene Konstruktoren. Ein Builder setzt nur die benötigten Angaben Schritt für Schritt über sprechende Methoden (.ablageort("Garage").wunschtag(…)) und erzeugt mit build() ein vollständiges, geprüftes Etikett. Der Code wird lesbarer, flexibler, und die Prüfung der Angaben steht an einer Stelle.

Bewertung: 1 P Problem des Konstruktors mit vielen optionalen Parametern, 1 P Arbeitsweise/Vorteil des Builders.

OOP-05 · Verhaltens- und Strukturmuster

Aufgabe OOP-05.1 · 6 Punkte

a) Passende Entwurfsmuster (3 P)

Nr.MusterBegründung
1AdapterEine vorhandene, nicht änderbare Klasse (SpeditionApi) wird über eine Adapterklasse an die erwartete Schnittstelle Statusquelle angepasst; der Adapter ruft holeStatusXml auf und übersetzt das Ergebnis.
2FassadeEine Klasse bietet eine einfache Methode einliefern(…) und koordiniert intern die fünf Teilsysteme.
3DecoratorJede Zusatzleistung umhüllt die Sendung, hat dieselbe Schnittstelle und erhöht den Preis; Kombinationen entstehen durch Verschachteln statt durch Unterklassen.
Bewertung: je richtigem Muster 1 P. Bei 3 ist „Dekorierer“ gleichwertig.

b) MVC im Kundenportal (3 P)

Bewertung: je Komponente 1 P.
Aufgabe OOP-05.2 · 10 Punkte

a) Ergänztes Klassendiagramm (4 P)

Paketauftrag- gewicht : double- strategie : Preisstrategie+ Paketauftrag(gewicht : double, strategie : Preisstrategie)+ setStrategie(s : Preisstrategie) : void+ berechnePreis() : double«interface»Preisstrategie+ preis(gewicht : double) : doublePrivattarif+ preis(gewicht : double) : doubleGeschaeftstarif+ preis(gewicht : double) : doubleAktionstarif+ preis(gewicht : double) : doublestrategie1
Lösung OOP-05.2a: Der Kontext Paketauftrag kennt nur das Interface; drei austauschbare Strategien
Bewertung: Methode + preis(gewicht : double) : double im Interface (1 P); Realisierung durch alle drei Tarifklassen, gestrichelt mit Dreieck am Interface (2 P, je fehlender Realisierung −1 P); gerichtete Assoziation Paketauftrag → Preisstrategie (1 P; Aggregation oder ungerichtete Assoziation ebenfalls akzeptiert).

b) Methoden preis und berechnePreis (4 P)

// Klasse Privattarif
methode preis(gewicht : double) : double
  wenn gewicht <= 2.0 dann
    rückgabe 4.99
  sonst wenn gewicht <= 10.0 dann
    rückgabe 7.49
  Ende wenn
  rückgabe 11.99
Ende methode

// Klasse Paketauftrag
methode berechnePreis() : double
  rückgabe strategie.preis(gewicht)
Ende methode
Bewertung: preis: Grenzen 2 und 10 kg jeweils einschließlich (2 P), drei richtige Rückgaben (1 P); berechnePreis: Delegation an das Strategie-Objekt ohne Fallunterscheidung (1 P).

Java · Aufgabe_OOP_05_2.java

interface Preisstrategie {
    double preis(double gewicht);
}

class Privattarif implements Preisstrategie {
    @Override
    public double preis(double gewicht) {
        if (gewicht <= 2.0) {
            return 4.99;
        } else if (gewicht <= 10.0) {
            return 7.49;
        }
        return 11.99;
    }
}

class Geschaeftstarif implements Preisstrategie {
    @Override
    public double preis(double gewicht) {
        return 3.20 + 0.25 * gewicht;
    }
}

class Aktionstarif implements Preisstrategie {
    @Override
    public double preis(double gewicht) {
        return 3.99;
    }
}

class Paketauftrag {
    private double gewicht;
    private Preisstrategie strategie;

    public Paketauftrag(double gewicht, Preisstrategie strategie) {
        this.gewicht = gewicht;
        this.strategie = strategie;
    }

    public void setStrategie(Preisstrategie strategie) {
        this.strategie = strategie;
    }

    public double berechnePreis() {
        return strategie.preis(gewicht);
    }
}

Ausgabe (Preise je Tarif, Wechsel zur Laufzeit)

Gewicht      Privattarif Geschaeftstarif    Aktionstarif
  1,5 kg            4,99            3,58            3,99
  2,0 kg            4,99            3,70            3,99
  6,0 kg            7,49            4,70            3,99
 14,0 kg           11,99            6,70            3,99
Strategie zur Laufzeit wechseln:
  6 kg, Privattarif:  7,49 EUR
  6 kg, Aktionstarif: 3,99 EUR

c) Vorteil gegenüber Unterklassen je Tarif (2 P)

Mit dem Strategie-Muster kann der Tarif eines bestehenden Auftrags zur Laufzeit gewechselt werden (setStrategie), z. B. wenn ein Kunde ein Geschäftskonto eröffnet. Bei Unterklassen müsste ein neues Objekt einer anderen Klasse erzeugt und alle Daten müssten übertragen werden. Außerdem bleiben die Tarife unabhängig von Paketauftrag: Neue Tarife erfordern keine Änderung am Auftrag, und es entsteht keine „Klassenexplosion“, wenn weitere Merkmale (z. B. Expressaufträge) mit allen Tarifen kombiniert werden müssen.

Bewertung: 2 P für einen erläuterten Vorteil (Laufzeitwechsel, keine Klassenexplosion, Wiederverwendbarkeit/Testbarkeit der Tarife, Komposition statt Vererbung).
Aufgabe OOP-05.3 · 26 Punkte

a) Funktionsweise und Begründung (4 P)

Funktionsweise: Ein Subject (hier Sendungsstatus) verwaltet eine Liste angemeldeter Observer. Interessierte Objekte (KundenApp, Hallenanzeige) melden sich mit addObserver an. Ändert sich der Zustand des Subjects, ruft es notifyObservers() auf; diese Methode ruft bei jedem angemeldeten Observer update() auf, und jeder Observer aktualisiert sich daraufhin selbst.

Begründung: Eine Statusänderung muss sofort und automatisch an mehrere, unterschiedliche Anzeigen weitergegeben werden. Weil das Subject nur das Interface Observer kennt, können später weitere Anzeigen (Kundenportal) ergänzt werden, ohne Sendungsstatus zu ändern – genau das fordert Anlage 12. Anzeigen müssen den Status nicht ständig abfragen (kein Polling).

Bewertung: 2 P Funktionsweise (Anmelden, Benachrichtigen aller Observer bei Änderung), 2 P Begründung mit Bezug zur Situation (mehrere Anzeigen, automatische Aktualisierung, Erweiterbarkeit ohne Änderung des Subjects).

b) Ergänztes Klassendiagramm (5 P)

«interface»Subject+ addObserver(o : Observer) : void+ removeObserver(o : Observer) : void+ notifyObservers() : void«interface»Observer+ update() : voidSendungsstatus- sendungsNr : string- status : string- observers : List<Observer>+ Sendungsstatus(sendungsNr : string)+ getSendungsNr() : string+ getStatus() : string+ setStatus(status : string) : void+ addObserver(o : Observer) : void+ removeObserver(o : Observer) : void+ notifyObservers() : voidKundenApp- quelle : Sendungsstatus- letzteMeldung : string+ KundenApp(quelle : Sendungsstatus)+ anzeigen() : void+ update() : voidHallenanzeige- quelle : Sendungsstatus- hallenNr : integer- zeile : string+ Hallenanzeige(quelle : Sendungsstatus, hallenNr : integer)+ anzeigen() : void+ update() : voidobservers*quelle1quelle1
Lösung OOP-05.3b: Realisierungen, Assoziation zum Observer-Interface mit Multiplizität *, Rückverweis quelle für die Pull-Variante
Bewertung: Realisierung Sendungsstatus → Subject (1 P); Realisierung KundenApp und Hallenanzeige → Observer (1 P); gerichtete Assoziation (oder Aggregation) Sendungsstatus → Observer mit * (1 P); Assoziation von KundenApp und Hallenanzeige zu Sendungsstatus (Attribut quelle) (1 P); ergänzte Methoden addObserver, removeObserver, notifyObservers in Sendungsstatus und update() in beiden Anzeigen (1 P).

c) Vorteil der Kopplung über das Interface (3 P)

Bewertung: je erläutertem Aspekt 1 P (max. 3).

d) Sequenzdiagramm (8 P)

Clientcreatestatus : Sendungsstatusnew KundenApp(status)app : KundenAppaddObserver(app)new Hallenanzeige(status, 3)halle : HallenanzeigeaddObserver(halle)setStatus("In Zustellung")notifyObservers()update()getStatus()"In Zustellung"anzeigen()update()getStatus()"In Zustellung"anzeigen()
Lösung OOP-05.3d: Schritte 4 bis 8 – setStatus, notifyObservers als Selbstaufruf, update mit getStatus (Pull) und anzeigen bei beiden Observern, Rückantworten
Bewertung: setStatus("In Zustellung") vom Client an status (1 P); Selbstaufruf notifyObservers() mit eigenem Aktivierungsbalken (1 P); update() an app (1 P); getStatus() von app an status mit Antwort (1 P); anzeigen() als Selbstaufruf von app (1 P); danach update() an halle (1 P); getStatus() mit Antwort und anzeigen() bei halle (1 P); Rückantworten von update und setStatus bis zum Client (1 P).
Ebenfalls akzeptiert: ein loop-Fragment „[für alle Observer]“ um einen update-Aufruf, wenn erkennbar ist, dass beide Observer benachrichtigt werden; fehlende geschachtelte Aktivierungsbalken bei getStatus kosten keine Punkte.

e) setStatus, notifyObservers und update (4 P)

// Klasse Sendungsstatus
methode setStatus(status : string) : void
  this.status := status
  notifyObservers()
Ende methode

methode notifyObservers() : void
  zähle i von 0 bis observers.size() - 1
    observers.get(i).update()
  Ende zähle
Ende methode

// Klasse KundenApp
methode update() : void
  letzteMeldung := quelle.getStatus()
  anzeigen()
Ende methode
Bewertung: setStatus: Status speichern und danach notifyObservers() aufrufen (1 P); notifyObservers: Schleife über alle Observer mit richtigen Grenzen (1 P) und Aufruf von update() (1 P); update: Status über quelle.getStatus() holen und anzeigen() aufrufen (1 P).

Java · Aufgabe_OOP_05_3.java (Interfaces, Subject, Observer)

interface Subject {
    void addObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObservers();
}

interface Observer {
    void update();
}

class Sendungsstatus implements Subject {
    private String sendungsNr;
    private String status;
    private List<Observer> observers;

    public Sendungsstatus(String sendungsNr) {
        this.sendungsNr = sendungsNr;
        this.status = "Angekündigt";
        this.observers = new ArrayList<>();
    }

    public String getSendungsNr() {
        return sendungsNr;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
        notifyObservers();
    }

    @Override
    public void addObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        observers.remove(o);
    }

    @Override
    public void notifyObservers() {
        for (int i = 0; i < observers.size(); i++) {
            observers.get(i).update();
        }
    }
}

class KundenApp implements Observer {
    private Sendungsstatus quelle;
    private String letzteMeldung;

    public KundenApp(Sendungsstatus quelle) {
        this.quelle = quelle;
        quelle.addObserver(this);
    }

    @Override
    public void update() {
        letzteMeldung = quelle.getStatus();     // Pull: Daten selbst abholen
        anzeigen();
    }

    public void anzeigen() {
        System.out.println("  KundenApp:       " + letzteMeldung);
    }

    public String getLetzteMeldung() {
        return letzteMeldung;
    }
}

class Hallenanzeige implements Observer {
    private Sendungsstatus quelle;
    private int hallenNr;
    private String zeile;

    public Hallenanzeige(Sendungsstatus quelle, int hallenNr) {
        this.quelle = quelle;
        this.hallenNr = hallenNr;
        quelle.addObserver(this);
    }

    @Override
    public void update() {
        zeile = quelle.getStatus();             // Pull
        anzeigen();
    }

    public void anzeigen() {
        System.out.println("  Hallenanzeige " + hallenNr + ": " + zeile);
    }
}

Ausgabe (zwei Statusänderungen, Abmelden der Hallenanzeige, dritte Änderung, Push-Vergleich)

  KundenApp:       In Zustellung
  Hallenanzeige 3: In Zustellung
  KundenApp:       In Paketstation eingelegt
  Hallenanzeige 3: In Paketstation eingelegt
-- Hallenanzeige meldet sich ab --
  KundenApp:       Abgeholt
Letzte Meldung in der App: Abgeholt
-- f) Push-Variante zum Vergleich --
  Push-Observer erhält: KT-4712 -> Im Sortierzentrum

f) Push-Variante (2 P)

Bei der Push-Variante schickt das Subject die geänderten Daten gleich mit: Die Methode erhält Parameter, z. B. update(sendungsNr : string, status : string), und notifyObservers() ruft observers.get(i).update(sendungsNr, status) auf. Der Observer muss den Status nicht mehr mit getStatus() abholen (der Rückaufruf im Sequenzdiagramm entfällt) und braucht keine Referenz quelle mehr. Nachteil: Alle Observer erhalten alle Daten, und bei zusätzlichen Daten ändert sich die Schnittstelle.

Bewertung: 1 P geänderte Signatur/Daten als Parameter, 1 P Wegfall von getStatus() bzw. der Referenz auf das Subject.
interface PushObserver {
    void update(String sendungsNr, String status);   // Daten werden mitgeschickt
}

class StatusQuellePush {
    private String sendungsNr;
    private String status;
    private List<PushObserver> observers = new ArrayList<>();

    public StatusQuellePush(String sendungsNr) {
        this.sendungsNr = sendungsNr;
    }

    public void addObserver(PushObserver o) {
        observers.add(o);
    }

    public void setStatus(String status) {
        this.status = status;
        for (int i = 0; i < observers.size(); i++) {
            observers.get(i).update(sendungsNr, status);
        }
    }
}