Zum Inhalt springen
Zurück zur Themenübersicht

Praktische Informatik

Objektorientierung: Kapselung, Vererbung, Polymorphie

Wie man neuen Code hinzufügt, ohne bestehenden zu ändern, und woran man erkennt, dass Vererbung die falsche Wahl ist.

Benötigte Grundlagen

Dieses Vorwissen brauchst du für das Kapitel. Schau kurz nach, wenn dir etwas davon nicht mehr präsent ist, sonst leg direkt los.

Einführung

Ein Zeichenprogramm kennt Kreise, Rechtecke und Dreiecke. Nun soll ein Fünfeck dazukommen.

In einem Programm ohne Objektorientierung muss man dafür jede Stelle suchen, an der Formen behandelt werden, und überall einen Fall ergänzen. Vergisst man eine, entsteht ein Fehler, der oft erst viel später auffällt.

Mit Objektorientierung schreibt man eine neue Klasse und ändert am bestehenden keine Zeile. Wie das möglich ist, ist der Inhalt dieses Kapitels. Der Schlüssel heißt Polymorphie, und er ist der eigentliche Gewinn des ganzen Ansatzes.

Das kannst du nach diesem Kapitel

  • Kapselung erläutern und die Sichtbarkeitsstufen begründen.

  • Vererbung anwenden und Ober- von Unterklasse unterscheiden.

  • Überschreiben von Methoden erklären.

  • Polymorphie an einem Beispiel erläutern und ihren Nutzen benennen.

  • begründet zwischen Vererbung und Komposition wählen.

Kurz aufgefrischt

Vorausgesetzt werden Klasse, , , Methode und das Klassendiagramm aus Objektorientierte Modellierung.

Neu sind hier die drei Mittel, die aus der Modellierung eine tragfähige Bauweise für große Programme machen: Kapselung, Vererbung und Polymorphie.

Kapselung

Eine Klasse legt fest, welche ihrer Bestandteile von außen erreichbar sind:

KennzeichenSichtbarkeitBedeutung
−-privatnur innerhalb der Klasse
++öffentlichfür alle sichtbar
#\#geschütztauch für Unterklassen

Üblich ist: privat, Methoden öffentlich. Der Zugriff läuft über Methoden.

Warum dieser Umweg? Sieh dir den Unterschied an:

konto.kontostand = -5000        // direkt: nichts prüft etwas

konto.abheben(5000)             // über Methode: die Klasse prüft

Im ersten Fall kann jede Stelle des Programms jeden Wert setzen, auch einen unsinnigen. Im zweiten entscheidet die Klasse selbst, ob der Vorgang zulässig ist.

🔴 Der eigentliche Gewinn liegt tiefer, als „vor Fehlern schützen". Die Klasse gibt eine Zusage nach außen: Der Kontostand unterschreitet nie den Verfügungsrahmen. Solange alle Zugriffe über Methoden laufen, gilt diese Zusage immer, ohne dass irgendjemand außerhalb darauf achten muss. Man muss den Rest des Programms nicht kennen, um sicher zu sein.

Das ist derselbe Gedanke wie beim abstrakten : außen die zugesagten Operationen, innen die frei wählbare Umsetzung. Wechselt man später die interne Speicherung, bleibt der benutzende unverändert.

Kapselung: außen die Zusage, innen die Umsetzung

Konto- kontostand : Zahl- rahmen : Zahl+ einzahlen(betrag)+ abheben(betrag)+ stand()

Lies die Vorzeichen, sie sind die ganze Aussage: Beide tragen ein Minus, sind also von außen nicht erreichbar; alle drei Methoden tragen ein Plus. Damit gibt es keinen Weg an der Klasse vorbei, wer den Kontostand ändern will, muss abheben\texttt{abheben} aufrufen, und dort prüft die Klasse selbst, ob der Verfügungsrahmen das hergibt. 🔴 Der Gewinn ist größer als „vor Fehlern schützen“. Die Klasse gibt eine Zusage nach außen: Der Kontostand unterschreitet nie den Rahmen. Solange jeder Zugriff über die Methoden läuft, gilt sie immer. Man muss den Rest des Programms nicht kennen, um sicher zu sein. Wäre kontostand\texttt{kontostand} öffentlich, müsste man dafür jede Stelle prüfen, die ihn berührt, und beim nächsten neuen Programmteil wieder von vorn. Es ist derselbe Gedanke wie beim abstrakten : außen die zugesagten Operationen, innen die frei wählbare Umsetzung.

Vererbung

