GoSuda

Go interfaces are not inheritance

By Yunjin Lee
views ...

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 :

  • main doit ê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.