Lektion 1.3

Vererbung, abstrakte Klassen und Polymorphie

20 min Lesezeit

Ein Dateimanager zeigt Ordner und Dateien. Ein Ordner kann Dateien und weitere Ordner enthalten, beliebig tief. Du willst die Gesamtgröße eines Ordners berechnen. Müsste das Programm bei jedem Element prüfen, ob es eine Datei oder ein Ordner ist, und für jede neue Art von Element angepasst werden, wäre es schwer zu pflegen.

Du lernst, wie Vererbung, abstrakte Klassen und Polymorphie das vermeiden, wie man sie sauber einsetzt und worauf Oberflächen und das dauerhafte Speichern von Objekten aufbauen.

Definition

Vererbung, Generalisierung und Klassenhierarchie

Bei der Vererbung übernimmt eine Unterklasse alle Attribute und Methoden ihrer Oberklasse, ergänzt eigene und kann geerbte überschreiben. Die Generalisierung fasst Gemeinsames mehrerer Klassen in einer Oberklasse zusammen, die Spezialisierung geht den umgekehrten Weg. Datei und Ordner haben beide einen Namen und eine Größe, also entsteht die Oberklasse Element. In Python steht die Oberklasse in Klammern (class Datei(Element):\texttt{class Datei(Element):}), und super().__init__(...)\texttt{super().\_\_init\_\_(...)} ruft den Konstruktor der Oberklasse auf. Im UML-Klassendiagramm führt eine Linie mit hohlem Dreieck zur Oberklasse.

Mehrere Klassen bilden eine Klassenhierarchie, einen Baum aus Ober- und Unterklassen. Vererbung passt dort, wo ein Objekt der Unterklasse überall eingesetzt werden kann, wo die Oberklasse erwartet wird, ohne dass Erwartungen an ihr Verhalten verletzt werden (Ersetzbarkeit). Gilt nur „hat ein“, nutzt man ein Attribut statt Vererbung.

Definition

Abstrakte Klassen

Eine abstrakte Klasse ist eine Klasse, von der man keine Objekte erzeugt. Sie legt gemeinsame Attribute und Methoden fest und deklariert abstrakte Methoden: Methoden ohne Rumpf, die jede konkrete Unterklasse selbst ausfüllen muss. So ist die Schnittstelle gesichert. Jedes Element hat eine Größe, aber wie sie entsteht, hängt von der Unterklasse ab. In Python dient das Modul abc\texttt{abc} dazu:

ZeileEinrückungAnweisung
10from abc import ABC, abstractmethod\texttt{from abc import ABC, abstractmethod}
20class Element(ABC):\texttt{class Element(ABC):}
31@abstractmethod\texttt{@abstractmethod}
41def groesse(self):\texttt{def groesse(self):}
52pass\texttt{pass}

Element()\texttt{Element()} bricht mit einem TypeError ab. Eine Unterklasse, die groesse\texttt{groesse} nicht überschreibt, bleibt abstrakt. In der UML schreibt man den Namen kursiv oder ergänzt {abstract}\texttt{\{abstract\}}.

Definition

Polymorphie und dynamische Bindung

Polymorphie (Vielgestaltigkeit) heißt: Objekte verschiedener Klassen antworten auf dieselbe Nachricht jeweils auf ihre Weise. Eine Variable vom Typ der Oberklasse darf auf Objekte jeder Unterklasse verweisen. Welche Methode läuft, entscheidet die Klasse des Objekts, nicht der Typ der Variablen. Das nennt man dynamische Bindung (späte Bindung): Der Aufruf wird erst zur Laufzeit an eine Methode gebunden. Bei der statischen Bindung steht die Methode schon beim Übersetzen fest. In Java hat eine Variable einen deklarierten (statischen) Typ, das Objekt hat einen dynamischen Typ, und überschreibbare Methoden werden dynamisch gebunden. Python bindet immer dynamisch.

Definition

Entwurfsmuster Kompositum

