Go-interfaces en structuurembedding: verschillen met klasse-overerving en compositieontwerp

Overzicht
Go-interfaces maken het gemakkelijk om functies met dezelfde argumenten en retourwaarden over meerdere structs heen te hebben, maar dit verschilt van de manier waarop Java's extends-sleutelwoord de functie-implementatie op passende wijze uitbreidt en overschrijft. Pas wanneer je Go's compositorische codehergebruik goed begrijpt, raak je het niet meer door elkaar halen met overerving, maar het is vanaf het begin moeilijk om dit theoretisch volledig te begrijpen. Laten we het bekijken aan de hand van scenario's waarin fouten snel worden gemaakt.
Veelgemaakte fouten
Beginners kunnen de volgende fouten maken.
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}
Wanneer je denkt dat Go traditionele overerving volgt, lijkt deze code geen problemen te bevatten. De uitvoer ervan is echter als volgt.
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 wordt het gedrag van Go pas echt duidelijk.
1watermelon := apple
Deze code zet Apple op geen enkele manier om in de Watermelon-klasse. In feite is watermelon slechts een pointer naar apple.
Nogmaals, Go volgt het traditionele concept van overerving niet.
Wanneer je code schrijft met dit misverstand, ontstaan ernstige fouten zoals betekenisloze pointercreatie en het onbedoeld kopiëren van functies naar andere structs.
Hoe ziet modelcode er dan uit?
Een geschikt voorbeeld 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}
Toch is het in Go mogelijk om overerving te laten lijken. Dat is het voorbeeld van anonieme embedding. Dit is mogelijk door de geneste struct zonder naam te declareren. In dit geval kun je de velden van de onderliggende struct zonder expliciete vermelding gebruiken en er rechtstreeks toegang toe krijgen. Door het patroon te gebruiken waarbij de velden van de onderliggende struct naar de bovenliggende struct worden gepromoveerd, kan de leesbaarheid in sommige gevallen verbeteren. Wanneer de onderliggende struct expliciet zichtbaar moet zijn, raden we echter aan dit niet te gebruiken.
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 dit voorbeeld is er het volgende verschil.
1w.PrintAll() // Geen w.Fruit.PrintAll(), maar automatische promotie-aanroep via de anonieme struct
Beide voorbeelden hebben de volgende belangrijke punten:
- Houd main eenvoudig en verdeel functies per functionaliteit
- Gebruik voor verschillende structs verschillende objecten
- Gebruik een interne struct wanneer iets gedeeld moet worden
Welke voordelen heeft deze programmeerfilosofie?
Voordelen
- Duidelijk onderscheid tussen methoden die gedeeld moeten worden en methoden die dat niet hoeven
- Scheiding van verantwoordelijkheden per afzonderlijke struct en methode
- Code die structureel is gescheiden volgens de vereiste functionele specificaties
In het begin is Go misschien ongewoon omdat het verschilt van traditionele OOP, maar naarmate je er vertrouwder mee wordt, wordt expliciet programmeren mogelijk.
Samenvatting
- Isoleer verantwoordelijkheden
- Verdeel in detail per struct
- Methoden mogen niet worden begrepen als abstracte klassen zoals in Java
- Programmeer expliciet en concreet Go is eenvoudiger en duidelijker dan het traditionele OOP-model en moet afzonderlijk worden behandeld. Schrijf in plaats van uitbreidbaar te programmeren liever gefaseerd en gestructureerd door de code op te splitsen.