Interfaces et embarquement de structures en Go : différences avec l'héritage de classes et conception par composition

Aperçu
Les interfaces Go permettent facilement à plusieurs structures de posséder des fonctions ayant 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 redéfinit (override) le comportement de ces fonctions internes. Il ne faut pas confondre la réutilisation de code compositionnelle de Go avec l'héritage, mais il est difficile d'en obtenir une compréhension théorique parfaite dès le départ. Explorons-la à travers des scénarios où l'on commet facilement des erreurs.
Erreurs fréquentes
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 ne poser aucun problème si l'on croit que Go suit l'héritage traditionnel. Cependant, sa sortie est la suivante.
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 simplement clair.
1watermelon := apple
Ce code ne convertit en aucun cas Apple en une structure Watermelon. watermelon est simplement un pointeur vers apple.
Il faut le redire ici : Go ne suit pas le concept traditionnel d'héritage.
Si l'on écrit du code avec cette incompréhension, on obtient des erreurs graves, comme la création inutile de pointeurs ou la copie de fonctions vers des structures inattendues.
Alors, à quoi ressemble un code exemplaire ?
Exemple approprié en 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 tout de même possible de faire en sorte qu'en Go cela ressemble à de l'héritage. C'est l'exemple de l'embedding anonyme. Cela est possible en déclarant la structure interne comme une structure sans nom. Dans ce cas, on peut accéder directement aux champs de la sous-structure sans les spécifier explicitement. En utilisant ce modèle de promotion des champs de la sous-structure vers la structure parente, la lisibilité peut parfois être améliorée. Toutefois, il est recommandé de ne pas l'utiliser lorsque la sous-structure doit être explicitement visible.
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, on observe la différence suivante.
1w.PrintAll() // Appel automatique par promotion via la structure sans nom, et non w.Friut.PrintAll()
Les deux exemples reposent sur les points importants suivants :
- main doit rester simple, et les fonctions doivent être organisées par fonctionnalité
- Pour des structures différentes, utiliser des objets différents
- Lorsqu'un partage est nécessaire, utiliser une structure interne
Quels sont les avantages de cette philosophie de programmation ?
Avantages
- Distinction claire entre les méthodes qui doivent être partagées et celles qui ne le doivent pas
- Séparation des responsabilités entre structures et méthodes individuelles
- Code structurellement séparé selon les spécifications des fonctionnalités requises
Au début, Go peut sembler peu familier car il diffère de la POO traditionnelle, mais une fois habitué, il permet une programmation explicite.
Résumé
- Isolez les responsabilités
- Découpez en détail, structure par structure
- Il ne faut pas comprendre les méthodes comme des classes abstraites de Java
- Pratiquez une programmation explicite et concrète Go doit être traité de manière plus simple, plus claire et plus indépendante que le modèle POO traditionnel. Plutôt que de programmer de manière extensible, écrivons du code séparé de façon progressive et structurée.