Champs d'interface Go

105

Je connais le fait que, dans Go, les interfaces définissent la fonctionnalité plutôt que les données. Vous placez un ensemble de méthodes dans une interface, mais vous ne pouvez pas spécifier de champs qui seraient requis sur tout ce qui implémente 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 faire est quelque chose 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 embarquées, j'ai découvert un moyen de le faire, d'une certaine 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 Person a. Il implémente également l'interface PersonProvider, afin que nous puissions transmettre à Bob 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 Go Playground 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 ces données intégrées et qui ne connaissent pas la nature de la structure externe. Et tout est vérifié au moment de la compilation! (La seule façon dont vous pourriez gâcher, que je peux voir, serait d'incorporer l'interface PersonProviderdans Bob, plutôt que dans un béton Person. Cela compilerait et échouerait à l'exécution.)

Maintenant, voici ma question: est-ce un bon truc, ou devrais-je le faire différemment?

Matt Mc
la source
4
"Je peux créer une interface qui définit les données plutôt que le comportement". Je dirais que vous avez un comportement qui renvoie des données.
jmaloney
Je vais écrire une réponse; Je pense que c'est bien si vous en avez besoin et que vous en connaissez les conséquences, mais il y a des conséquences et je ne le ferais pas tout le temps.
twotwotwo
@jmaloney Je pense que vous avez raison, si vous vouliez le regarder clairement. Mais globalement, avec les différentes pièces que j'ai montrées, la sémantique devient "cette fonction accepte toute structure qui a un ___ dans sa composition". Du moins, c'est ce que je voulais.
Matt Mc
1
Ce n'est pas du matériel de «réponse». Je suis arrivé à votre question en recherchant sur Google "interface as struct property golang". J'ai trouvé une approche similaire en définissant une structure qui implémente une interface en tant que propriété d'une autre structure. Voici le terrain de jeu, play.golang.org/p/KLzREXk9xo Merci de m'avoir donné quelques idées.
Dale
1
Rétrospectivement, et après 5 ans d'utilisation de Go, il est clair pour moi que ce qui précède n'est pas Go idiomatique. C'est une tension vers les génériques. Si vous vous sentez tenté de faire ce genre de chose, je vous conseille de repenser l'architecture de votre système. Acceptez les interfaces et retournez les structures, partagez en communiquant et réjouissez-vous.
Matt Mc

Réponses:

55

C'est définitivement une belle astuce. Cependant, exposer des pointeurs permet toujours d'accéder directement aux données, de sorte qu'il ne vous procure qu'une flexibilité supplémentaire limitée pour les modifications futures. De plus, les conventions Go ne vous obligent pas à toujours placer une abstraction devant vos attributs de données .