Ein Entwurfsmuster ist eine bewährte Lösung für ein wiederkehrendes Entwurfsproblem. Das Kompositum behandelt einzelne Objekte und Zusammensetzungen gleich. Es besteht aus einer abstrakten Oberklasse (der Komponente), Blättern für einzelne Objekte und Komposita als Containern, die eine Liste von Komponenten halten und Nachrichten an alle weitergeben. Weil der Container selbst eine Komponente ist, lassen sich Container beliebig tief verschachteln. Beispiele sind Ordner und Dateien, Gruppen von Figuren in einem Zeichenprogramm und Fenster mit Bedienelementen.

Beispiel

Gesamtgröße eines Ordners

Zu Element gehören diese Unterklassen. Der Ordner bilder enthält Dateien der Größe 30 und 25. Der Ordner projekt enthält eine Datei der Größe 10, den Ordner bilder und eine Datei der Größe 5. Welche Zahl gibt projekt.groesse()\texttt{projekt.groesse()} aus?

ZeileEinrückungAnweisung
60class Datei(Element):\texttt{class Datei(Element):}
71def __init__(self, g):\texttt{def \_\_init\_\_(self, g):}
82self._g = g\texttt{self.\_g = g}
91def groesse(self):\texttt{def groesse(self):}
102return self._g\texttt{return self.\_g}
110class Ordner(Element):\texttt{class Ordner(Element):}
121def __init__(self):\texttt{def \_\_init\_\_(self):}
132self._kinder = []\texttt{self.\_kinder = []}
141def hinzufuegen(self, e):\texttt{def hinzufuegen(self, e):}
152self._kinder.append(e)\texttt{self.\_kinder.append(e)}
161def groesse(self):\texttt{def groesse(self):}
172summe = 0\texttt{summe = 0}
182for k in self._kinder:\texttt{for k in self.\_kinder:}
193summe = summe + k.groesse()\texttt{summe = summe + k.groesse()}
202return summe\texttt{return summe}

Für bilder ruft die Schleife in Zeile 18 groesse()\texttt{groesse()} der beiden Dateien auf (30 und 25), die Summe ist 55. Für projekt liefern die Kinder der Reihe nach 10, 55 (der Ordner bilder rechnet selbst, wie eben) und 5. Die Summe ist 10 + 55 + 5 = 70, also gibt projekt.groesse()\texttt{projekt.groesse()} die Zahl 70 aus.

Zeile 19 weiß nicht, ob k eine Datei oder ein Ordner ist: Die dynamische Bindung wählt die passende Methode. Ein neuer Elementtyp, etwa eine Verknüpfung, braucht in Ordner keine Änderung. Der Ablauf wiederholt sich auf jeder Ebene: Ein Ordner ruft dieselbe Methode seiner Kinder auf. Dieses Prinzip heißt Rekursion und kommt später in der Reihe.

Definition

Generische Datentypen und Collections

Eine Collection ist eine Klasse, die viele Objekte verwaltet, zum Beispiel eine Liste. Ein generischer Datentyp hat einen Typparameter für den Inhalt: In Java ist ArrayList<Buch>\texttt{ArrayList<Buch>} eine Liste von Büchern, in Python schreibt man den Typhinweis list[Buch]\texttt{list[Buch]}. Die Collection wird einmal für beliebige Elementtypen geschrieben. Der Compiler in Java oder ein Prüfwerkzeug in Python lehnt es ab, ein Element des falschen Typs einzufügen. Ohne Typparameter fiele ein solcher Fehler erst bei der Entnahme zur Laufzeit auf. Python führt Typhinweise selbst nicht aus.

Definition

Grafische Benutzeroberflächen

Bibliotheken für grafische Benutzeroberflächen sind Klassenhierarchien. In tkinter sind Beschriftung, Knopf und Rahmen Unterklassen von Widget\texttt{Widget}. Ein Rahmen (Frame\texttt{Frame}) ist ein Container, der Bedienelemente und weitere Rahmen enthält: ein Kompositum. Eigene Oberflächen schreibt man als Unterklasse, etwa class Zaehler(tk.Frame):\texttt{class Zaehler(tk.Frame):}, und legt die Bedienelemente im Konstruktor an. Ereignisse rufen Methoden dieses Objekts auf.

