Dans le langage Go, la prise en charge de la composition est réalisée par le mécanisme d'embedding — c'est la possibilité d'inclure une structure à l'intérieur d'une autre sans nommer explicitement de champ. On supposait que cette approche permettrait de modéliser l'héritage "à sa manière", tout en conservant la simplicité du langage et en évitant de nombreuses complications liées à l'héritage multiple.
Le développeur souhaite souvent étendre le comportement ou l'interface d'une certaine structure de base sans complexifier l'architecture. En Go, on utilise souvent l'embedding pour cela, ce qui permet d'accéder directement aux méthodes et aux champs du type imbriqué, comme s'ils étaient définis dans la structure parent. Mais l'embedding a ses propres particularités, et une compréhension incorrecte du mécanisme peut conduire à des erreurs inattendues, telles que des conflits de noms, le double embedding et l'héritage incorrect des méthodes.
Une utilisation correcte et parcimonieuse de l'embedding permet d'obtenir une composition plus propre. Il convient de se rappeler que les méthodes et les champs "remontent" uniquement d'un niveau, et que l'embedding représente précisément une relation "has-a" et non "is-a". Il est nécessaire d'éviter les conflits de noms et de comprendre comment fonctionne la méthode réceptrice.
Exemple de code :
package main import "fmt" type Engine struct { Power int } func (e Engine) Start() { fmt.Println("Moteur démarré avec une puissance", e.Power) } type Car struct { Engine // embedding, et non un champ Engine comme engine Engine Brand string } func main() { c := Car{Engine: Engine{Power: 200}, Brand: "Toyota"} c.Start() // accessible directement fmt.Println(c.Power) // le champ est aussi remonté }
Caractéristiques clés :
L'embedding peut-elle réaliser l'héritage multiple ?
Non, l'embedding ne réalise pas l'héritage comme dans la POO, c'est de la composition. Dans une structure, plusieurs autres peuvent être imbriquées, mais en cas de méthodes coincidentes, un conflit de compilation se produit, et non une fusion.
Que se passe-t-il si les champs des structures imbriquées ont des noms identiques ?
Il y aura une erreur de compilation en cas d'accès direct : le compilateur ne sait pas à quel champ accéder. Il est nécessaire d'indiquer explicitement le chemin via le nom de la structure imbriquée.
Exemple de code :
type A struct {X int} type B struct {X int} type C struct { A B } func main() { c := C{} // c.X = 1 // erreur : sélecteur ambigu c.A.X = 1 // uniquement ainsi }
Les méthodes avec valeur et pointeur remontent-elles de la même manière lors de l'embedding ?
Non. Les méthodes avec récepteur de pointeur ne remontent que si la structure utilise également un pointeur (c := &Car{}). Si la structure est par valeur, les méthodes avec récepteur de pointeur ne "remonteront" pas.
Un projet avec une structure profondément imbriquée, où l'embedding est utilisé pour simuler une héritage multi-niveaux. Un novice ne comprend pas d'où provient le champ ou la méthode, et tout le projet devient compliqué et "magique".
Avantages :
Inconvénients :
L'embedding est utilisé pour réaliser une interface, par exemple, l'interface Logger est réalisée par l'embedding d'un type avec la méthode Println, et cela est clairement documenté et couvert par des tests.
Avantages :
Inconvénients :