Oft sind mehrere Klassen einander ähnlich. Ein Schüler und ein Lehrer haben beide Name und Geburtsdatum, unterscheiden sich aber in anderem.

Die Vererbung zieht das Gemeinsame in eine Oberklasse; die Unterklassen übernehmen es und ergänzen ihr Eigenes.

        ┌─────────────────┐
        │     Person      │
        │ - name          │
        │ - geburtsdatum  │
        │ + alter()       │
        └────────△────────┘
                 │
       ┌─────────┴─────────┐
┌──────┴──────┐    ┌───────┴──────┐
│   Schüler   │    │    Lehrer    │
│ - klasse    │    │ - faecher    │
│ + zeugnis() │    │ + stundenplan()│
└─────────────┘    └──────────────┘

Das Dreieck zeigt zur Oberklasse. Ein der Klasse Schüler hat damit name\texttt{name}, geburtsdatum\texttt{geburtsdatum}, alter()\texttt{alter()}, klasse\texttt{klasse} und zeugnis()\texttt{zeugnis()}.

Die Prüffrage für jede Vererbung lautet: „Ist ein Schüler eine Person?" Ja. Dann ist Vererbung richtig.

Überschreiben

Eine Unterklasse darf eine geerbte Methode ersetzen. Das nennt man Überschreiben.

Klasse Form:
    flaeche():  gib 0 zurück

Klasse Kreis erbt von Form:
    flaeche():  gib 3.14159 * radius * radius     // überschreibt

Klasse Rechteck erbt von Form:
    flaeche():  gib laenge * breite               // überschreibt

Wichtig ist der Unterschied zum Überladen: Überschreiben ersetzt eine geerbte Methode gleichen Namens und gleicher Parameter in der Unterklasse. Überladen bedeutet, mehrere Methoden gleichen Namens mit verschiedenen Parametern in derselben Klasse anzubieten. Das eine hat mit Vererbung zu tun, das andere nicht.

Polymorphie

Jetzt der entscheidende Punkt des Kapitels.

Polymorphie heißt: Ein einer Unterklasse darf überall dort stehen, wo die Oberklasse erwartet wird, und beim Aufruf einer überschriebenen Methode wird die Fassung der tatsächlichen Klasse ausgeführt.

formen = [ Kreis(5), Rechteck(3,4), Kreis(2) ]

für jede f in formen:
    ausgeben(f.flaeche())

Die weiß nicht, welche Form sie gerade vor sich hat. Sie ruft flaeche()\texttt{flaeche()} auf, und jedes Objekt antwortet mit seiner eigenen Berechnung.

🔴 Der Gewinn zeigt sich beim Erweitern. Kommt ein Dreieck dazu, schreibt man:

Klasse Dreieck erbt von Form:
    flaeche():  gib 0.5 * grundseite * hoehe

Und ändert an der Schleife keine Zeile. Der bestehende funktioniert weiter, weil er nie nach der konkreten Klasse gefragt hat.

Vergleiche das mit der Alternative ohne Polymorphie:

für jede f in formen:
    wenn f ist Kreis:       ausgeben(kreisflaeche(f))
    sonst wenn f ist Rechteck: ausgeben(rechteckflaeche(f))
    sonst wenn f ist Dreieck:  ausgeben(dreiecksflaeche(f))
    ...

Diese Fallunterscheidung steht erfahrungsgemäß nicht an einer Stelle, sondern an einem Dutzend: beim Zeichnen, beim Speichern, beim Berechnen des Umfangs. Jede neue Form verlangt eine Ergänzung an jeder dieser Stellen, und jede vergessene ist ein Fehler.

Man kann den Nutzen in einem Satz fassen: Polymorphie ersetzt Fallunterscheidungen über den Typ durch die Wahl der Klasse. Statt zu fragen „was bist du?", sagt man „tu, was du kannst".

Dreimal derselbe Methodenname

Form+ flaeche()Kreis- radius+ flaeche()Rechteck- laenge- breite+ flaeche()Dreieck- grundseite- hoehe+ flaeche()

