Interfețele Go și inserarea structurilor (struct embedding): diferențele față de moștenirea claselor și proiectarea prin compoziție

Prezentare generală
Interfețele din Go permit ca mai multe structuri să aibă cu ușurință funcții cu aceiași argumenți și aceleași valori de returnare, dar acest lucru diferă de modul în care extends din Java prelungește și suprascrie în mod adecvat comportamentul acelor funcții interne. Doar înțelegând corect reutilizarea compozițională a codului în Go nu vom confunda lucrurile cu moștenirea, însă a obține o înțelegere teoretică perfectă de la bun început este dificil. Să le explorăm împreună printr-un scenariu în care se greșește ușor.
Greșeli frecvente
Începătorii pot face următoarele greșeli.
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}
Dacă gândiți că Go urmează moștenirea tradițională, acest cod pare fără probleme. Totuși, rezultatul său este următorul.
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
Aici comportamentul Go devine doar mai clar prin:
1watermelon := apple
Această linie nu transformă deloc Apple în clasa Watermelon. Watermelon este pur și simplu un pointer către apple.
Subliniem din nou: Go nu urmează conceptul tradițional de moștenire.
Dacă scriem cod având această înțelegere greșită, apar erori grave precum crearea de pointeri inutili sau copierea neintenționată a funcțiilor pentru alte structuri.
Atunci, care este codul exemplar?
Un exemplu potrivit în 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}
Totuși, în Go este posibil și să se obțină un aspect asemănător moștenirii. Este vorba de embedding anonim. Acesta se realizează declarând structura internă fără nume. În acest caz, câmpurile structurii inferioare pot fi accesate direct, chiar fără a le specifica explicit. Folosind acest model de promovare a câmpurilor structurii inferioare către structura superioară, lizibilitatea poate fi îmbunătățită în anumite situații. Totuși, în cazurile în care structura inferioară trebuie prezentată explicit, recomandăm să nu se folosească această metodă.
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}
În acest exemplu există următoarea diferență.
1w.PrintAll() // Apelare automată prin promovare, realizată prin structura anonimă, în loc de w.Friut.PrintAll()
Pentru ambele exemple, punctele importante sunt următoarele:
- main să fie simplu, funcțiile pe funcționalități
- structuri diferite înseamnă obiecte diferite
- pentru partajare, se folosește structura internă
Ce avantaje are această filozofie de programare?
Avantaje
- Distincție clară între metodele care necesită partajare și cele care nu
- Separarea responsabilităților pe structuri și metode individuale
- Cod separat structural, în funcție de specificațiile funcționalităților necesare
La început, Go poate părea neobișnuit deoarece diferă de OOP tradițional, dar odată ce vă obișnuiți, devine posibilă programarea explicită.
Rezumat
- Izolați responsabilitățile
- Împărțiți pe niveluri de structuri
- Metodele nu trebuie înțelese ca clasele abstracte din Java
- Scrieți programe explicite și concrete Go trebuie tratat mai simplu și mai clar decât modelul OOP tradițional, componente separate individual. În loc să programăm extensiv, să scriem cod separat în pași și structurat.