Go interfaces are not inheritance
Aperçu
Les interfaces Go permettent à plusieurs structs de posséder facilement des fonctions avec les mêmes arguments et valeurs de retour, mais cela diffère de la manière dont le mot-clé extends de Java étend et surcharge de manière appropriée le comportement des fonctions internes. Comprendre correctement la réutilisation de code compositionnelle de Go est essentiel pour éviter toute confusion avec l'héritage, mais une compréhension théorique parfaite dès le début est difficile. Examinons cela avec des scénarios propices aux erreurs.
Erreurs courantes
Les débutants peuvent commettre les erreurs suivantes :
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}
Ce code semble correct si l'on suppose que Go suit l'héritage traditionnel. Cependant, son résultat est le suivant :
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
Ici, le comportement de Go devient clair.
1watermelon := apple
Ce code ne convertit pas du tout Apple en une classe Watermelon.watermelon est simplement un pointeur vers apple.
Il est important de souligner à nouveau que Go ne suit pas le concept d'héritage traditionnel.
Si l'on écrit du code avec cette incompréhension, des erreurs critiques peuvent survenir, telles que la création de pointeurs inutiles ou la copie inattendue de fonctions destinées à d'autres structs.
Alors, quel serait un code exemplaire ?
Exemple approprié en langage 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}
Cependant, il est possible de faire en sorte que Go ressemble à de l'héritage. Ceci est un exemple d'intégration anonyme. Cela est possible en déclarant le struct interne comme un struct sans nom. Dans de tels cas, il est possible d'accéder aux champs du struct enfant sans les spécifier explicitement. L'utilisation de ce modèle qui provoque l'élévation des champs du struct enfant vers le struct parent peut améliorer la lisibilité dans certains cas. Cependant, il est déconseillé de l'utiliser lorsque le struct enfant doit être affiché explicitement.
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}
Dans cet exemple, il y a ces différences :
1w.PrintAll() // appel de promotion automatique via le struct sans nom, et non w.Friut.PrintAll()
Les points importants des deux exemples sont les suivants :
maindoit être simplifié, les fonctions doivent être organisées par fonctionnalité.- Si ce sont des structs différents, ce sont des objets différents.
- Si le partage est nécessaire, utilisez des structs internes.
Quels sont les avantages de cette philosophie de programmation ?
Avantages
- Distinction claire entre les méthodes nécessitant un partage et celles qui n'en nécessitent pas.
- Séparation des responsabilités pour les structs et les méthodes individuelles.
- Code structuré séparément selon les spécifications fonctionnelles requises.
Au début, le langage Go peut être inhabituel car il diffère de la POO traditionnelle, mais une fois habitué, il permet une programmation explicite.
Résumé
- Isolons la responsabilité.
- Divisons le code en unités de struct détaillées.
- Les méthodes ne doivent pas être comprises comme des classes abstraites en Java.
- Pratiquons une programmation explicite et concrète. Le langage Go doit être traité de manière simple et individuelle, plutôt que comme un modèle de POO traditionnel. Au lieu de programmer de manière extensible, écrivons le code de manière progressive et structurellement séparée.