Vererbung: für Objekte nützlich, für Werte gefährlich

Seite 3: Bundling

Inhaltsverzeichnis

Zwar unterscheiden sich objektorientierte Sprachen darin, wie Vererbung im Detail funktioniert (Einfach- versus Mehrfachvererbung, unterschiedliche Zugriffs-Modifier, dynamisches Binden etc.), allen verbreiteten objektorientierten Sprachen – Java, C#, Scala, Kotlin, Ceylon, Swift, Eiffel, Smalltalk et cetera – ist aber gemeinsam, dass ihre Vererbung zwei grundlegend verschiedene Sprachbausteine miteinander kombiniert: Subclassing und Subtyping.

Subclassing ist die Übernahme der Implementierung und wird darum als "Implementierungsvererbung" bezeichnet: Alle Member – Datenfelder und Methoden – der Superklasse stehen automatisch auch in der Subklasse zur Verfügung. Letztere kann zusätzliche Member definieren und innerhalb vorgegebener Regeln die geerbten Methoden überschreiben, das heißt mit einer eigenen Implementierung versehen. Definiert etwa eine Klasse Fahrzeug die Member-Variablen Hersteller, Höchstgeschwindigkeit und Hubraum, werden sie auf eine Subklasse wie Motorrad vererbt. Zusätzlich kann die Subklasse eigene, Motorrad-spezifische Member hinzufügen.

Subtyping, auch als "Schnittstellenvererbung" bekannt, beschreibt den Umstand, dass sich der eine Typ als Subtyp des anderen auffassen lässt: Alle dessen Exemplare zusammengenommen bilden eine Teilmenge aller Exemplare des Supertyps, und darum lässt sich, wo ein Element des Supertyps erwartet wird, ein Element des Subtyps verwenden. So ist die Menge aller Motorräder eine Teilmenge aller Fahrzeuge, und Code, der Fahrzeuge bearbeitet, kann auch Motorräder handhaben, ohne dass er speziell für sie anzupassen ist. Der benutzende Code muss nicht einmal wissen, dass er es mit Motorrädern zu tun hat.

Subclassing und Subtyping bewirken unterschiedliche Arten von Wiederverwendung. Beim Subclassing wird der Code der Superklasse wiederverwendet: Die Member-Variablen und Methoden der Superklasse sind in der Subklasse nicht nochmals zu programmieren. Beim Subtyping dagegen wird der Code wiederverwendet, der die Superklasse benutzt, also seine Klienten. Code, der Fahrzeuge anspricht, lässt sich ohne weiteres auch für Motorräder, PKWs, LKWs et cetera benutzen.

Im Fahrzeugbeispiel fallen Subclassing und Subtyping zusammen. Die Klasse Motorrad erbt die Implementierung der Klasse Fahrzeug, und der Typ Motorrad bildet einen Subtyp des Typs Fahrzeug. Dass das aber nicht immer so ist, zeigen die eingangs beschriebenen Problemfälle Kreis/Ellipse, Float/Complex und Point2D/Point3D.

Grundsätzlich ist es möglich, Subclassing sprachlich von Subtyping zu trennen, und in einigen Programmiersprachen gibt es Ansätze dafür. So unterstützt C++ private Vererbung. Auf diese Weise erbt die Subklasse die Implementierung, sie bildet aber keinen Subtyp. Mit Duck Typing, das zum Beispiel Python zugrunde liegt, lässt sich umgekehrt eine Klasse immer dann anstelle einer anderen verwenden, wenn sie über deren Methoden verfügt. Sie fungiert dann als ihr Subtyp, unabhängig davon, ob es zwischen den beiden eine Vererbungsbeziehung gibt oder nicht.

Die meisten Programmiersprachen aber betreiben "Bundling": Mit einem einzigen Symbol (wie ":") oder Schlüsselwort (wie "extends") erhalten Programmierer immer beides, Subclassing und Subtyping. Sie sind untrennbar aneinander gekoppelt, das eine ist nicht ohne das andere zu haben. Diese Kopplung verringert die Lesbarkeit des Codes. Stößt man in bestehendem Code auf eine Anweisung wie A extends B, bleibt die Intention der Programmierer unklar. Wollten sie hier die Implementierung von A in B wiederverwenden? Oder B als Subtyp von A definieren? Oder beides? Vielen Programmierern ist der Unterschied zwischen Subclassing und Subtyping gar nicht bewusst, sie betrachten Vererbung als ein einziges, unteilbares Konzept.

Wie das Beispiel Fahrzeug/Motorrad zeigt, funktioniert die Kombination von Subclassing und Subtyping in manchen Fällen recht gut. Das Subclassing ist hier sogar ursächlich für das Subtyping: Weil die Klasse Motorrad alle Member-Variablen und Methoden von der Basisklasse Fahrzeug erbt, ist sie konform zu deren Schnittstelle, und deshalb lässt sich ein Motorrad-Exemplar überall dort verwenden, wo per Definition ein Fahrzeug akzeptiert wird.

In den eingangs beschriebenen Beispielen dagegen funktioniert die Kombination von Subclassing und Subtyping nicht. Dort führt Subclassing nicht zu Subtyping. Bereits anschaulich ist klar, dass die Menge der dreidimensionalen Punkte keine Teilmenge der zweidimensionalen Punkte bildet und die komplexen Zahlen nicht in den Gleitkommazahlen enthalten sind. Wer Ellipse von Kreis erben lässt, um die Implementierung von Kreis in Ellipse wiederzuverwenden, erhält in Hinblick auf Subtyping genau die "falsche" Richtung: Ellipsen sind keine speziellen Kreise, sondern umgekehrt sind Kreise spezielle Ellipsen.

In all diesen Beispielen handelt es sich um Werte: unveränderliche Abstraktionen mit fester Menge. Zahlen und Punkte sind Werte, auch Formen wie Kreis und Ellipse lassen sich als wertartige, mathematische Abstraktionen auffassen. Das ist der entscheidende Unterschied: Was bei Objekten hinlänglich gut funktioniert – Motorräder sind spezielle Fahrzeuge, Buttons sind spezielle Widgets –, klappt nicht bei Werten.

Bereits mehrfach wurde beobachtet, dass Vererbung in Zusammenhang mit Werten kritisch zu sehen ist. Kevlin Henney konstatierte bereits 2002, dass Vererbung auf Werte keine Anwendung findet: "Values do not typically find themselves in class hierarchies". Michael Kölling und John Rosenberg, deren Sprache Blue Werte in Form von "Manifest Classes" unterstützt, drücken es noch drastischer aus: "manifest classes and inheritance simply do not go together". Auch Programmiersprachen halten sich bei Werten mit Vererbung zurück. Beispielsweise gibt es in C# Vererbung nur bei Klassen, nicht aber bei den für die Unterstützung von Wertsemantik gedachten structs.