Achte darauf, was in allen vier Kästen gleich ist und was verschieden. Gleich ist der Methodenname flaeche()\texttt{flaeche()}. Er steht viermal da. Verschieden sind die , denn ein Kreis rechnet aus seinem Radius, ein Rechteck aus Länge und Breite. Genau daraus entsteht die Polymorphie: Weil jede Unterklasse dieselbe Methode überschreibt, darf eine über eine Liste von Formen laufen und blind flaeche()\texttt{flaeche()} aufrufen; ausgeführt wird jedes Mal die Fassung der tatsächlichen Klasse. 🔴 Der Nutzen zeigt sich beim Erweitern, und der ist im Bild ablesbar: Ein viertes Kästchen unter Form\texttt{Form} zu hängen ändert an der Schleife keine Zeile. Die Alternative ohne Polymorphie wäre eine Fallunterscheidung „wenn Kreis … sonst wenn Rechteck …“, und die steht erfahrungsgemäß nicht an einer Stelle, sondern an einem Dutzend, beim Zeichnen, beim Speichern, beim Umfang. Jede neue Form verlangt dann eine Ergänzung an jeder dieser Stellen, und jede vergessene ist ein Fehler.

Derselbe Aufruf, drei Antworten

Oberklasse Formflaeche()Kreis Ar = 5flaeche()→ 78,5Rechteck3 und 4flaeche()→ 12Kreis Br = 2flaeche()→ 12,6Ein Aufruf, drei Rechnungen: π·5²,3·4 und π·2².

Oben der Bauplan, unten drei und jedes antwortet auf denselben Aufruf mit seiner eigenen Rechnung. Rechne die drei Werte nach: 3,14159⋅52=78,53{,}14159 \cdot 5^2 = 78{,}5, 3⋅4=123 \cdot 4 = 12 und 3,14159⋅22=12,63{,}14159 \cdot 2^2 = 12{,}6. Die , die diese Liste durchgeht, weiß von alledem nichts; sie fragt nicht „was bist du?“, sondern sagt „tu, was du kannst“. 🔴 Beachte auch den Unterschied zwischen den beiden Kreisen: Sie sind dieselbe Klasse und liefern trotzdem verschiedene Ergebnisse, weil sie verschiedene Werte tragen. Polymorphie heißt also nicht „jedes Objekt ist anders“, sondern: Welche Fassung einer Methode läuft, entscheidet die Klasse des Objekts, nicht die Stelle, an der der Aufruf steht.

Abstrakte Klassen

Bei Form\texttt{Form} fällt etwas auf: Was soll flaeche()\texttt{flaeche()} dort eigentlich zurückgeben? Die 0 oben ist ein Notbehelf, denn eine „Form" ohne nähere Angabe hat keine Fläche.

Eine abstrakte Klasse löst das. Sie schreibt vor, dass jede Unterklasse eine bestimmte Methode besitzen muss, gibt aber selbst keine Umsetzung an, und von ihr lassen sich keine erzeugen.

abstrakte Klasse Form:
    abstrakt flaeche()          // keine Umsetzung, Pflicht für Unterklassen
    beschreibung():             // normale geerbte Methode
        gib "Fläche: " + flaeche()

Das ist mehr als Kosmetik: Eine Unterklasse, die flaeche()\texttt{flaeche()} vergisst, wird schon vom Übersetzer beanstandet, statt später eine sinnlose 0 zu liefern.

Vererbung oder Komposition?

Der häufigste Entwurfsfehler ist Vererbung an der falschen Stelle. Die Unterscheidung ist einfach:

BeziehungMittelBeispiel
ist einVererbungEin Kreis ist eine Form.
hat einKompositionEin Auto hat einen Motor.

Ein Auto ist kein Motor, also darf Auto\texttt{Auto} nicht von Motor\texttt{Motor} erben. Stattdessen bekommt Auto\texttt{Auto} ein vom Typ Motor\texttt{Motor}; das nennt man Komposition.

Ein verbreiteter Fehlgriff ist auch Quadrat\texttt{Quadrat} erbt von Rechteck\texttt{Rechteck}. Geometrisch stimmt es, und trotzdem geht es schief: Ein Rechteck erlaubt, Länge und Breite unabhängig zu setzen, ein Quadrat nicht. Die Unterklasse kann die Zusagen der Oberklasse also nicht einhalten. Die Prüfung „ist ein" allein reicht deshalb nicht; man muss zusätzlich fragen, ob die Unterklasse überall dort einsetzbar ist, wo die Oberklasse steht.

Der häufigste Entwurfsfehler

ist einhat einDie PrüffrageIst ein Kreiseine Form? Ja.Hat ein Autoeinen Motor? Ja.Das MittelVererbungKompositionWas man bautKreis erbt vonForm undübernimmt allesAuto bekommt einAttribut vom TypMotorUnd „ist ein“ allein genügt nicht:Ein Quadrat IST ein Rechteck unddarf trotzdem nicht davon erben.

