Go-interfaces zijn geen overerving
Overzicht
Go-interfaces maken het mogelijk dat verschillende structs functies met dezelfde argumenten en retourwaarden hebben, maar dit verschilt van de manier waarop de extends-sleutel in Java de werking van interne functies op de juiste manier uitbreidt en overschrijft. Om het concept van compositorische codehergebruik in Go correct te begrijpen, moet men het niet verwarren met overerving, maar het is moeilijk om vanaf het begin een theoretisch perfect begrip te hebben. Laten we dit onderzoeken aan de hand van scenario's waarin vaak fouten worden gemaakt.
Veelvoorkomende 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}
Deze code lijkt geen problemen te hebben als men veronderstelt dat Go traditionele overerving volgt. De uitvoer van deze code 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 de werking van Go duidelijk.
1watermelon := apple
Deze code converteert Apple geenszins naar de Watermelon-klasse. In plaats daarvan is watermelon slechts een pointer naar apple.
Nogmaals, Go volgt geen traditioneel overervingsconcept.
Als men code schrijft met dit misverstand, zullen fatale fouten optreden, zoals het aanmaken van zinloze pointers of het onverwacht kopiëren van functies voor andere structs.
Wat zou dan de best practice-code zijn?
Passend voorbeeld in Go-taal
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}
Het is echter mogelijk om Go op overerving te laten lijken. Een voorbeeld hiervan is anonieme embedding. Dit is mogelijk door een innerlijke struct als een naamloze struct te declareren. In dergelijke gevallen zijn de velden van de substruct toegankelijk zonder expliciete vermelding. Door dit patroon te gebruiken, waarbij de velden van de substruct worden gepromoveerd naar de bovenliggende struct, kan de leesbaarheid in sommige gevallen worden verbeterd. Het wordt echter afgeraden om dit te gebruiken wanneer de substruct expliciet moet worden weergegeven.
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 zijn de volgende verschillen aanwezig:
1w.PrintAll() // aanroep van automatische promotie via een naamloze struct, niet w.Friut.PrintAll()
2Beide voorbeelden tonen de volgende belangrijke punten:
3- main is beknopt, functies zijn gesegmenteerd per functionaliteit
4- Verschillende objecten voor verschillende structs
5- Bij behoefte aan delen, gebruik interne structs
6
7Welke voordelen biedt deze programmeerfilosofie?
8
9## Voordelen
10
11- Duidelijke scheiding tussen methoden die gedeeld moeten worden en methoden die niet gedeeld moeten worden
12- Verdeling van verantwoordelijkheid over individuele structs en methoden
13- Structureel gescheiden code volgens de specificaties van de vereiste functionaliteit
14
15
16In het begin kan Go-taal onbekend zijn, omdat het afwijkt van traditionele OOP, maar naarmate men er vertrouwd mee raakt, maakt het expliciet programmeren mogelijk.
17
18## Samenvatting
19- Isoleer verantwoordelijkheden
20- Splits gedetailleerd op in struct-eenheden
21- Methoden moeten niet worden begrepen als abstracte klassen in Java
22- Programmeer expliciet en concreet
23Go-taal moet eenvoudiger en individueler worden behandeld dan traditionele OOP-modellen. Laten we code schrijven die stapsgewijs en structureel is opgesplitst, in plaats van extensief te programmeren.
24