Informatik

Generalisierung und Spezialisierung

Eine Mediathek verwaltet Bücher, DVDs und Zeitschriften. Drei Klassen, drei Quelltexte – und in allen dreien stehen dieselben Zeilen für Titel, Signatur und Ausleihe.

Dass das schlecht ist, weißt du seit der Einführungsphase. Die neue Frage lautet nicht ob man das zusammenzieht, sondern was genau nach oben gehört – und wann eine Vererbung die falsche Antwort ist.

Zwei Richtungen, eine Beziehung

Eine Vererbungsbeziehung lässt sich aus zwei Richtungen lesen:

  • Von unten nach oben ist es eine Generalisierung: Man erkennt, was mehrere Klassen gemeinsam haben, und zieht es in eine Oberklasse. Aus Buch, Dvd und Zeitschrift wird Medium.
  • Von oben nach unten ist es eine Spezialisierung: Man nimmt eine allgemeine Klasse und ergänzt Besonderheiten. Aus Medium wird Dvd mit einer Spieldauer.

Beides beschreibt dieselbe Beziehung im Diagramm – denselben Pfeil. Welche Richtung du gehst, hängt nur davon ab, wie du zu deinem Entwurf kommst: Wer drei fertige Klassen vor sich hat, generalisiert. Wer bei null anfängt, spezialisiert.

Was gehört nach oben?

Beim Zusammenziehen entstehen drei Sorten von Bestandteilen. Sie werden verschieden behandelt.

Bestandteil Beispiel Wohin?
bei allen gleich, gleiche Bedeutung, gleiche Umsetzung titel, leiheAus() in die Oberklasse – einmal geschrieben, dreimal geerbt
bei allen vorhanden, aber jeweils anders leihdauerInTagen() in die Oberklasse und in jeder Unterklasse überschrieben
nur bei einer vorhanden seiten, minuten bleibt unten in der Unterklasse

Die häufigste Verschlimmbesserung ist, alles in die Oberklasse zu ziehen, was in mindestens zwei Klassen vorkommt. Dann bekommt jede Zeitschrift eine Spieldauer, weil DVD und Hörbuch eine haben. Eine Oberklasse ist kein Sammelbecken – sie ist die Antwort auf die Frage: Was gilt für jedes dieser Objekte?

Drei Beobachtungen an diesem Entwurf:

  1. Buch überschreibt leihdauerInTagen nicht. Was nicht überschrieben wird, wird geerbt – und die geerbten 28 Tage sind für Bücher genau richtig. Nichts zu schreiben ist hier die richtige Lösung.
  2. Die Attribute sind private, obwohl es Unterklassen gibt. Die Unterklassen kommen über getTitel() heran und brauchen keinen direkten Zugriff.
  3. Medium[] bestand nimmt alle drei Sorten auf, und die Schleife behandelt sie gleich – obwohl leihdauerInTagen() bei jedem etwas anderes tut. Warum das funktioniert, ist das Thema der nächsten Lektion.

Der „ist ein"-Test

Nicht jede Gemeinsamkeit rechtfertigt eine Vererbung. Der Test dafür passt in einen Satz.

Eine Vererbung B extends A ist nur dann richtig, wenn der Satz „jedes B ist ein A" stimmt – und zwar so, dass ein B überall stehen darf, wo ein A erwartet wird.

Stimmt stattdessen „ein B hat ein A", gehört dorthin keine Vererbung, sondern eine Assoziation: ein Attribut vom Typ A.

Satz Beziehung im Quelltext
Eine DVD ist ein Medium. Vererbung class Dvd extends Medium
Ein Auto hat einen Motor. Assoziation private Motor motor;
Ein Kreis hat einen Mittelpunkt. Assoziation private Punkt mittelpunkt;

Der häufigste Fehler ist, Vererbung als Abkürzung zum Wiederverwenden zu benutzen: „Ein Stapel braucht ein Feld, also class Stapel extends Feld." Der Satz „ein Stapel ist ein Feld" stimmt aber nicht – und die Folge ist, dass jeder von außen an jeder Stelle des Stapels herumfummeln darf. Man erbt eben nicht nur die nützlichen Methoden, sondern alle.