Definition

Persistenz

Persistenz heißt, dass Daten über das Programmende hinaus erhalten bleiben, etwa in einer Datei oder Datenbank. Beim Speichern wird der Zustand eines Objekts samt der Objekte, auf die es verweist, in ein Speicherformat umgewandelt (Serialisierung). Beim Laden entstehen daraus neue Objekte. Speicheradressen werden nicht gespeichert, denn sie gelten nur im laufenden Programm. In Python übernimmt das Modul pickle\texttt{pickle} das: pickle.dump(bib, f)\texttt{pickle.dump(bib, f)} schreibt, pickle.load(f)\texttt{pickle.load(f)} liest. Lade nie pickle\texttt{pickle}-Dateien aus fremder Quelle, sie können beim Laden Programmcode ausführen.

Häufige Fehler

Der Typ der Variablen bestimmt die Methode

„Die Variable hat den Typ Element, also läuft beim Aufruf e.groesse()\texttt{e.groesse()} immer die Methode von Element.“

Richtig ist

Es läuft die Methode der Klasse, zu der das Objekt gehört. Verweist e auf eine Datei, läuft die Methode von Datei. Die Methode von Element ist abstrakt und enthält keine Rechnung.

Häufige Fehler

Ein Quadrat ist ein Rechteck, also erbt es

„In der Mathematik ist jedes Quadrat ein Rechteck. Also ist Quadrat eine Unterklasse von Rechteck.“

Richtig ist

Vererbung verlangt Ersetzbarkeit im Verhalten, nicht nur im Begriff. Hat Rechteck eine Methode, die nur die Breite ändert, kann ein Quadrat sie nicht übernehmen, ohne kein Quadrat mehr zu sein. Code, der ein Rechteck erwartet, würde brechen.

Zusammenfassung

  • Eine Unterklasse erbt von ihrer Oberklasse und kann überschreiben. Generalisierung fasst Gemeinsames in einer Oberklasse zusammen.

  • Vererbung passt bei „ist ein“ mit Ersetzbarkeit des Verhaltens, bei „hat ein“ nutzt man ein Attribut.

  • Eine abstrakte Klasse hat keine Objekte und gibt über abstrakte Methoden die Schnittstelle vor.

  • Polymorphie: Dieselbe Nachricht wirkt je nach Klasse des Objekts anders. Die dynamische Bindung wählt die Methode zur Laufzeit.

  • Das Kompositum behandelt einzelne Objekte und Container gleich, weil beide Komponenten sind.

  • Generische Datentypen haben einen Typparameter für den Inhalt. Persistenz speichert den Zustand von Objekten, beim Laden entstehen neue Objekte.

Übungen

Aufgabe 1Numerisch

Der Ordner wurzel enthält eine Datei der Größe 12, den Ordner x und eine Datei der Größe 1. Der Ordner x enthält eine Datei der Größe 7 und den Ordner y. Der Ordner y enthält Dateien der Größe 3 und 4. Welche Zahl gibt wurzel.groesse()\texttt{wurzel.groesse()} aus?

Aufgabe 2Multiple Choice

Klasse B erbt von A und überschreibt die Methode f. In A liefert f den Text "A", in B den Text "B". In Java steht A x = new B();\texttt{A x = new B();}. Was liefert x.f()\texttt{x.f()}?

Aufgabe 3Multiple Choice

Element ist abstrakt und hat die abstrakte Methode groesse. Was geschieht in Python bei e = Element()\texttt{e = Element()}?

Aufgabe 4Freitext

Rechteck hat die Methode setze_breite(b)\texttt{setze\_breite(b)}, die nur die Breite ändert. Quadrat soll von Rechteck erben. Begründe an einem Zahlenbeispiel, warum das Ersetzbarkeit verletzt.

Aufgabe 5Multiple Choice

Welche Aussage zur Persistenz von Objekten ist richtig?