Die Prüffrage in der ersten Zeile entscheidet fast jeden Fall: Ein Auto ist kein Motor, also darf Auto\texttt{Auto} nicht von Motor\texttt{Motor} erben; es hat einen, bekommt also ein dieses Typs. 🔴 Die Fußzeile ist der Grund, warum die Frage allein trotzdem nicht reicht, und sie ist der berühmteste Fallstrick des Themas. Ein Quadrat ist geometrisch zweifellos ein Rechteck, und trotzdem geht die Vererbung schief: Ein Rechteck sagt zu, dass man Länge und Breite unabhängig setzen kann, ein Quadrat kann diese Zusage nicht einhalten. Wer also ein Quadrat dort einsetzt, wo ein Rechteck erwartet wird, bekommt Überraschungen. Die vollständige Prüfung lautet deshalb: Ist die Unterklasse überall dort einsetzbar, wo die Oberklasse steht, ohne deren Zusagen zu brechen?

Die drei Mittel im Zusammenspiel

MittelLeistet
Kapselungschützt Zusagen über den inneren
Vererbungvermeidet doppelten für Gemeinsames
Polymorphieerlaubt Erweiterung ohne Änderung des Bestehenden

Die Reihenfolge ist keine Rangfolge, aber die dritte Zeile ist der Grund, aus dem sich der Ansatz durchgesetzt hat.

Polymorphie beim Erweitern

Ein Programm berechnet die Gesamtfläche einer Liste von Formen. Zeige, was beim Hinzufügen eines Dreiecks zu ändern ist, mit und ohne Polymorphie.

  1. 1

    Mit Polymorphie, der bestehende :

    abstrakte Klasse Form:
        abstrakt flaeche()
    
    Klasse Kreis erbt von Form:
        flaeche(): gib 3.14159 * r * r
    
    Klasse Rechteck erbt von Form:
        flaeche(): gib a * b
    
    gesamtflaeche(formen):
        summe = 0
        für jede f in formen:
            summe = summe + f.flaeche()
        gib summe
    
  2. 2

    Die Erweiterung: eine neue Klasse, sonst nichts.

    Klasse Dreieck erbt von Form:
        flaeche(): gib 0.5 * g * h
    
  3. 3

    An gesamtflaeche\texttt{gesamtflaeche} ändert sich keine Zeile. Die hat nie gefragt, welche Form vorliegt; sie ruft flaeche()\texttt{flaeche()} auf, und das antwortet selbst.

  4. 4

    Ohne Polymorphie sähe die Funktion so aus:

    gesamtflaeche(formen):
        summe = 0
        für jede f in formen:
            wenn f ist Kreis:          summe = summe + kreisflaeche(f)
            sonst wenn f ist Rechteck: summe = summe + rechteckflaeche(f)
        gib summe
    
  5. 5

    Hier muss ein weiterer Zweig eingefügt werden. Und dieselbe Fallunterscheidung steht typischerweise auch in zeichnen\texttt{zeichnen}, umfang\texttt{umfang} und speichern\texttt{speichern}; jede vergessene Stelle ist ein Fehler, der erst später auffällt.

  6. 6

    Die Bilanz: Polymorphie verschiebt das Wissen darüber, wie eine Fläche berechnet wird, von den vielen benutzenden Stellen in die eine Klasse, die es betrifft. Genau das macht Programme erweiterbar.

Mit Polymorphie genügt eine neue Klasse; ohne sie muss jede Fallunterscheidung im ganzen Programm ergänzt werden.

Vererbung oder Komposition entscheiden

Prüfe für diese Paare, ob Vererbung oder Komposition richtig ist: (1) Auto und Motor, (2) Sparkonto und Konto, (3) Buch und Seite, (4) Quadrat und Rechteck.

  1. 1

    Das Verfahren: Man setzt beide Sätze und schaut, welcher stimmt. „X ist ein Y" → Vererbung. „X hat ein Y" → Komposition.

  2. 2

    (1) Auto und Motor: „Ein Auto ist ein Motor" ist falsch. „Ein Auto hat einen Motor" stimmt. → Komposition: Auto\texttt{Auto} bekommt ein vom Typ Motor\texttt{Motor}.

  3. 3

    (2) Sparkonto und Konto: „Ein Sparkonto ist ein Konto" stimmt. → Vererbung: Sparkonto\texttt{Sparkonto} erbt von Konto\texttt{Konto} und ergänzt etwa den Zinssatz.

  4. 4

    (3) Buch und Seite: „Ein Buch ist eine Seite" ist falsch. „Ein Buch hat Seiten" stimmt, und zwar viele. → Komposition, hier mit einer Liste von Seiten.

  5. 5

    (4) Quadrat und Rechteck: „Ein Quadrat ist ein Rechteck" stimmt geometrisch. Trotzdem ist Vererbung hier problematisch.

  6. 6

    Warum: Ein Rechteck sagt zu, dass sich Länge und Breite unabhängig setzen lassen. Ein Quadrat kann diese Zusage nicht halten; setzt man dort die Länge, muss sich die Breite mitändern. , der mit einem Rechteck rechnet, verhält sich mit einem Quadrat also plötzlich anders.

  7. 7

    Folgerung: Die Frage „ist ein" ist notwendig, aber nicht hinreichend. Zusätzlich muss gelten, dass die Unterklasse überall einsetzbar ist, wo die Oberklasse steht. Sauberer wäre hier eine gemeinsame Oberklasse Viereck\texttt{Viereck} ohne die Zusage unabhängiger Seiten.