Wie weit macht man auf?

Wer eine Oberklasse baut, entscheidet noch etwas: Ob die Unterklassen an die Attribute herandürfen.

private protected
Unterklasse kommt direkt heran nein ja
Oberklasse kann die Speicherung später ändern ja, ohne Rückfrage nur, wenn keine Unterklasse mitschreibt
Regeln der Oberklasse gelten weiterhin ja nein – die Unterklasse kann sie umgehen

Faustregel: im Zweifel private. protected gibt man nur dann, wenn eine Unterklasse den Wert wirklich braucht und ein Getter nicht reicht. Jedes protected ist ein Versprechen an alle künftigen Unterklassen – und Versprechen kann man schlecht zurücknehmen.


Teil 1: Lesen

Aufgabe 1: Sechs Vorschläge beurteilen

Ohne Rechner. Entscheide für jeden Vorschlag: Vererbung, Assoziation oder gar keine Beziehung? Prüfe mit dem „ist ein"-Satz und schreib ihn jedes Mal auf.

a) class Sparkonto extends Konto

b) class Auto extends Motor

c) class Kreis extends Punkt – ein Kreis hat einen Mittelpunkt

d) class Lehrerin extends Person

e) class Stapel extends Feld – ein Stapel speichert seine Elemente in einem Feld

f) class Quadrat extends Rechteck

Tipp: die Gegenprobe

Der „ist ein"-Satz allein reicht nicht immer. Mach die Gegenprobe: Nimm eine Methode der Oberklasse und frag, ob sie bei der Unterklasse noch sinnvoll ist.

Bei e) etwa: Ein Feld erlaubt den Zugriff auf jede Stelle. Ein Stapel soll genau das verbieten.

Lösung. Erfrage das Passwort bei deiner Lehrkraft.

Aufgabe 2: Was gehört nach oben?

Ohne Rechner. Eine Schule verwaltet drei Personengruppen. Hier stehen sie – mit allen Wiederholungen.

Entscheide für jedes Attribut und jede Methode, wohin es gehört: in die Oberklasse Person, in die Oberklasse und überschrieben, oder unten in der jeweiligen Klasse. Zeichne anschließend das Implementationsdiagramm der aufgeräumten Hierarchie.

public class Schuelerin {
    private String name;
    private int geburtsjahr;
    private int jahrgangsstufe;
 
    public String getName() { return name; }
    public int alterImJahr(int pJahr) { return pJahr - geburtsjahr; }
    public String ausweiszeile() { return name + ", Stufe " + jahrgangsstufe; }
    public boolean darfWaehlen(int pJahr) { return alterImJahr(pJahr) >= 16; }
}
 
public class Lehrerin {
    private String name;
    private int geburtsjahr;
    private String fach;
    private int stundenzahl;
 
    public String getName() { return name; }
    public int alterImJahr(int pJahr) { return pJahr - geburtsjahr; }
    public String ausweiszeile() { return name + ", " + fach; }
    public boolean darfWaehlen(int pJahr) { return true; }
}
 
public class Hausmeister {
    private String name;
    private int geburtsjahr;
    private String schluesselbund;
 
    public String getName() { return name; }
    public int alterImJahr(int pJahr) { return pJahr - geburtsjahr; }
    public String ausweiszeile() { return name + " (Hausdienst)"; }
    public boolean darfWaehlen(int pJahr) { return true; }
}
Tipp: drei Fragen je Bestandteil
  1. Kommt es in allen dreien vor? Wenn nein → bleibt unten.
  2. Ist die Umsetzung in allen dreien gleich? Wenn ja → nur in die Oberklasse.
  3. Kommt es überall vor, ist aber verschieden → Oberklasse und überschreiben.
Lösung. Erfrage das Passwort bei deiner Lehrkraft.

Teil 2: Schreiben

