GoSuda

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

By Yunjin Lee views ...

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.