(1) und (3) Komposition, (2) Vererbung, (4) trotz „ist ein" keine Vererbung, weil das Quadrat die Zusagen des Rechtecks nicht halten kann.

Typischer Fehler

„Vererbung ist praktisch, um wiederzuverwenden. Wenn zwei Klassen ähnliche Methoden brauchen, lasse ich die eine von der anderen erben."

Wiederverwendung ist eine Folge richtiger Vererbung, nicht ihr Grund. Wer sie zum Grund macht, baut Beziehungen, die inhaltlich falsch sind.

Ein bekanntes Beispiel: Stapel\texttt{Stapel} erbt von Liste\texttt{Liste}, weil eine Liste schon Einfügen und Löschen kann. Was passiert dann? Der erbt auch einfuegenAn(index)\texttt{einfuegenAn(index)} und loescheAn(index)\texttt{loescheAn(index)}. Damit lässt sich mitten im Stapel etwas herausnehmen, und genau das sollte die Struktur ja verhindern. Die LIFO-Zusage ist zerstört.

Richtig ist Komposition: Stapel\texttt{Stapel} hat eine Liste als privates und bietet nach außen nur ablegen\texttt{ablegen} und entnehmen\texttt{entnehmen} an. Der Code wird genauso wiederverwendet, aber die Zusage bleibt.

Die Prüfung besteht aus zwei Fragen, und beide müssen mit Ja beantwortet sein:

Ist X wirklich ein Y? Kann X überall dort stehen, wo Y erwartet wird, ohne dass sich etwas anders verhält?

Ein weiteres Warnzeichen ist tiefe Verschachtelung. Erbt A\texttt{A} von B\texttt{B}, das von C\texttt{C}, das von D\texttt{D} erbt, wird schwer nachvollziehbar, woher eine Methode stammt und welche Fassung gilt. Als Faustregel gilt: Vererbung sparsam und flach einsetzen, Komposition großzügig.

Übung 1

leicht

a) Was bedeuten die Kennzeichen −-, ++ und #\# im ? b) Warum sollten privat sein? c) Entscheide jeweils zwischen Vererbung und Komposition: Fahrrad und Rad, Hund und Tier, Computer und Festplatte, Roman und Buch.

Tipp anzeigen

Zu c): Sprich beide Sätze aus, „ist ein" und „hat ein".

Lösung anzeigen

a) −- privat (nur innerhalb der Klasse), ++ öffentlich (für alle sichtbar), #\# geschützt (auch für Unterklassen).

b) Damit der Zugriff über Methoden läuft und die Klasse prüfen kann, ob ein Wert zulässig ist. Dadurch kann sie eine Zusage über ihren einhalten, etwa dass ein Kontostand nie unter den Verfügungsrahmen fällt. Ohne Kapselung müsste jede Stelle des Programms selbst darauf achten.

c) Fahrrad und Rad: „Ein Fahrrad hat Räder" → Komposition. Hund und Tier: „Ein Hund ist ein Tier" → Vererbung. Computer und Festplatte: „Ein Computer hat eine Festplatte" → Komposition. Roman und Buch: „Ein Roman ist ein Buch" → Vererbung.

Detaillierte Schritterklärung anzeigen

Hier wird jeder Schritt einzeln erklärt, vor allem, warum er gemacht wird.

Erklärungstiefe