Aufgabe 3: Drei Klassen zusammenziehen

Im Programmierbereich stehen drei Klassen mit viel doppeltem Quelltext. Zieh das Gemeinsame in die vorbereitete Oberklasse Fahrzeug, bis alle Tests grün sind.

Die Regeln:

  • Fahrzeug bekommt alles, was für jedes Fahrzeug gilt: Kennzeichen, Kilometerstand, fahre(...).
  • fahre(pKilometer) erhöht den Kilometerstand, aber nur bei positiver Angabe.
  • maut() liefert die Maut pro gefahrenem Kilometer in Cent. Sie ist bei jeder Fahrzeugart anders: PKW 10, LKW 30, Motorrad 5.
  • mautSumme() steht nur in Fahrzeug und liefert Kilometerstand mal Maut.
  • Was nur eine Klasse hat, bleibt dort.
Tipp 1: Was gehört in Fahrzeug?

Alles, was in allen drei Klassen gleich aussähe: kennzeichen, kilometerstand, der Konstruktor, die beiden Getter, fahre(...) und mautSumme().

maut() gehört auch nach oben – aber nur, damit es die Methode gibt. Der Wert dort ist der des PKW, und die beiden anderen Klassen überschreiben ihn.

Tipp 2: mautSumme steht nur oben

mautSumme() ruft maut() auf – und zwar ohne zu wissen, um welches Fahrzeug es sich handelt:

return getKilometerstand() * maut();

Beim Motorrad kommen trotzdem 5 Cent heraus. Warum das funktioniert, ist genau das Thema der nächsten Lektion.

Tipp 3: Wie überschreibt man?

In der Unterklasse dieselbe Signatur noch einmal hinschreiben, mit einem anderen Rumpf:

public int maut() {
    return 30;
}

Kein extends in der Methode, kein Schlüsselwort davor – nur derselbe Kopf.

Lösung. Erfrage das Passwort bei deiner Lehrkraft.

Zum Weiterdenken

Vertiefung 1: Wenn „ist ein" nicht genügt

In der Mathematik ist jedes Quadrat ein Rechteck. Der Programmierbereich zeigt, dass daraus nicht folgt, dass Quadrat extends Rechteck ein guter Entwurf ist.

a) Sag voraus, was die vier Zeilen ausgeben.

b) Führ das Programm aus. Die Methode richteAus bekommt ein Rechteck und weiß nichts über Quadrate. Warum liefert sie trotzdem zwei verschiedene Ergebnisse?

c) Wer hat den Fehler gemacht – wer richteAus geschrieben hat oder wer Quadrat geschrieben hat? Begründe.

d) Formuliere die Regel, die hier verletzt wird, als Erweiterung des „ist ein"-Tests. Sie beginnt mit: Eine Unterklasse darf …

e) Nenne zwei Entwürfe, die das Problem vermeiden, und beurteile sie.

Lösung. Erfrage das Passwort bei deiner Lehrkraft.

Vertiefung 2: Wie tief darf eine Hierarchie sein?

Ein Entwurf für ein Rollenspiel sieht so aus:

Spielobjekt
└── Lebewesen
    └── Kaempfer
        └── Magier
            └── Feuermagier
                └── Erzfeuermagier

a) Nenne drei praktische Nachteile dieser Tiefe. Denk an jemanden, der Erzfeuermagier zum ersten Mal liest.

b) Ein Erzfeuermagier soll fliegen können – aber auch ein Drache, der kein Kaempfer ist. Wo baust du das ein?

c) Die Klasse Kaempfer hat protected int lebenspunkte. Fünf Ebenen tiefer setzt jemand lebenspunkte = -5;. Was ist daran schlimm, und was hätte private verhindert?

d) Beurteile: Formuliere eine Faustregel, ab wann eine Hierarchie zu tief ist – und was man stattdessen tut.

Lösung. Erfrage das Passwort bei deiner Lehrkraft.

Selbsttest

Generalisierung und Spezialisierung

Teilbare URL erstellen

Abschnitte auswählen