GoSuda

Go-gränssnitt och strukturinbäddning: skillnader jämfört med klassarv och kompositionsdesign

By Yunjin Lee views ...

Översikt

Go-gränssnitt (interfaces) gör det enkelt för flera structar att ha funktioner med samma argument och returvärden, men det fungerar annorlunda än det sätt på vilket Javas extends-nyckelord utökar och överskuggar (overrides) beteendet hos de inneboende funktionerna. Man måste förstå Gos kompositionsbaserade kodåteranvändning ordentligt för att inte förväxla den med arv, men det är svårt att uppnå en teoretiskt fullständig förståelse från början. Låt oss undersöka detta tillsammans, med scenarier där misstag lätt uppstår.

Vanliga misstag

Nybörjare kan göra följande misstag.

 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}

Om man antar att Go följer traditionellt arv verkar denna kod vara utan problem. Dess utdata är dock följande.

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

Här blir Gos beteende endast tydligt med följande kod.

1watermelon := apple

Denna kod omvandlar inte Apple till en Watermelon-klass. Variabeln watermelon är helt enkelt en pekare till apple.

Låt oss betona igen: Go följer inte det traditionella arvsbegreppet.

Om man skriver kod med denna missuppfattning uppstår allvarliga fel, såsom meningslösa pekarskapelser och oväntat kopierade funktioner för andra structar.

Hur ser då en mustergill kod ut?

Lämpligt exempel 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}

Go gör dock det möjligt att få det att se ut som arv. Det handlar om anonym inbäddning (embedding). Detta sker genom att deklarera den inre structen utan namn. I detta fall går det att komma åt fälten i den underordnade structen direkt, utan att uttryckligen ange dem. Genom att använda mönstret att lyfta upp (promote) fälten från den underordnade structen till den överordnade kan läsbarheten förbättras i vissa fall. Om den underordnade structen måste synas explicit rekommenderas dock att inte använda detta.

 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 detta exempel finns följande skillnad.

1w.PrintAll() // Anropas automatiskt via den anonyma structen genom promotion, i stället för w.Fruit.PrintAll()

Båda exemplen har följande viktiga poänger:

  • Håll main enkel och dela upp funktionerna efter funktionalitet
  • Använd olika objekt för olika structar
  • Använd en intern struct när delning krävs

Vilka fördelar har denna programmeringsfilosofi?

Fördelar

  • Tydlig åtskillnad mellan metoder som behöver delas och de som inte gör det
  • Ansvarsfördelning mellan enskilda structar och metoder
  • Kod som är strukturellt separerad efter specificerade funktionskrav

Till en början kan Go kännas ovant eftersom det skiljer sig från traditionell OOP, men när man vant sig blir det möjligt att programmera explicit.

Sammanfattning

  • Isolera ansvarsområden
  • Dela upp i detalj per struct
  • Metoder ska inte förstås som abstrakta klasser i Java
  • Programmera explicit och konkret Go är enklare och mer okomplicerat än traditionella OOP-modeller och bör hanteras individuellt. Snarare än att programmera för utvidgning, låt oss skriva kod med stegvis och strukturerad separation.