✦ Empfohlen: Standard – Die normale Erklärungstiefe passt zum Einstieg.

  1. 1

    a) Die drei Sichtbarkeitszeichen

    −- bedeutet privat (nur innerhalb der Klasse), ++ bedeutet öffentlich (für alle sichtbar), #\# bedeutet geschützt (auch für Unterklassen).

  2. 2

    b) Warum Attribute privat sein sollten

    Damit der Zugriff über Methoden läuft und die Klasse prüfen kann, ob ein Wert zulässig ist. Dadurch kann sie eine Zusage über ihren Zustand einhalten, etwa dass ein Kontostand nie unter den Verfügungsrahmen fällt. Ohne Kapselung müsste jede Stelle des Programms selbst darauf achten.

  3. 3

    c) Vererbung oder Komposition: beide Sätze aussprechen

    Der Prüfstein sind zwei Sätze: „ist ein“ deutet auf Vererbung, „hat ein“ auf Komposition. Fahrrad und Rad: „Ein Fahrrad hat Räder“ → Komposition. Hund und Tier: „Ein Hund ist ein Tier“ → Vererbung. Computer und Festplatte: „hat eine“ → Komposition. Roman und Buch: „ist ein“ → Vererbung.

Übung 2

mittel

Eine Schulverwaltung soll Personen erfassen: Schüler (mit Klasse und Noten) und Lehrer (mit Fächern und Stundenzahl). Beide haben Name und Geburtsdatum.

a) Entwirf die Klassenstruktur mit Vererbung und gib die Sichtbarkeiten an. b) Beide sollen eine Methode beschreibung()\texttt{beschreibung()} haben, die unterschiedlich ausfällt. Wie setzt man das um? c) Erkläre, was beim Aufruf von beschreibung()\texttt{beschreibung()} auf einer Liste gemischter Personen passiert. d) Nun kommen Hausmeister dazu. Was ist zu ändern?

Tipp anzeigen

Zu c): Woher weiß der Rechner, welche Fassung er ausführen soll?

Lösung anzeigen

a) Struktur:

Klasse Person:
    - name
    - geburtsdatum
    + alter()
    + beschreibung()

Klasse Schüler erbt von Person:
    - klasse
    - noten
    + durchschnitt()
    + beschreibung()          // überschrieben

Klasse Lehrer erbt von Person:
    - faecher
    - stundenzahl
    + beschreibung()          // überschrieben

privat (−-), Methoden öffentlich (++). Sollen die Unterklassen direkt auf name\texttt{name} zugreifen, wäre #\# (geschützt) angebracht.

b) Durch Überschreiben: Die Oberklasse legt die Methode fest, jede Unterklasse gibt ihre eigene Umsetzung an.

Schu¨ler.beschreibung()\texttt{Schüler.beschreibung()} liefert etwa „Anna Meier, Klasse 12". Lehrer.beschreibung()\texttt{Lehrer.beschreibung()} liefert etwa „Herr Weber, Mathematik und Physik".

Sinnvoll ist, Person\texttt{Person} abstrakt zu machen, denn eine Person ohne nähere Bestimmung soll es nicht geben.

c) Es greift die Polymorphie. Der kann über die Liste laufen und für jedes Element beschreibung()\texttt{beschreibung()} aufrufen, ohne zu wissen, ob es ein Schüler oder ein Lehrer ist. Ausgeführt wird jeweils die Fassung der tatsächlichen Klasse des , nicht die der Oberklasse.

d) Es kommt eine neue Klasse hinzu:

Klasse Hausmeister erbt von Person:
    - zustaendigkeit
    + beschreibung()

Am bestehenden Code ändert sich nichts. Die über die Personenliste funktioniert unverändert, weil sie nie nach der konkreten Klasse gefragt hat. Genau das ist der Gewinn der Polymorphie: Erweiterung ohne Änderung des Bestehenden.

Detaillierte Schritterklärung anzeigen

Hier wird jeder Schritt einzeln erklärt, vor allem, warum er gemacht wird.

Erklärungstiefe