En prenant ces choses ensemble, je tendrais vers un extrême ou l'autre pour un cas d'utilisation donné: soit a) il suffit de créer un attribut public (en utilisant l'incorporation le cas échéant) et de passer des types concrets, soit b) s'il semble que l'exposition des données causer des problèmes plus tard, exposez un getter / setter pour une abstraction plus robuste.

Vous allez peser cela sur une base par attribut. Par exemple, si certaines données sont spécifiques à l'implémentation ou si vous prévoyez de changer les représentations pour une autre raison, vous ne souhaitez probablement pas exposer l'attribut directement, alors que d'autres attributs de données peuvent être suffisamment stables pour que les rendre publics soit une nette victoire.


Le masquage des propriétés derrière les getters et les setters vous donne une flexibilité supplémentaire pour apporter des modifications rétrocompatibles ultérieurement. Disons que vous voulez un jour changer Personpour stocker non seulement un seul champ "nom", mais le premier / milieu / dernier / préfixe; si vous avez des méthodes Name() stringet SetName(string), vous pouvez Personsatisfaire les utilisateurs existants de l' interface tout en ajoutant de nouvelles méthodes plus fines. Ou vous voudrez peut-être être en mesure de marquer un objet sauvegardé par une base de données comme "sale" lorsqu'il a des modifications non enregistrées; vous pouvez le faire lorsque les mises à jour des données passent toutes par des SetFoo()méthodes.

Donc: avec les getters / 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 car personne ne peut se passer de p.Name = "bob"passer par votre code.

Cette flexibilité est plus pertinente lorsque le type est compliqué (et que la base de code est grande). Si vous avez un PersonCollection, il peut être sauvegardé en interne par un sql.Rows, un []*Person, un []uintdes ID de base de données, ou autre. En utilisant la bonne interface, vous pouvez éviter aux appelants de se soucier de ce que c'est, de la façon dont io.Readerles connexions réseau et les fichiers se ressemblent.

Une chose spécifique: interfaces dans Go ont la propriété particulière que vous pouvez en implémenter un 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 simplement des chaînes ou autre, tous PersonProvidersdoivent importer le package où Personest défini. Cela peut être bien ou même inévitable; c'est juste une conséquence à connaître.


Mais encore une fois, la communauté Go n'a pas de convention forte contre l'exposition des membres de données dans l'API publique de votre type . Il est laissé à votre jugement s'il est raisonnable d'utiliser l'accès public à un attribut dans le cadre de votre API dans un cas donné, plutôt que de décourager toute exposition, car cela pourrait éventuellement compliquer ou empêcher un changement d'implémentation plus tard.

Ainsi, par exemple, le stdlib fait des choses comme vous permettre d'initialiser un http.Serveravec votre configuration et promet qu'un zéro bytes.Bufferest utilisable. C'est bien de faire vos propres trucs comme ça, et, en fait, je ne pense pas que vous devriez faire abstraction des choses de manière préventive si la version plus concrète et exposant les données semble susceptible de fonctionner. Il s'agit simplement d'être conscient des compromis.

deux
la source
Une chose supplémentaire: l'approche d'intégration est un peu plus comme l'héritage, non? Vous obtenez tous les champs et méthodes de la structure intégrée, et vous pouvez utiliser son interface afin que tout superstruct soit qualifié, sans réimplémenter des ensembles d'interfaces.
Matt Mc
Ouais - un peu comme l'héritage virtuel dans d'autres langues. Vous pouvez utiliser l'incorporation pour implémenter une interface, qu'elle soit définie en termes de getters et de setters ou d'un pointeur vers les données (ou, une troisième option pour l'accès en lecture seule à de minuscules structures, une copie de la structure).
twotwotwo
Je dois dire que cela me donne des flashbacks sur 1999 et que j'apprends à écrire des rames de getters et de setters standard en Java.
Tom le
C'est dommage que la bibliothèque standard de Go ne fasse pas toujours cela. Je suis en train d'essayer de simuler certains appels à os.Process pour les tests unitaires. Je ne peux pas simplement envelopper l'objet de processus dans une interface car la variable membre Pid est accessible directement et les interfaces Go ne prennent pas en charge les variables membres.
Alex Jansen le
1
@Tom C'est vrai. Je ne pense getters / setters ajouter plus de flexibilité que d' exposer un pointeur, mais je aussi ne pense pas que tout le monde devrait getter / setter tout-ifier (ou que cela correspond un style typique Go). J'avais auparavant quelques mots pour faire signe à cela, mais j'ai révisé le début et la fin pour le souligner beaucoup plus.
twotwotwo le
2

Si je comprends bien, vous souhaitez remplir un champ struct dans un autre. Mon avis de ne pas utiliser d'interfaces pour s'étendre. Vous pouvez facilement le faire par la prochaine approche.

package main

import (
    "fmt"
)

type Person struct {
    Name        string
    Age         int
    Citizenship string
}

type Bob struct {
    SSN string
    Person
}

func main() {
    bob := &Bob{}

    bob.Name = "Bob"
    bob.Age = 15
    bob.Citizenship = "US"

    bob.SSN = "BobSecret"

    fmt.Printf("%+v", bob)
}

https://play.golang.org/p/aBJ5fq3uXtt

Remarque Persondans la Bobdéclaration. Cela rendra le champ struct inclus disponible dans la Bobstructure directement avec du sucre syntaxique.

Igor A. Melekhine
la source