Go-rajapinnat ja rakenteiden upottaminen: erot luokkaperintään ja koostamiseen perustuvaan suunnitteluun

Yleiskatsaus
Go-rajapinnat mahdollistavat sen, että useilla structeilla on helposti funktioita, joilla on samat argumentit ja paluuarvot, mutta tämä eroaa Javan extends-avainsanan tavasta, jolla funktion sisäistä toimintaa laajennetaan ja ylikirjoitetaan asianmukaisesti. Go-kielen kompositiivisen koodin uudelleenkäytön ymmärtäminen on välttämätöntä, jotta sitä ei sekoiteta periytymiseen, mutta teoreettisesti täydellinen ymmärrys alusta alkaen on vaikeaa. Tutustutaan siihen virheherkkien skenaarioiden avulla.
Yleisiä virheitä
Aloittelijat saattavat tehdä seuraavanlaisia virheitä.
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}
Jos ajattelee, että Go noudattaa perinteistä periytymistä, tämä koodi näyttää ongelmattomalta. Sen tulostus on kuitenkin seuraava.
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
Tässä Go-kielen toiminta selkeytyy vain vähän:
1watermelon := apple
Tämä koodi ei lainkaan muunna Applea sellaisenaan Watermelon-luokaksi. Watermelon on pelkästään osoitin apple-muuttujaan.
Tässä on vielä kerran korostettava: Go ei noudata perinteistä periytymiskäsitettä.
Jos kirjoittaa koodia tällaisen väärinkäsityksen vallitessa, seurauksena on merkityksettömiä osoittimia, odottamatonta funktioiden kopiointia muille structeille ja muita vakavia virheitä.
Millainen on sitten mallikelpoinen koodi?
Sopiva esimerkki Go-kielellä
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-kielessä on kuitenkin mahdollista saada aikaan periytymistä muistuttava rakenne. Kyseessä on anonyymi upotus. Se toteutuu, kun sisempi struct määritellään nimettömänä structina. Tässä tapauksessa alemman structin kenttiin voi päästä käsiksi ilman erillistä määritystä suoraan. Kun käytetään tätä alemman structin kenttien nostamista ylemmälle tasolle (promotion), luettavuus voi joissakin tapauksissa parantua. Jos alempi struct on kuitenkin tarpeen näyttää eksplisiittisesti, suosittelemme, ettei tätä menetelmää käytetä.
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}
Tässä esimerkissä on seuraava ero.
1w.PrintAll() // w.Friut.PrintAll()이 아닌, 이름 없는 구조체를 통한 자동 승격 호출
Molemmissa esimerkeissä tärkeät kohdat ovat seuraavat:
- main pidetään yksinkertaisena, funktiot jaotellaan toiminnoittain
- eri structeille käytetään eri olioita
- kun jaettavuutta tarvitaan, käytetään sisempää structia
Mitä etua tällaisesta ohjelmointifilosofiasta on?
Edut
- Selkeä erottelu metodien välillä, jotka tarvitsevat jaettavuutta, ja niiden, jotka eivät tarvitse
- Vastuualueiden erottaminen yksittäisten structien ja metodien välillä
- Koodi, joka on rakenteellisesti jaettu tarvittavan toiminnallisuuden määrittelyn mukaan
Aluksi Go-kieli, joka poikkeaa perinteisestä OOP:stä, saattaa tuntua vieraalta, mutta totuttuaan siihen voi kirjoittaa eksplisiittistä ohjelmointia.
Yhteenveto
- Eristä vastuualueet
- Jaa koodi yksittäisten structien tasolla
- Metodeja ei tule ymmärtää kuten Javan abstrakteja luokkia
- Kirjoita eksplisiittistä ja konkreettista ohjelmointia Go-kieli on yksinkertaisempi ja selkeämpi kuin perinteinen OOP-malli, ja sen osat tulee käsitellä erikseen. Sen sijaan, että ohjelmoisi laajentavasti, kirjoita koodi vaiheittain ja rakenteellisesti erotellen.