✦ Empfohlen: Standard – Die normale Erklärungstiefe passt zum Einstieg.

  1. 1

    Teil a): Das Gemeinsame nach oben ziehen

    Man sucht, was beide haben, und stellt es in die Oberklasse. Alles Übrige bleibt bei den Unterklassen. Vorher prüft man die Frage „ist ein": Ein Schüler ist eine Person, ein Lehrer ebenfalls.

    Zwischenergebnis

    name\texttt{name} und geburtsdatum\texttt{geburtsdatum} nach oben, Rest unten.

    Wird ein Attribut nur von einer Unterklasse gebraucht, gehört es nicht in die Oberklasse. Sonst tragen alle anderen etwas mit, das sie nichts angeht.

  2. 2

    Teil b): Gleicher Name, eigene Umsetzung

    Die Methode wird in der Oberklasse festgelegt und in jeder Unterklasse überschrieben. Der Name und die Parameter müssen dabei übereinstimmen.

    Zwischenergebnis

    Zwei Fassungen von beschreibung()\texttt{beschreibung()}.

  3. 3

    Teil c): Wer entscheidet, welche Fassung läuft?

    Nicht der Typ der entscheidet, sondern die tatsächliche Klasse des Objekts. Deshalb kann derselbe Aufruf verschiedene Ergebnisse liefern.

    Zwischenergebnis

    Ausgeführt wird die Fassung der tatsächlichen Klasse.

  4. 4

    Teil d): Prüfen, was sich wirklich ändert

    Man geht den bestehenden Code durch und sucht Stellen, die die Klassen einzeln aufzählen. Findet man keine, ist auch nichts zu ändern.

    Zwischenergebnis

    Nur eine neue Klasse, sonst nichts.

    Genau daran erkennt man einen guten objektorientierten Entwurf: Eine Erweiterung berührt nur neue Dateien. Muss man an vielen Stellen etwas ergänzen, fehlt irgendwo die Polymorphie.

Übung 3

schwer

a) Erkläre, warum Polymorphie Programme erweiterbar macht. Vergleiche mit einer Lösung über Fallunterscheidungen. b) Was ist eine abstrakte Klasse, und wann setzt man sie ein? c) Ein Entwickler lässt Quadrat\texttt{Quadrat} von Rechteck\texttt{Rechteck} erben. Beurteile das. d) Ein Entwickler lässt Stapel\texttt{Stapel} von Liste\texttt{Liste} erben, um zu sparen. Beurteile das und nenne die bessere Lösung.

Tipp anzeigen

Zu c) und d): Kann die Unterklasse die Zusagen der Oberklasse halten?

Lösung anzeigen

a) Bei Polymorphie steht das Wissen darüber, wie eine Aufgabe zu erledigen ist, in der jeweiligen Klasse. Der benutzende Code ruft nur die Methode auf und fragt nie nach dem Typ. Eine neue Unterklasse bringt ihre Umsetzung selbst mit, und der bestehende Code bleibt unverändert.

Bei Fallunterscheidungen steht die Liste aller bekannten Klassen im benutzenden Code, und zwar an jeder Stelle, an der unterschieden wird: beim Zeichnen, beim Speichern, beim Berechnen. Jede neue Klasse verlangt an all diesen Stellen eine Ergänzung. Vergisst man eine, entsteht ein Fehler, der oft erst spät auffällt, weil der Code weiterläuft und nur ein falsches Ergebnis liefert.

Kurz: Polymorphie verschiebt das Wissen von den vielen benutzenden Stellen in die eine Klasse, die es betrifft.

b) Eine abstrakte Klasse schreibt Methoden vor, ohne sie umzusetzen, und von ihr lassen sich keine erzeugen.

Eingesetzt wird sie, wenn eine Oberklasse nur als gemeinsames Dach dient und für sich genommen keinen Sinn ergibt. Eine „Form" ohne nähere Angabe hat keine Fläche; die Rückgabe 0 wäre ein Notbehelf und würde einen Fehler verschleiern.

Vorteil: Eine Unterklasse, die die vorgeschriebene Methode nicht umsetzt, wird bereits vom Übersetzer beanstandet. Der Fehler tritt damit beim Übersetzen auf und nicht erst im Betrieb.

c) Geometrisch ist „ein Quadrat ist ein Rechteck" richtig, für den Entwurf ist die Vererbung trotzdem problematisch.

Ein Rechteck sagt zu, dass sich Länge und Breite unabhängig voneinander setzen lassen. Ein Quadrat kann das nicht halten: Setzt man die Länge, muss sich die Breite mitändern. Code, der ein Rechteck erwartet und Länge und Breite getrennt setzt, verhält sich mit einem Quadrat also plötzlich anders.

Daraus folgt die verschärfte Prüfregel: „ist ein" ist notwendig, aber nicht hinreichend. Zusätzlich muss die Unterklasse überall einsetzbar sein, wo die Oberklasse steht. Besser wäre hier eine gemeinsame Oberklasse Viereck\texttt{Viereck}, die keine unabhängigen Seiten zusagt.

d) Das ist ein Entwurfsfehler, und zwar aus demselben Grund wie in c).

