Ein Einstieg in die Programmiersprache Go, Teil 1

Seite 4: Komposition

Inhaltsverzeichnis

Vererbung dient zum Wiederverwenden vorhandener Objektdefinitionen, meistens in der Form von Klassen. Entwickler können die Funktionen ergänzen und/oder modifizieren und müssen sie nicht neu implementieren. Als Alternative bietet sich Komposition an, die komplexe Objekte durch eine Kombination von einfachen Objekten bildet.

Go unterstützt Komposition über das sogenannte Embedding direkt. Dies bettet einen Typ in einen anderen Typ ein und dessen Methoden sind per Promotion direkt auf dem einbettenden Typ aufrufbar.

type Address struct {
street string
zipcode string
city string
}

// der Receiver wird nicht modifiziert,
// daher wird kein Pointer benötigt
func (a Address) humanReadableAddress() string {
return a.zipcode + a.city + a.street
}

type Clerk struct {
// ein Feld ohne Bezeichner ist embedded
Address
Name string
Age int
salary int
}

a := Address{"Karl-Wiechert-Allee 10", "30625 ", "Hannover"}
c := Clerk{a, "Max Mustermann", 30, 4000}

// Die Methoden des eingebetteten Typs sind direkt auf dem
// einbettenden Typ aufrufbar
c.humanReadableAddress()

Go unterstützt Polymorphie bei Parametern und Rückgabewerten von Funktionen und Methoden sowie bei eingebetteten Datentypen mit Interfaces. Ein Interface ist eine definierte Menge von Methoden.

type PaidEmployee interface {
ChangeSalary(amount int)
}

type Manager struct {
salary int
}

func (m *Manager) ChangeSalary(amount int) {
if amount < 0 {
amount *= -1
}
m.salary += amount
}

Im Unterschied zu Programmiersprachen wie Java muss ein Typ nicht explizit deklarieren, dass er ein Interface implementiert. Es reicht, wenn alle Methoden des Interfaces implementiert sind. Im obigen Beispiel implementiert der Typ *Clerk und der Typ *Manager das Interface PaidEmployee. Interface-Typen entscheiden über die aufzurufende Methode erst zur Laufzeit (dynamic binding).

func RaiseSalaries(e []PaidEmployee) {
for i := 0; i < len(e); i++ {
// welche ChangeSalary Methode aufgerufen
// wird, entscheidet sich erst zur Laufzeit
e.ChangeSalary(-100)
}
}

Interface-Typen lassen sich mit Embedding kombinieren, entweder um ein Interface aus mehreren Interface-Typen zusammen zu setzen oder um als Typ in einen anderen Typ eingebettet zu werden.

// Interface aus mehreren Interface-Typen zusammengesetzt
type Payroll interface {
Address
PaidEmployee
}

type worker struct {
// Interface als eingebetteter Typ
PaidEmployee
Adress
}

Eine Besonderheit stellt das leere [i]interface{} dar. Es enthält keine Methoden: Jeder Typ implementiert es. Nützlich ist es vor allem, wenn die Struktur eines Typs beim Kompilieren noch nicht bekannt ist, beispielsweise beim Encoding oder Decoding von XML oder JSON.

Mit der Kombination von Methoden, Komposition und Interface-Typen lässt sich genausogut objektorientiert programmieren wie mit Klassen und Vererbung. Die Unterschiede sind eher syntaktischer Natur. Methoden müssen im Quelltext nicht mehr zusammen mit ihren Typen definiert werden, wie es bei Klassen der Fall ist. Das hat den Vorteil, dass Entwickler einem Typ nachträglich Methoden hinzufügen können, ohne zwingend den Quelltext des Typs verändern zu müssen.

Ähnlich verhält es sich mit Interface-Typen. Nutzer können sie auch nachträglich einführen, ohne den Quelltext des implementierenden Typs verändern zu müssen, weil eine explizite Deklaration nicht nötig ist. Gerade bei größeren Entwicklungsteams und einer sich schnell ändernden Codebasis ist das von Vorteil, da die Vorgehensweise viele Merges und somit Merge-Konflikte vermeiden kann.