Go-grensesnitt og strukturinnbygging: forskjeller fra klassearv og komposisjonsdesign

Oversikt
Go-grensesnitt (interfaces) gjør det enkelt for flere structer å ha funksjoner med samme argumenter og returverdier, men dette fungerer annerledes enn måten Javas extends-nøkkelord utvider og overstyrer oppførselen til de interne funksjonene på. Man må forstå Gos komposisjonsbaserte kodegjenbruk ordentlig for ikke å forveksle det med arv, men det er vanskelig å oppnå en teoretisk fullstendig forståelse fra starten. La oss se nærmere på dette med scenarioer der det er lett å gjøre feil.
Vanlige feil
Nybegynnere kan gjøre følgende feil.
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}
Dette kan se uproblematisk ut dersom man tror at Go følger tradisjonell arv. Utdataene fra koden er imidlertid som følger.
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 blir Gos oppførsel først tydelig.
1watermelon := apple
Denne koden konverterer ikke Apple til Watermelon-klassen. Watermelon er ganske enkelt en peker til apple.
Her gjentar vi igjen: Go følger ikke det tradisjonelle arvekonseptet.
Dersom man koder med denne misforståelsen, oppstår alvorlige feil som unødvendig opprettelse av pekere og kopiering av funksjoner til uvedkommende structer.
Hva er da en mønstergyldig kodeløsning?
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}
Likevel er det mulig å få Go til å se ut som arv. Det gjelder anonym innbygging (embedding). Dette gjøres ved å deklarere den indre structen uten navn. I så fall kan man få tilgang til feltene i underordnet struct uten å spesifisere dem eksplisitt. Ved å bruke dette mønsteret, der feltene i underordnet struct blir promotert til den overordnede structen, kan lesbarheten i noen tilfeller forbedres. Man bør likevel unngå det dersom den underordnede structen må vises eksplisitt.
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 eksemplet finnes følgende forskjell.
1w.PrintAll() // I stedet for w.Friut.PrintAll(), automatisk promotering kall via den anonyme structen
Begge eksemplene har følgende viktige poenger:
- Hold main enkel, og del funksjonene etter funksjonalitet
- Ulike structer bør ha ulike objekter
- Bruk indre structer når deling er nødvendig
Hvilke fordeler finnes det med denne programmeringsfilosofien?
Fordeler
- Tydelig skille mellom metoder som må deles og de som ikke må det
- Ansvar isoleres i separate structer og metoder
- Kode strukturelt delt etter spesifikasjonen av nødvendig funksjonalitet
Til å begynne med kan Go være uvant fordi det skiller seg fra tradisjonell OOP, men når man blir vant til det, muliggjør det eksplisitt programmering.
Sammendrag
- Isoler ansvarsområder
- Del opp detaljert på structnivå
- Metoder må ikke forstås som abstrakte klasser i Java
- Skriv eksplisitt og konkret kode Go er enklere og mer tydelig enn den tradisjonelle OOP-modellen og bør håndteres enkeltvis. Skriv heller trinnvis og strukturert, med separate deler, enn å programmere utvidbart.