Stapel\texttt{Stapel} würde neben ablegen\texttt{ablegen} und entnehmen\texttt{entnehmen} auch einfuegenAn(index)\texttt{einfuegenAn(index)} und loescheAn(index)\texttt{loescheAn(index)} erben. Damit ließe sich mitten im etwas herausnehmen, und genau das soll die Struktur verhindern. Die LIFO-Zusage wäre nicht mehr garantiert, sondern hinge davon ab, dass niemand die geerbten Methoden benutzt.

Bessere Lösung: Komposition. Stapel\texttt{Stapel} bekommt eine Liste als privates und bietet nach außen nur ablegen\texttt{ablegen}, entnehmen\texttt{entnehmen} und istLeer\texttt{istLeer} an. Der Code der Liste wird genauso wiederverwendet, aber die Zusage bleibt erhalten, weil die Liste von außen unerreichbar ist.

Die allgemeine Lehre: Wiederverwendung ist kein Grund für Vererbung. Sie ist eine Folge richtiger Vererbung; als Motiv führt sie zu Beziehungen, die inhaltlich falsch sind.

Detaillierte Schritterklärung anzeigen

Hier wird jeder Schritt einzeln erklärt, vor allem, warum er gemacht wird.

Erklärungstiefe

✦ Empfohlen: Standard – Die normale Erklärungstiefe passt zum Einstieg.

  1. 1

    a) Polymorphie verschiebt das Wissen dorthin, wo es hingehört

    Bei Polymorphie steht das Wissen darüber, wie eine Aufgabe zu erledigen ist, in der jeweiligen Klasse. Der benutzende Code ruft nur die Methode auf und fragt nie nach dem Typ. Eine neue Unterklasse bringt ihre Umsetzung selbst mit, und der bestehende Code bleibt unverändert.

  2. 2

    b) Abstrakte Klassen: vorschreiben ohne umzusetzen

    Eine abstrakte Klasse schreibt Methoden vor, ohne sie umzusetzen, und von ihr lassen sich keine Objekte erzeugen. Eingesetzt wird sie, wenn eine Oberklasse nur als gemeinsames Dach dient und für sich genommen keinen Sinn ergibt, eine „Form“ ohne nähere Angabe hat keine Fläche.

  3. 3

    c) Quadrat erbt von Rechteck: geometrisch richtig, im Entwurf falsch

    Geometrisch ist „ein Quadrat ist ein Rechteck“ richtig, für den Entwurf ist die Vererbung trotzdem problematisch. Ein Rechteck sagt zu, dass sich Länge und Breite unabhängig voneinander setzen lassen. Ein Quadrat kann das nicht halten: Setzt man die Länge, muss sich die Breite mitändern.

  4. 4

    d) Stapel erbt von Liste: Wiederverwendung ist kein Grund für Vererbung

    Das ist ein Entwurfsfehler, und zwar aus demselben Grund wie in c): Stapel\texttt{Stapel} würde neben ablegen\texttt{ablegen} und entnehmen\texttt{entnehmen} auch einfuegenAn(index)\texttt{einfuegenAn(index)} und loescheAn(index)\texttt{loescheAn(index)} erben. Damit ließe sich mitten im Stapel etwas herausnehmen, genau das soll die Struktur verhindern. Bessere Lösung: Komposition.

Zusammenfassung

Kapselung macht privat und leitet jeden Zugriff über Methoden; ihr Zweck ist nicht Geheimhaltung, sondern eine Zusage über den inneren , die immer gilt, ohne dass der übrige darauf achten muss. Vererbung zieht Gemeinsames in eine Oberklasse, die Unterklassen ergänzen ihr Eigenes und dürfen geerbte Methoden überschreiben; die Prüffrage lautet „ist ein", während „hat ein" auf Komposition führt. Der eigentliche Gewinn ist die Polymorphie: Ein einer Unterklasse steht überall dort, wo die Oberklasse erwartet wird, und beim Aufruf wird die Fassung der tatsächlichen Klasse ausgeführt. Damit ersetzt sie Fallunterscheidungen über den Typ, und eine neue Unterklasse lässt sich hinzufügen, ohne bestehenden Code zu ändern. Abstrakte Klassen schreiben Methoden vor, ohne sie umzusetzen, und lassen den Übersetzer prüfen, dass jede Unterklasse sie liefert. Die Prüffrage „ist ein" allein genügt allerdings nicht: Die Unterklasse muss die Zusagen der Oberklasse auch halten können, sonst führt die Vererbung wie bei Quadrat und Rechteck oder bei und Liste zu einem falschen Entwurf.