GoSuda

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

By Yunjin Lee views ...

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.