Man kann mit Go objektorientiert programmieren, obwohl zur Definition von Typen und zur Implementierung von Objekten keine Klassen genutzt werden können. Wie das trotzdem funktionieren kann, zeigt der vorliegende Blogpost.
Die Programmiersprache Go wurden bereits in einem anderen Blogpost vorgestellt. Wer einen allgemeinen Einstieg in das Thema sucht, findet dort wichtige Grundlagen.
Typdefinition
Go bietet zur Definition von Typen struct
und interface
. Zunächst soll struct
genauer vorgestellt werden.
Eine Struktur ist zunächst nur eine Sammlung von Variablen. Wie das folgende Beispiel jedoch zeigt, können für eine Struktur auch Funktionen definiert werden. In diesem Fall könnte man auch von Methoden sprechen, weil sie zu einem Objekt gehören. Die Methoden der Objekte (Strukturen) sind übrigens im Gegensatz zu Interfaces nicht virtuell.
In diesem Beispiel erhält die Struktur Circle
eine Methode zur Berechnung der Fläche. Dieses kleine Beispiel funktioniert ohne Probleme. In der Methode Area
kann der Radius des übergebenen Kreises benutzt werden, um die Fläche zu berechnen. Wenn man jedoch eine Methode Enlarge
hinzufügen würde, die den Radius ändert, wird man keinen Erfolg haben, weil die Zustandsänderung auf einer Kopie erfolgt.
Beim Aufruf einer Funktion bzw. Methode werden als Argumente stets Kopien übergeben. Denn in allen Sprachen der C-Familie wird Call-by-Value verwendet. Konsequenterweise arbeitet auch Enlarge
auf einer Kopie und lässt das ursprüngliche Circle
Objekt unverändert. Wenn die aufgerufene Methode das Objekt ändern soll, muss ein Zeiger benutzt werden:
Tipp: Das Entwurfsmuster Command-Query-Separation unterscheidet zwischen Kommandos, die den Zustand des Objektes ändern, und Abfragen, die einen Wert zurückgeben. Eine Funktion ist entweder ein Kommando oder eine Abfrage, aber niemals beides. Entsprechend dieser Idee könnte man nur dann einen Zeiger übergeben, wenn das referenzierte Objekt geändert wird. Bei Übergabe einer Kopie muss man sich auch keine Gedanken über Immutability machen. Ein Nachteil wäre jedoch, dass das Erstellen der Kopien die Performance verschlechtert.
Ein Typ kann alternativ mit interface
definiert werden. Aufbauend auf dem zuvor gezeigten Beispiel soll der Typ Shape
definiert werden. Eine explizite Typdeklaration, wie man sie beispielsweise in Java mit „implements“ kennt, ist nicht notwendig. Ein Objekt vom Typ Circle
kann einer Variable vom Typ Shape
zugeordnet werden, weil Circle
die notwendige Funktion Area
bietet.
Hätte das Interface Shape
zusätzlich eine Methode Length
, die nicht von Circle
implementiert wird, wäre die Wertzuweisung nicht möglich. Man kann den Compiler nutzen, um sicherzustellen, dass tatsächlich das Interface implementiert wird:
Im Paket fmt wird u.a. das Interface Stringer
definiert, das automatisch von jeder Struktur implementiert wird, die eine String
Methode bietet. Die Implementierung des Interfaces ist ohne Typhierarchie möglich.
Typerweiterungen
Es ist nicht möglich, Funktionen zu existierenden Strukturen in anderen Paketen hinzuzufügen. Falls man es dennoch versucht, wird man eine Fehlermeldung vom Compiler erhalten. Stattdessen kann man eine neue lokale Struktur definieren und den existierenden Typ darin einbetten.
Ein verbreitetes Idiom ist das Definieren von Alias-Typen. Ein lokaler Alias-Typ kann mit diversen Funktionen ausgestattet werden.Nennenswert ist ebenfalls der Gebrauch einer anonymen Variable. Angenommen man hat einen Typ Product
und möchte diesen erweitern um den Typ Computer
, dann erhält Computer
eine anonyme Variable vom Typ *Product
, um von diesem zu „erben“. Exemplarisch könnte das so aussehen:
Es wäre auch möglich, die Funktion Sell
für Computer
zu „überschreiben“. Ebenso könnte Computer
weitere anonyme Variablen erhalten. In diesem Fall muss man jedoch auf Namenskonflikte achten.
Polymorphie
Go-Interfaces haben ausschließlich virtuelle Methoden und können von unterschiedlichen Strukturen implementiert werden. Beispielsweise wäre es möglich, das Interface Shape
mit den Strukturen Circle
und Rectangle
zu implementieren. Ob tatsächlich Circle
und Rectangle
das Interface implementieren, wird zum Kompilierzeitpunkt überprüft. Andere Code-Teile, die Objekte durch das Interface Shape
nutzen, müssen nicht wissen, um welche konkreten Strukturen es sich handelt.
In dynamischen Sprachen wie Python ist das Konzept Duck Typing verbreitet: „If it looks like a duck and quacks like a duck, it’s a duck.“ Go bietet zwar kein Duck Typing, wegen der statischen Typüberprüfung, dennoch ist die implizite Implementierung von Interfaces praktisch und sicher. Der für Go gewählte Ansatz ist ein ausgewogener Kompromiss aus Typsicherheit und Convenience.
Keine Konstruktoren
Go hat keine Klassen und demzufolge gibt es auch keine Konstruktoren zur Erzeugung von Instanzen. Man kann aber durchaus Fabrikmethoden implementieren:
Man beachte, dass die Struktur circle
kleingeschrieben ist und deswegen nicht von anderen Paketen mit new(shape.circle)
erzeugt werden kann. Die einzige Möglichkeit, ein Objekt zu instanziieren, ist shape.NewCircle(2)
.
Weil die Struktur von circle
so einfach ist, könnte man die Fabrikmethode auch etwas kompakter formulieren:
Builder
Zur Objekterzeugung sind Builder mit Fluent API sehr beliebt. Eine Fluent API wird typischerweise durch eine Methodenkette umgesetzt. Auch die Go-Bibliothek Squirrel zur Konstruktion von SQL-Abfragen nutzt eine Fluent API. Ein wesentlicher Vorteil einer solchen API ist, dass der entstandene Client-Code sauber und lesbar ist.
Zum Abschluss dieses Blogposts soll eine solche Fluent API entworfen werden, bei der viele der vorgestellten Sprach-Features benutzt werden. Das Ergebnis soll in der Benutzung so aussehen:
Mit der Fluent-API des Builders wird ein request
-Objekt erzeugt. Dem Builder wird die URI und die HTTP-Methode für den Aufruf übergeben. Alle Methoden des Builders sind großgeschrieben, sodass man sie in anderen Paketen nutzen kann. Der Name des Paketes request
und der Name der Methode New
sind aufeinander abgestimmt, weil sie im Client-Code hintereinanderstehen.
Der vollständige Code des Beispiels ist hier:
Fazit
Mit Strukturen und Interfaces können in Go Typen definiert werden, um objektorientiert zu programmieren. Klassen zur Implementierung von Objekten, die ein oder mehrere Typen implementieren, sind nicht notwendig. Einfach- und Mehrfachvererbung erlaubt Go durch das Einbetten eines oder mehrerer anonymer Variablen in eine „erbende“ Struktur. Die offizielle Dokumentation von Go spricht jedoch nicht von Vererbung oder Mixins, sondern vom Konzept der anonymen Variablen. Letztendlich bietet Go Polymorphie und vermeidet komplexe Typhierarchien.