Le interfacce Go non sono ereditarietà
Panoramica
Le interfacce Go consentono a diverse struct di avere facilmente funzioni con gli stessi argomenti e valori di ritorno, ma ciò differisce dal modo in cui la parola chiave extends di Java estende e sovrascrive adeguatamente il comportamento delle funzioni interne. Non si confonderà con l'ereditarietà solo se si comprende correttamente il riutilizzo del codice compositivo di Go, ma è difficile ottenere una comprensione teoricamente perfetta fin dall'inizio. Esaminiamo gli scenari in cui è facile commettere errori.
Errori comuni
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}
Questo codice potrebbe sembrare privo di problemi se si suppone erroneamente 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 una Apple in una classe Watermelon.
Piuttosto, watermelon è solo un puntatore ad apple.
Vorrei sottolineare ancora una volta che Go non segue il concetto tradizionale di ereditarietà.
Se si scrive codice con questa incomprensione, si verificheranno errori critici come la creazione di puntatori inutili e la copia inaspettata di funzioni per altre struct.
Qual è, 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, anche in Go è possibile far sì che sembri esserci ereditarietà. Un esempio è l'embedding anonimo. Ciò è possibile dichiarando la struct interna come una struct senza nome. In questi casi, è possibile accedere ai campi della struct inferiore anche senza specificarli esplicitamente. Utilizzando questo pattern per promuovere i campi della struct inferiore alla struct superiore, è possibile migliorare la leggibilità in alcuni casi. Tuttavia, si sconsiglia di usarlo quando la struct inferiore deve essere mostrata esplicitamente.
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, ci sono le seguenti differenze:
1w.PrintAll() // Chiamata di promozione automatica tramite una struct senza nome, non w.Friut.PrintAll()
Il punto importante in entrambi gli esempi è il seguente:
maindeve essere conciso, le funzioni devono essere separate per funzionalità.- Se si tratta di struct diverse, usare oggetti diversi.
- Se è necessaria la condivisione, usare struct interne.
Quali sono i vantaggi di una tale filosofia di programmazione?
Vantaggi
- Chiare distinzioni tra metodi che richiedono condivisione e quelli che non la richiedono.
- Separazione delle responsabilità per singole struct e metodi.
- Codice strutturalmente separato in base alle specifiche delle funzionalità richieste.
Inizialmente, il linguaggio Go potrebbe essere insolito a causa della sua differenza dall'OOP tradizionale, ma una volta acquisita familiarità, consente una programmazione esplicita.
Riepilogo
- Isoliamo le responsabilità.
- Suddividiamo in dettaglio per unità di struct.
- I metodi non devono essere intesi come classi astratte di Java.
- Pratichiamo una programmazione esplicita e concreta. Il linguaggio Go dovrebbe essere trattato in modo più semplice e individuale rispetto ai modelli OOP tradizionali. Invece di programmare in modo estensivo, dovremmo scrivere il codice in modo graduale e strutturalmente separato.