Je connais bien le fait que, dans Go, les interfaces définissent la fonctionnalité plutôt que les données. Vous mettez un ensemble de méthodes dans une interface, mais vous ne pouvez pas spécifier de champs qui seraient requis sur tout élément implémentant cette interface.
Par exemple:
// Interface
type Giver interface {
Give() int64
}
// One implementation
type FiveGiver struct {}
func (fg *FiveGiver) Give() int64 {
return 5
}
// Another implementation
type VarGiver struct {
number int64
}
func (vg *VarGiver) Give() int64 {
return vg.number
}
Nous pouvons maintenant utiliser l'interface et ses implémentations:
// A function that uses the interface
func GetSomething(aGiver Giver) {
fmt.Println("The Giver gives: ", aGiver.Give())
}
// Bring it all together
func main() {
fg := &FiveGiver{}
vg := &VarGiver{3}
GetSomething(fg)
GetSomething(vg)
}
/*
Resulting output:
5
3
*/
Maintenant, ce que vous ne pouvez pas est comme ceci:
type Person interface {
Name string
Age int64
}
type Bob struct implements Person { // Not Go syntax!
...
}
func PrintName(aPerson Person) {
fmt.Println("Person's name is: ", aPerson.Name)
}
func main() {
b := &Bob{"Bob", 23}
PrintName(b)
}
Cependant, après avoir joué avec les interfaces et les structures intégrées, j'ai découvert un moyen de le faire, à la manière:
type PersonProvider interface {
GetPerson() *Person
}
type Person struct {
Name string
Age int64
}
func (p *Person) GetPerson() *Person {
return p
}
type Bob struct {
FavoriteNumber int64
Person
}
En raison de la structure intégrée, Bob a tout ce que la personne a. Il implémente également l'interface PersonProvider afin que nous puissions passer à Bob dans des fonctions conçues pour utiliser cette interface.
func DoBirthday(pp PersonProvider) {
pers := pp.GetPerson()
pers.Age += 1
}
func SayHi(pp PersonProvider) {
fmt.Printf("Hello, %v!\r", pp.GetPerson().Name)
}
func main() {
b := &Bob{
5,
Person{"Bob", 23},
}
DoBirthday(b)
SayHi(b)
fmt.Printf("You're %v years old now!", b.Age)
}
Voici un terrain de je qui illustre le code ci-dessus.
En utilisant cette méthode, je peux créer une interface qui définit les données plutôt que le comportement et qui peut être implémentée par n'importe quelle structure simplement en incorporant ces données. Vous pouvez définir des fonctions qui interagissent explicitement avec les données incorporées et ignorent la nature de la structure externe. Et tout est vérifié au moment de la compilation! (La seule façon dont vous pourriez vous tromper, ce que je peux voir, serait d’intégrer l’interface PersonProvider
dans Bob
, plutôt qu’un béton concret Person
. Il compilerait et échouerait à runtime.)
Maintenant, voici ma question: est-ce une astuce, ou devrais-je le faire différemment?
C'est certainement une astuce, et fonctionne tant que vous êtes cool en donnant accès à ces champs dans le cadre de votre API. L’autre solution envisageable consiste à conserver le fichier struct/interface
incorporable, mais à définir l’interface en termes de getters et de setters.
Le fait de masquer les propriétés derrière les accesseurs et les régulateurs vous donne une flexibilité supplémentaire pour effectuer ultérieurement des modifications compatibles avec les versions antérieures. Supposons que vous souhaitiez un jour changer Person
pour stocker non pas un seul champ "nom", mais également le premier/le milieu/le dernier/le préfixe; si vous avez les méthodes Name() string
et SetName(string)
, vous pouvez contenter les utilisateurs existants de l'interface Person
tout en ajoutant de nouvelles méthodes plus fines. Vous pouvez également vouloir pouvoir marquer un objet sauvegardé dans une base de données comme "sale" lorsqu'il contient des modifications non enregistrées; vous pouvez le faire lorsque toutes les mises à jour de données passent par les méthodes SetFoo()
.
Donc, avec les accesseurs/setters, vous pouvez modifier les champs de structure tout en conservant une API compatible, et ajouter une logique autour de la propriété get/sets puisque personne ne peut simplement faire p.Name = "bob"
sans passer par votre code.
Cette flexibilité est plus pertinente lorsque votre type fait quelque chose de plus compliqué. Si vous avez un PersonCollection
, il pourrait être soutenu en interne par un sql.Rows
, une []*Person
, une []uint
des ID de base de données, ou autre chose. En utilisant la bonne interface, vous pouvez éviter aux appelants de se préoccuper de la manière dont ils se trouvent io.Reader
rend les connexions réseau et les fichiers se ressemblent.
Une chose spécifique: interface
s dans Go possède la propriété particulière que vous pouvez implémenter sans importer le package qui le définit; cela peut vous aider éviter les importations cycliques . Si votre interface renvoie un *Person
, au lieu de chaînes ou peu importe, tous PersonProviders
doivent importer le paquet où Person
est défini. Cela peut être bien ou même inévitable; c'est juste une conséquence à connaître.
Cela étant dit, il n'y a pas de convention Go qui vous oblige à cacher toutes vos données. (C’est une différence bienvenue par rapport à C++, par exemple.) Stdlib fait des choses comme vous permet d’initialiser un http.Server
avec votre configuration et vous promet qu’un zéro bytes.Buffer
est utilisable. C'est bien de faire ce que vous voulez comme ça, et en effet, je ne pense pas que vous ayez à faire une abstraction prématurée si la version plus concrète, exposant les données, fonctionne. Il s'agit simplement d'être au courant des compromis.