GoSuda

Interfacce Go e struct embedding: differenze rispetto all'ereditarietà di classe e progettazione basata sulla composizione

By Yunjin Lee views ...

Panoramica

Le interfacce di Go permettono di avere facilmente, in più strutture, funzioni con gli stessi argomenti e valori di ritorno, ma questo non corrisponde al modo in cui la parola chiave extends di Java estende e sovrascrive in modo appropriato il comportamento delle funzioni interne. Solo comprendendo correttamente il riuso compositivo del codice in Go è possibile non confonderlo con l'ereditarietà, ma è difficile raggiungere subito una comprensione teorica perfetta. Vediamo insieme alcuni scenari in cui è facile commettere errori.

Errori frequenti

I principianti possono commettere i seguenti errori.

 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}

Un codice del genere sembra non avere problemi se si crede che Go segua l'ereditarietà tradizionale. Tuttavia, il suo output è il seguente.

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

Qui il comportamento di Go diventa semplicemente chiaro.

1watermelon := apple

Questo codice non converte affatto Apple nella classe Watermelon. Semplicemente, watermelon è soltanto un puntatore ad apple.

Lo ribadiamo: Go non segue il concetto tradizionale di ereditarietà.

Scrivere codice con questo fraintendimento porta a errori critici, come la creazione di puntatori inutili o la copia di funzioni in strutture non previste.

Quale sarebbe allora un codice esemplare?

Esempio appropriato in 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}

Tuttavia, in Go è possibile anche far apparire qualcosa come ereditarietà. È l'esempio dell'incorporamento anonimo. Ciò è possibile dichiarando la struttura interna come struttura senza nome. In questo caso, si può accedere direttamente ai campi della sottostruttura anche senza specificarla esplicitamente. Utilizzando questo schema di promozione dei campi della sottostruttura alla struttura superiore, in alcuni casi si può migliorare la leggibilità. Tuttavia, si sconsiglia di usarlo quando è necessario mostrare esplicitamente la sottostruttura.

 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}

In questo esempio c'è la seguente differenza.

1w.PrintAll() // Non w.Friut.PrintAll(), ma una chiamata automatica promossa tramite la struttura senza nome

Entrambi gli esempi condividono questi punti fondamentali:

  • main sintetico, funzioni suddivise per funzionalità
  • strutture diverse richiedono oggetti diversi
  • per la condivisione, utilizzare una struttura interna

Quali vantaggi offre questa filosofia di programmazione?

Vantaggi

  • Distinzione chiara tra metodi condivisi e non condivisi
  • Separazione delle responsabilità tra strutture e metodi individuali
  • Codice strutturalmente separato in base alle specifiche delle funzionalità richieste

All'inizio Go può risultare poco familiare perché differisce dall'OOP tradizionale, ma una volta acquisita familiarità diventa possibile una programmazione esplicita.

Riepilogo

  • Isoliamo le responsabilità
  • Suddividiamo nel dettaglio per unità di struttura
  • I metodi non devono essere intesi come le classi astratte di Java
  • Programmiamo in modo esplicito e concreto Go deve essere trattato come un modello più semplice, chiaro e modulare rispetto al modello OOP tradizionale. Anziché programmare in modo espansivo, scriviamo il codice separandolo in modo graduale e strutturato.