Swift: Unterscheidung statt Vererbung

von Hubert Schmid vom 2014-07-27

Vererbung ist ein zentraler Bestandteil fast aller Objekt-orientierter Programmiersprachen. Verwendet wird sie in der Regel für Spezialisierungen von Klassen mit der sogenannten Ist-ein-Beziehung. Doch nicht jede Ist-ein-Beziehung lässt sich sinnvoll mittels Vererbung abbilden. Um ein typisches Beispiel mit alternativer Abbildung geht es im Folgenden.

Beispiel JSON

Die JavaScript Object Notation (kurz JSON) erfreut sich großer Beliebtheit als Format für den Datenaustausch zwischen Anwendungen. Dabei handelt es sich um eine rekursiv definierte Datenstruktur mit der Besonderheit, dass Elemente unterschiedlicher Typen vereinigt werden. Konkret lassen sich die folgenden sechs Gruppen im Rahmen dieser Polymorphie unterscheiden:

In Java EE 7 wird der JSON-Typ durch eine Klassenhierarchie mit der Basisklasse JsonValue, den drei ausgezeichneten Instanzen JsonValue.NULL, JsonValue.FALSE und JsonValue.TRUE sowie den folgenden vier Unterklassen abgebildet, die im Wesentlichen obiger Gruppierung entsprechen:

Ein Blick in die Basisklasse JsonValue offenbart die Schwächen dieser Abbildung: Es gibt praktisch keine polymorphen Operationen auf dieser Typhierarchie. Stattdessen muss der Anwender mit Hilfe der Methode getValue() selbst den konkreten Typ bestimmen und konvertieren, um Operationen darauf auszuführen.

Enumerations with associated Values

Wie es anders geht, zeigt die Programmiersprache Swift. Dort gibt es für polymorphe Typen neben Vererbung auch die sogenannten Enumerations with associated Values. Dabei handelt es sich um eine Variante der normalen Enumerations, wobei die eigentlichen Enumeration-Werte als Diskriminatoren verwendet werden.

Das folgende Listing zeigt eine vollständige Definition für einen entsprechenden JSON-Typ. Dabei werden von den obigen sechs Fällen nur die letzten fünf explizit abgebildet. Zur Abbildung des Nullwerts wird stattdessen die Optional-Funktionalität von Swift verwendet, um von den eingebauten Operatoren profitieren zu können.

enum Json { case number(Double) case string(String) case boolean(Bool) case array([Json?]) case object([String: Json?]) }

Die Diskriminatoren können wie Konstruktoren verwendet werden, um Json-Elemente zu erzeugen. Im folgenden Listing wird auf diese Weise ein doppelt verschachteltes Objekt konstruiert und der lokalen Variablen json zugewiesen.

let json = Json.object([ "foo": Json.object([ "bar": Json.number(42), "baz": Json.array([ Json.string("a"), Json.string("b"), Json.string("c"), ]), ]), ])

Die Unterscheidung der Typen erfolgt wie bei normalen Enumerations mittels switch-Anweisung. Während der Übersetzungsphase wird dabei sichergestellt, dass alle Fälle abgedeckt sind – oder eine default-Behandlung existiert. Das folgende Listing zeigt eine Member-Funktion, um auf ein Unterelement zuzugreifen, das über einen Pfad spezifiziert wird. Existiert kein entsprechendes Element im Json-Objekt, wird stattdessen nil zurückgegeben. Für obiges Beispiel liefert der Aufruf json.get(["foo", "bar"]) das Ergebnis Json.number(42).

func get(path : [String], startAt index: Int = 0) -> Json? { if index < path.count { switch self { case let .object(items): return items[path[index]]??.get(path, startAt: index + 1) default: return nil } } else { return self } }

Fazit

Das Beispiel verdeutlicht gut, dass es für bestimmte Arten von Polymorphie bessere Techniken als Vererbung zur Abbildung gibt. Swift bietet dafür die Enumerations with associated Values. Andere Programmiersprachen besitzen ähnliche Konzepte, die beispielsweise als Discriminated Unions oder Variants bekannt sind. Wichtig ist vor allem sich bewusst zu machen, dass Vererbung nicht immer die ideale Lösung ist – insbesondere wenn die eingesetzte Programmiersprache keine Alternativen unterstützt. Denn noch schlimmer als nur einen Hammer zu haben, ist zu glauben, es gäbe keine anderen Werkzeuge als Hämmer.