Go-Interfaces stellen keine Vererbung dar
Überblick
Go-Interfaces ermöglichen es auf einfache Weise, Funktionen mit denselben Argumenten und Rückgabewerten in mehreren Strukturen zu haben, unterscheiden sich jedoch von der Art und Weise, wie Java's extends-Schlüsselwort das Verhalten dieser internen Funktionen angemessen erweitert und überschreibt. Ein korrektes Verständnis der kompositionellen Code-Wiederverwendung in Go würde Verwechslungen mit Vererbung vermeiden, doch ein theoretisch perfektes Verständnis von Anfang an ist schwierig. Lassen Sie uns dies anhand von Szenarien untersuchen, die leicht zu Fehlern führen können.
Häufige Fehler
Anfänger können die folgenden Fehler 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}
Dieser Code scheint unproblematisch, wenn man irrtümlicherweise annimmt, dass Go traditionelle Vererbung unterstützt. Die Ausgabe davon ist 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 deutlich:
1watermelon := apple
Dieser Code konvertiert Apple keineswegs in eine Watermelon-Klasse.watermelon ist lediglich ein Pointer auf apple.
An dieser Stelle sei nochmals betont: Go folgt nicht dem traditionellen Vererbungskonzept.
Wenn Code unter dieser Fehlannahme geschrieben wird, führt dies zu kritischen Fehlern wie der Erzeugung bedeutungsloser Pointer oder dem unerwarteten Kopieren von Funktionen für andere Strukturen.
Wie sieht also vorbildlicher 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}
In Go ist es jedoch möglich, Vererbung ähnliche Strukturen zu erzeugen. Ein Beispiel hierfür ist das anonyme Embedding. Dies wird ermöglicht, indem eine interne Struktur als unbenannte Struktur deklariert wird. In solchen Fällen ist es möglich, auf die Felder der untergeordneten Struktur direkt zuzugreifen, ohne sie explizit zu benennen. Dieses Muster, bei dem Felder einer untergeordneten Struktur in die übergeordnete Struktur hochgestuft werden, kann in bestimmten Fällen die Lesbarkeit verbessern. Es wird jedoch nicht empfohlen, es zu verwenden, wenn die untergeordnete Struktur explizit sichtbar sein soll.
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 gibt es folgende Unterschiede:
1w.PrintAll() // w.Friut.PrintAll() ist kein Aufruf, sondern eine automatische Höherstufung durch die unbenannte Struktur
Beide Beispiele betonen folgende wichtige Punkte:
mainsollte prägnant sein, Funktionen nach ihrer Funktionalität getrennt- Für unterschiedliche Strukturen sollten unterschiedliche Objekte verwendet werden
- Bei Bedarf der gemeinsamen Nutzung interne Strukturen verwenden
Welche Vorteile bietet eine solche Programmierphilosophie?
Vorteile
- Klare Unterscheidung zwischen benötigten und nicht benötigten geteilten Methoden.
- Trennung der Verantwortlichkeiten für einzelne Strukturen und Methoden.
- Strukturell getrennter Code gemäß den erforderlichen Funktionsspezifikationen.
Anfangs mag die Go-Sprache, die sich von der traditionellen OOP unterscheidet, ungewohnt erscheinen, aber mit der Gewöhnung ermöglicht sie eine explizite Programmierung.
Zusammenfassung
- Verantwortlichkeiten isolieren.
- In detaillierte Struktureinheiten unterteilen.
- Methoden sollten nicht wie abstrakte Klassen in Java verstanden werden.
- Explizit und konkret programmieren. Die Go-Sprache sollte im Vergleich zu traditionellen OOP-Modellen einfacher und individueller behandelt werden. Statt extensiv zu programmieren, sollte man schrittweise und strukturell getrennt vorgehen.