Go interfészek és struktúra-beágyazás: a klasszikus öröklődéstől való eltérések és a kompozíciós tervezés

Áttekintés
A Go interfészei megkönnyítik, hogy több struktúrának legyen azonos argumentumokkal és visszatérési értékekkel rendelkező függvénye, azonban ez eltér attól a módtól, ahogy a Java extends kulcsszava segítségével a belső függvények viselkedése megfelelően kiterjeszthető és felülírható. A Go kompozíción alapuló kódújrafelhasználását csak akkor értjük meg igazán, ha az nem keveredik az öröklődéssel, ugyanakkor a kezdetektől fogva elméletileg teljesen tökéletes megértés nehezen érhető el. Vizsgáljuk meg együtt olyan forgatókönyveken keresztül, amelyekben könnyen hibázhatunk.
Gyakori hibák
A kezdők a következő hibákat követhetik el.
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}
Ha valaki azt hiszi, hogy a Go hagyományos öröklődést követ, a fenti kód problémamentesnek tűnhet. Ennek kimenete azonban a következő:
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
Itt a Go működése csak ebben az esetben válik egyértelművé:
1watermelon := apple
Ez a kód egyáltalán nem alakítja át az Apple-t Watermelon osztállyá. A watermelon egyszerűen csak az apple-re mutató pointer.
Ismételten hangsúlyozzuk: a Go nem követi a hagyományos öröklődés fogalmát.
Ha ezzel a félreértéssel írunk kódot, olyan súlyos hibákhoz juthatunk, mint a értelmetlen pointer-létrehozás vagy a váratlan módon más struktúrák számára történő függvénymásolás.
Akkor milyen a mintaszerű kód?
Megfelelő példa Go nyelven
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}
Go-ban azonban lehetséges, hogy az öröklődéshez hasonló módon járjunk el. Ez az anonim beágyazás (embedding), amely úgy valósítható meg, hogy a belső struktúrát névtelen struktúraként deklaráljuk. Ebben az esetben az alstruktúra mezői külön megjelölés nélkül is közvetlenül elérhetők. Ha az alstruktúra mezőit a felső struktúrába emeljük (promóció), ez egyes esetekben javíthatja az olvashatóságot. Ha azonban az alstruktúrát explicit módon kell megjeleníteni, ezt a megoldást nem javasoljuk.
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}
Ebben a példában a következő különbség figyelhető meg:
1w.PrintAll() // w.Friut.PrintAll() helyett, automatikus promóció a névtelen struktúrán keresztül
Mindkét példa esetében a következő pontok a lényegesek:
- a main legyen egyszerű, a függvények pedig funkció szerint legyenek szétválasztva
- különböző struktúrákhoz különböző objektumokat használjunk
- ha megosztásra van szükség, használjunk belső struktúrát
Milyen előnyei vannak ennek a programozási szemléletnek?
Előnyök
- Egyértelmű különbségtétel a megosztott és nem megosztott metódusok között
- A felelősség szétválasztása az egyes struktúrák és metódusok között
- Szerkezetileg szétválasztott kód a szükséges funkcionális specifikáció szerint
Kezdetben a Go nyelv eltérése a hagyományos OOP-től szokatlanul hathat, de megszokva lehetővé teszi az explicit programozást.
Összefoglalás
- Izoláljuk a felelősségi köröket
- Részletezzük a bontást struktúra szinten
- A metódusokat nem szabad a Java absztrakt osztályaihoz hasonlóan értelmezni
- Törekedjünk explicit és konkrét programozásra A Go nyelv egyszerűbb és közvetlenebb, mint a hagyományos OOP modell, és egyedileg kell kezelni. Ahelyett, hogy kiterjesztésekben gondolkodnánk, lépésről lépésre, strukturáltan szétválasztva írjunk kódot.