Vererbung und Polymorphie · Übung
Eine unpassende Hierarchie ersetzen
Vererbung ist schnell eingesetzt und schwer wieder aufzulösen. In dieser Lektion erkennst du, wann du sie zu weit getrieben hast und was du anschließend tun kannst.
Eine gewachsene Hierarchie
Hier ist eine Familie, die sinnvoll begann und weiterwuchs:
class Question:
...
class GradedQuestion(Question):
...
class TimedGradedQuestion(GradedQuestion):
...
class TimedGradedShuffledQuestion(TimedGradedQuestion):
...
Jede Klasse wurde aus einem echten Grund ergänzt. Zusammen beschreiben sie ein Problem, das die Hierarchie nicht lösen kann: Was passiert, wenn jemand eine zeitbegrenzte Frage ohne Bewertung möchte oder eine zufällig angeordnete Frage ohne Zeitlimit?
Jede Kombination unabhängiger Merkmale braucht eine eigene Klasse. Mit jedem neuen Merkmal verdoppelt sich die Klassenanzahl. Zeitbegrenzung, zufällige Anordnung und Bewertung sind keine drei Fragearten. Es sind drei Eigenschaften, die eine Frage unabhängig voneinander haben kann.
Die Anzeichen
Du bist zu weit gegangen, wenn du Folgendes siehst:
Namen als Listen von Adjektiven. TimedGradedShuffledQuestion gibt schon im Klassennamen zu, dass es eine Kombination ist.
Eine Unterklasse, die den Großteil des Geerbten ignoriert. Passen acht von zehn geerbten Methoden nicht, ist die „ist ein“-Beziehung nicht echt.
Überschreibungen, die Ausnahmen auslösen oder nichts tun. Das LockedQuestion aus Lektion 8, das reword ablehnt, ist eine Unterklasse, die ihre Basis nicht erfüllt.
Tiefe um ihrer selbst willen. Jede zusätzliche Ebene muss das Verhalten rechtfertigen, das sie teilt. Jede Ebene ist ein weiterer Ort, an dem sich eine Methode verstecken kann. Tiefe Hierarchien verdienen deshalb mehr Prüfung als flache.
Eine immer weiter wachsende Basis. Wenn eine neue Unterklasse ein zusätzliches Basisattribut verlangt, das nur sie verwendet, ist die Basis zur Vereinigung ihrer Kinder geworden.
Was deutet am stärksten auf eine fehlgeleitete Hierarchie hin?
Durch Komposition ersetzen
Der Ausweg ist die Antwort aus Kapitel 6: Was eine Frage ist, bleibt Vererbung. Was sie hat, wird Komposition.

Eine Frageklasse, mit oder ohne Zeitlimit. Zufällige Anordnung ergänzt einen weiteren optionalen Partner, statt die Klassenliste zu verdoppeln. Die Kombinationen entstehen beim Konstruktoraufruf durch den aufrufenden Code, statt von dir im Voraus aufgezählt zu werden.
Was Vererbung bleiben sollte
Beachte, dass TextQuestion(Question) erhalten blieb. Das soll es auch.
Der Unterschied: Eine Textfrage ist eine echte Art. Sie ändert die Bedeutung von is_correct, und keine Frage ist zugleich eine Text- und eine Zahlenfrage. Zeitbegrenzung ist keine Art. Eine Frage hat ein Zeitlimit oder nicht, und dieselbe Frage könnte morgen ein anderes haben.
Die praktische Frage lautet: Ist das eine Art von Ding oder etwas, das es hat? Sich gegenseitig ausschließende Arten, die das Kernverhalten ändern, kommen als Unterklassen infrage. Frei kombinierbare Merkmale sind Partnerobjekte.
Nachträglich ändern
Eine Hierarchie in funktionierendem Code zu entflechten ist ein Refactoring. Es erfolgt in kleinen Schritten:
Wähle ein Merkmal, das keine Art ist, etwa die Zeitbegrenzung.
Schreibe die kleine Klasse, die dafür verantwortlich ist.
Gib der Basis ein optionales Attribut dafür, standardmäßig ohne Wert.
Verschiebe das Verhalten dieses Merkmals aus den Unterklassen in die neue Klasse.
Lösche jede Unterklasse, die nur für dieses Merkmal existierte.
Halte die Tests bei jedem Schritt erfolgreich.
Der letzte Punkt ist am wichtigsten und das gesamte Thema von Kapitel 13. Refactoring ohne Tests bedeutet umschreiben und hoffen.
Kein Argument gegen Vererbung
Vererbung ist nicht schlecht. Sie ist ein Werkzeug, und dieses Kapitel hat sie sinnvoll verwendet: eine kleine Familie von Fragearten mit einer Ebene, von denen jede eine tatsächlich andere Regel für denselben Vorgang liefert.
Der Fehler ist, sie als Weg zum Teilen beliebiger Dinge zu behandeln. Komposition lässt sich frei kombinieren, Vererbung nicht. Daher ist Komposition der Ausgangspunkt. Zur Vererbung greifst du, wenn das „ist ein“ stimmt.
Aufgabe
Sechs Klassen beschreiben hier zwei Fragearten und zwei optionale Merkmale. Reduziere sie auf drei Klassen und zwei Partnerklassen.
Behalte TextQuestion und NumericQuestion als Unterklassen von Question. Eine Textfrage ändert tatsächlich die Bedeutung von is_correct, und keine Frage ist beides.
Mache Zeitbegrenzung und Hinweise zu Dingen, die eine Frage hat. Schreibe ein validiertes TimeLimit(seconds), das positive Sekunden verlangt, und ein validiertes Hint(text), das nicht leeren Text verlangt. Gib Question optionale Attribute timing und hint, beide mit dem Standard None. Behalte die Validierung für einen nicht leeren Fragetext und nicht negative Punkte.
Question.describe() gibt "<prompt> (<points> points)" zurück, gefolgt von jeweils einem Leerzeichen und der Beschreibung jedes vorhandenen Partners, Zeitbegrenzung vor Hinweis.
Halte den Antwortvertrag aus Lektion 8 ein: Zahlenfragen geben bei leerem oder nicht numerischem Text False zurück.
Lösche TimedTextQuestion, HintedTextQuestion und TimedHintedTextQuestion. Jede Kombination soll jetzt durch übergebene Partnerobjekte entstehen, auch Kombinationen, für die niemand eine Klasse geschrieben hat.