Go-Schnittstellen und Struktureinbettung: Unterschiede zur Klassenvererbung und kompositionsbasiertes Design

Überblick
Go-Interfaces erleichtern es, dass mehrere Structs Funktionen mit denselben Argumenten und Rückgabewerten besitzen, unterscheiden sich jedoch von der Art, wie Java mit dem Schlüsselwort extends das Verhalten dieser Methoden erweitert und überschreibt. Um die kompositionelle Code-Wiederverwendung in Go richtig zu verstehen, sollte man sie nicht mit Vererbung verwechseln; doch ein theoretisch vollständiges Verständnis von Grund auf ist schwer zu erlangen. Lassen Sie uns anhand von Szenarien, in denen man leicht Fehler macht, darüber sprechen.
Häufige Fehler
Anfänger können Fehler wie die folgenden machen.
1package main
2import (
3 "fmt"
4 "strings"
5)
6
7type Fruits interface {
8 GetBrix() float64
9 GetName() string
10 SetLabel()
11 GetLabel(string) string
12 PrintAll()
13}
14
15type Apple struct {
16 Label string
17 Name string
18 Brix float64
19}
20
21type Watermelon struct {
22 Label string
23 Name string
24 Brix float64
25}
26
27func (a *Apple) PrintAll() {
28 fmt.Printf("Fruit: %s, Label: %s, Brix: %v\n", a.Name, a.Label, a.Brix)
29}
30
31const (
32 NO_LABEL = "EMPTY LABEL"
33)
34
35func (a *Apple) SetLabel(lbl string) {
36 a.Brix = 14.5;
37 a.Name = "apple";
38 lbl_lower := strings.ToLower(lbl)
39 if strings.Contains(lbl_lower, a.Name) {
40 fmt.Println("Succeed: Label was ", lbl)
41 a.Label = lbl;
42 } else {
43 fmt.Println("Failed: Label was ", lbl)
44 a.Label = NO_LABEL;
45 }
46}
47
48func (w *Watermelon) SetLabel(lbl string) {
49 w.Brix = 10;
50 w.Name = "watermelon";
51 lbl_lower := strings.ToLower(lbl)
52 if strings.Contains(lbl_lower, w.Name) {
53 w.Label = lbl;
54 } else {
55 w.Label = NO_LABEL;
56 }
57}
58
59func main() {
60 fmt.Println("Inheritance test #1")
61 apple := new(Apple)
62 watermelon := apple
63 apple.SetLabel("Apple_1")
64 fmt.Println("Apple, before copied to Watermelon")
65 apple.PrintAll()
66 watermelon.SetLabel("WaterMelon_2")
67 fmt.Println("Apple, after copied to Watermelon")
68 apple.PrintAll()
69 fmt.Println("Watermelon, which inherited Apple's Method")
70 watermelon.PrintAll()
71}
Wer glaubt, dass Go traditionelle Vererbung kennt, dem scheint dieser Code unproblematisch. Die Ausgabe dieses Codes lautet jedoch wie folgt.
1Inheritance test #1
2Succeed: Label was Apple_1
3Apple, before copied to Watermelon
4Fruit: apple, Label: Apple_1, Brix: 14.5
5Failed: Label was WaterMelon_2
6Apple, after copied to Watermelon
7Fruit: apple, Label: EMPTY LABEL, Brix: 14.5
8Watermelon, which inherited Apple's Method
9Fruit: apple, Label: EMPTY LABEL, Brix: 14.5
Hier wird das Verhalten von Go lediglich deutlicher.
1watermelon := apple
Dieser Code wandelt Apple keineswegs in die Struct Watermelon um. Vielmehr ist watermelon lediglich ein Zeiger auf apple.
Nochmals betont: Go folgt nicht dem traditionellen Konzept der Vererbung.
Wer mit diesem Missverständnis Code schreibt, begeht gravierende Fehler wie die Erzeugung sinnloser Zeiger oder das unbeabsichtigte Kopieren von Funktionen für andere Structs.
Wie sieht also ein musterhafter Code aus?
Angemessenes Beispiel in Go
1package main
2import (
3 "fmt"
4 "strings"
5)
6
7type Fruits interface {
8 GetBrix() float64
9 GetName() string
10 SetLabel()
11 GetLabel(string) string
12 PrintAll()
13}
14
15type BaseFruit struct {
16 Name string
17 Brix float64
18}
19
20type Apple struct {
21 Label string
22 Fruit BaseFruit
23}
24
25type Watermelon struct {
26 Label string
27 Fruit BaseFruit
28
29}
30
31func (b *BaseFruit) PrintAll() {
32 fmt.Printf("Fruit: %s, Brix: %v\n", b.Name, b.Brix)
33}
34
35
36const (
37 NO_LABEL = "EMPTY LABEL"
38)
39
40func (a *Apple) SetLabel(lbl string) {
41 a.Fruit.Brix = 14.5;
42 a.Fruit.Name = "apple";
43 lbl_lower := strings.ToLower(lbl)
44 if strings.Contains(lbl_lower, a.Fruit.Name) {
45 fmt.Println("Succeed: Label was ", lbl)
46 a.Label = lbl;
47 } else {
48 fmt.Println("Failed: Label was ", lbl)
49 a.Label = NO_LABEL;
50 }
51 fmt.Printf("Fruit %s label set to %s\n", a.Fruit.Name, a.Label);
52 a.Fruit.PrintAll()
53}
54
55func (w *Watermelon) SetLabel(lbl string) {
56 w.Fruit.Brix = 10;
57 w.Fruit.Name = "Watermelon";
58 lbl_lower := strings.ToLower(lbl)
59 if strings.Contains(lbl_lower, w.Fruit.Name) {
60 w.Label = lbl;
61 } else {
62 w.Label = NO_LABEL;
63 }
64 fmt.Printf("Fruit %s label set to %s\n", w.Fruit.Name, w.Label);
65 w.Fruit.PrintAll()
66}
67
68func main() {
69 apple := new(Apple)
70 watermelon := new(Watermelon)
71 apple.SetLabel("Apple_1")
72 watermelon.SetLabel("WaterMelon_2")
73}
Dennoch ist es in Go möglich, Vererbung ähnlich erscheinen zu lassen. Ein Beispiel dafür ist die anonyme Einbettung. Sie ist möglich, wenn man die innere Struct als namenlose Struct deklariert. In diesem Fall kann man auch ohne explizite Angabe auf die Felder der untergeordneten Struct direkt zugreifen. Mit diesem Muster, bei dem die Felder der untergeordneten Struct in die übergeordnete Struct hochgestuft werden, kann die Lesbarkeit je nach Situation verbessert werden. Wenn die untergeordnete Struct jedoch explizit sichtbar gemacht werden muss, wird von der Verwendung abgeraten.
1package main
2import (
3 "fmt"
4 "strings"
5)
6
7type Fruits interface {
8 GetBrix() float64
9 GetName() string
10 SetLabel()
11 GetLabel(string) string
12 PrintAll()
13}
14
15type BaseFruit struct {
16 Name string
17 Brix float64
18}
19
20type Apple struct {
21 Label string
22 BaseFruit
23}
24
25type Watermelon struct {
26 Label string
27 BaseFruit
28
29}
30
31func (b *BaseFruit) PrintAll() {
32 fmt.Printf("Fruit: %s, Brix: %v\n", b.Name, b.Brix)
33}
34
35
36const (
37 NO_LABEL = "EMPTY LABEL"
38)
39
40func (a *Apple) SetLabel(lbl string) {
41 a.Brix = 14.5;
42 a.Name = "apple";
43 lbl_lower := strings.ToLower(lbl)
44 if strings.Contains(lbl_lower, a.Name) {
45 fmt.Println("Succeed: Label was ", lbl)
46 a.Label = lbl;
47 } else {
48 fmt.Println("Failed: Label was ", lbl)
49 a.Label = NO_LABEL;
50 }
51 fmt.Printf("Fruit %s label set to %s\n", a.Name, a.Label);
52 a.PrintAll()
53}
54
55func (w *Watermelon) SetLabel(lbl string) {
56 w.Brix = 10;
57 w.Name = "Watermelon";
58 lbl_lower := strings.ToLower(lbl)
59 if strings.Contains(lbl_lower, w.Name) {
60 w.Label = lbl;
61 } else {
62 w.Label = NO_LABEL;
63 }
64 fmt.Printf("Fruit %s label set to %s\n", w.Name, w.Label);
65 w.PrintAll()
66}
67
68func main() {
69 apple := new(Apple)
70 watermelon := new(Watermelon)
71 apple.SetLabel("Apple_1")
72 watermelon.SetLabel("WaterMelon_2")
73}
In diesem Beispiel besteht folgender Unterschied.
1w.PrintAll() // Automatischer Aufstieg über die namenlose Struct-Einbettung, statt w.Friut.PrintAll()
Beide Beispiele haben folgende wichtige Gesichtspunkte gemeinsam:
- main schlank halten, Funktionen nach Funktionalität aufteilen
- Für unterschiedliche Structs jeweils eigene Objekte verwenden
- Bei Bedarf gemeinsamer Nutzung die innere Struct verwenden
Welche Vorteile hat diese Programmierphilosophie?
Vorteile
- Klare Unterscheidung zwischen Methoden, die gemeinsam genutzt werden müssen, und solchen, die es nicht müssen
- Trennung der Verantwortlichkeiten auf einzelne Structs und Methoden
- Entsprechend der benötigten Funktionsspezifikation strukturell getrennter Code
Zunächst mag Go ungewohnt sein, da es sich von der traditionellen OOP unterscheidet; mit zunehmender Vertrautheit wird jedoch eine explizite Programmierung möglich.
Zusammenfassung
- Verantwortlichkeiten isolieren
- Nach Structs im Einzelnen aufteilen
- Methoden dürfen nicht wie abstrakte Klassen in Java verstanden werden
- Explizit und konkret programmieren Go ist einfacher und übersichtlicher als das traditionelle OOP-Modell und sollte einzeln behandelt werden. Anstatt erweiterbar zu programmieren, schreiben Sie den Code besser schrittweise und strukturiert getrennt.