Go-grænseflader og strukturindlejring: forskelle fra klassearv og komposition som designmønster

Oversigt
Go-interfaces gør det let for flere structs at have funktioner med samme argumenter og returværdier, men det fungerer anderledes end Javas extends-nøgleord, som udvider og overskriver funktionernes adfærd. Man må forstå Gos kompositionsbaserede kodegenbrug korrekt for ikke at forveksle det med arv, men det er svært at opnå en teoretisk fuldstændig forståelse fra starten. Lad os se på det sammen med scenarier, hvor man let begår fejl.
Almindelige fejl
Nybegyndere kan begå følgende fejl.
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}
Hvis man tror, at Go følger traditionel arv, virker denne kode uden problemer. Dens outputresultat er dog følgende.
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
Her bliver Gos adfærd blot tydelig.
1watermelon := apple
Denne kode konverterer på ingen måde Apple direkte til Watermelon-klassen. Watermelon er blot en pointer til apple.
Det skal understreges igen: Go følger ikke det traditionelle arvebegreb.
Hvis man skriver kode med denne misforståelse, opstår alvorlige fejl såsom meningsløs oprettelse af pointere og uventet kopiering af funktioner til andre structs.
Hvordan ser en mønstergyldig kode så ud?
Passende eksempel i 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}
Det er dog muligt i Go at få det til at ligne arv på en måde. Det er eksemplet med anonym embedding. Dette opnås ved at deklarere den indre struct uden navn. I dette tilfælde kan man få direkte adgang til feltene i den underordnede struct uden at angive dem eksplicit. Ved at bruge mønsteret, hvor felterne i den underordnede struct forfremmes til den overordnede struct, kan læsbarheden i nogle tilfælde forbedres. Hvis den underordnede struct skal vises eksplicit, anbefales det dog ikke at bruge dette.
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}
I dette eksempel er der følgende forskel.
1w.PrintAll() // w.Friut.PrintAll()이 아닌, 이름 없는 구조체를 통한 자동 승격 호출
Begge eksempler har følgende vigtige punkter.
- Hold main enkel, og opdel funktioner efter funktionalitet
- Brug forskellige objekter til forskellige structs
- Brug en intern struct, når der er behov for deling
Hvilke fordele har denne programmeringsfilosofi?
Fordele
- Tydelig adskillelse mellem metoder, der skal deles, og dem, der ikke skal
- Adskillelse af ansvar mellem individuelle structs og metoder
- Kode, der er strukturelt adskilt efter de nødvendige funktionelle specifikationer
Til at begynde med kan Go føles uvant, da det adskiller sig fra traditionel OOP, men når man vænner sig til det, bliver eksplicit programmering mulig.
Resume
- Isoler ansvarsområder
- Opdel detaljeret på struct-niveau
- Metoder må ikke forstås som abstrakte klasser i Java
- Programmer eksplicit og konkret Go bør behandles mere enkelt og tydeligt end den traditionelle OOP-model, og hvert enkelt element bør håndteres separat. Skriv hellere trinvist og struktureret med adskilte dele end ekspansivt.