Gehen Sie zu Schnittstellenfeldern

105

Ich bin mit der Tatsache vertraut, dass in Go Schnittstellen eher Funktionalität als Daten definieren. Sie fügen eine Reihe von Methoden in eine Schnittstelle ein, können jedoch keine Felder angeben, die für irgendetwas erforderlich wären, das diese Schnittstelle implementiert.

Beispielsweise:

// 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
}

Jetzt können wir die Schnittstelle und ihre Implementierungen verwenden:

// 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
*/

Was Sie jetzt nicht tun können, ist ungefähr so:

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)
}

Nachdem ich jedoch mit Schnittstellen und eingebetteten Strukturen herumgespielt habe, habe ich einen Weg gefunden, dies auf folgende Weise zu tun:

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
}

Aufgrund der eingebetteten Struktur hat Bob alles, was Person hat. Es implementiert auch die PersonProvider-Schnittstelle, sodass wir Bob an Funktionen übergeben können, die für die Verwendung dieser Schnittstelle ausgelegt sind.

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)
}

Hier ist ein Go-Spielplatz , der den obigen Code demonstriert.

Mit dieser Methode kann ich eine Schnittstelle erstellen, die eher Daten als Verhalten definiert und die von jeder Struktur implementiert werden kann, indem diese Daten einfach eingebettet werden. Sie können Funktionen definieren, die explizit mit diesen eingebetteten Daten interagieren und sich der Natur der äußeren Struktur nicht bewusst sind. Und alles wird beim Kompilieren überprüft! (Die einzige Möglichkeit, vermasseln könnte, dass ich sehen kann, würde die Schnittstelle werden die Einbettung PersonProviderin Bob, sondern als ein Beton Person. Es wäre kompilieren und zur Laufzeit fehl.)

Hier ist meine Frage: Ist das ein ordentlicher Trick oder sollte ich es anders machen?

Matt Mc
quelle
3
"Ich kann eine Schnittstelle erstellen, die eher Daten als Verhalten definiert". Ich würde argumentieren, dass Sie ein Verhalten haben, das Daten zurückgibt.
Jmaloney
Ich werde eine Antwort schreiben. Ich denke, es ist in Ordnung, wenn Sie es brauchen und die Konsequenzen kennen, aber es gibt Konsequenzen und ich würde es nicht die ganze Zeit tun.
Zwei
@jmaloney Ich denke du hast recht, wenn du es klar sehen wolltest. Insgesamt wird die Semantik mit den verschiedenen Stücken, die ich gezeigt habe, zu "Diese Funktion akzeptiert jede Struktur, deren Zusammensetzung ein ___ enthält". Zumindest habe ich das beabsichtigt.
Matt Mc
1
Dies ist kein "Antwort" -Material. Ich bin zu Ihrer Frage gekommen, indem ich "interface as struct property golang" gegoogelt habe. Ich habe einen ähnlichen Ansatz gefunden, indem ich eine Struktur festgelegt habe, die eine Schnittstelle als Eigenschaft einer anderen Struktur implementiert. Hier ist der Spielplatz, play.golang.org/p/KLzREXk9xo Vielen Dank, dass Sie mir einige Ideen gegeben haben.
Dale
1
Rückblickend und nach 5 Jahren mit Go ist mir klar, dass das oben Genannte kein idiomatisches Go ist. Es ist eine Belastung für Generika. Wenn Sie versucht sind, so etwas zu tun, empfehle ich Ihnen, die Architektur Ihres Systems zu überdenken. Akzeptieren Sie Schnittstellen und geben Sie Strukturen zurück, teilen Sie sie durch Kommunikation und freuen Sie sich.
Matt Mc

Antworten:

55

Es ist definitiv ein ordentlicher Trick. Durch das Anzeigen von Zeigern steht jedoch weiterhin ein direkter Zugriff auf Daten zur Verfügung, sodass Sie nur eine begrenzte zusätzliche Flexibilität für zukünftige Änderungen erhalten. Außerdem müssen Sie bei Go-Konventionen nicht immer eine Abstraktion vor Ihre Datenattribute stellen .

Wenn ich diese Dinge zusammen nehme, tendiere ich für einen bestimmten Anwendungsfall zum einen oder anderen Extrem: entweder a) erstelle einfach ein öffentliches Attribut (ggf. durch Einbetten) und gebe konkrete Typen weiter oder b) wenn es so aussieht, als würde es die Daten offenlegen Wenn Sie später Probleme verursachen, legen Sie einen Getter / Setter für eine robustere Abstraktion frei.

Sie werden dies pro Attribut abwägen. Wenn beispielsweise einige Daten implementierungsspezifisch sind oder Sie erwarten, dass die Darstellungen aus einem anderen Grund geändert werden, möchten Sie das Attribut wahrscheinlich nicht direkt verfügbar machen, während andere Datenattribute möglicherweise stabil genug sind, um sie öffentlich zu machen.


Durch das Ausblenden von Eigenschaften hinter Gettern und Setzern erhalten Sie zusätzliche Flexibilität, um später abwärtskompatible Änderungen vorzunehmen. Angenommen, Sie möchten eines Tages Personnicht nur ein einzelnes "Namens" -Feld, sondern auch das erste / mittlere / letzte / Präfix speichern. Wenn Sie über Methoden Name() stringund verfügen SetName(string), können Sie vorhandene Benutzer der PersonBenutzeroberfläche bei Laune halten, während Sie neue feinkörnigere Methoden hinzufügen. Oder Sie möchten ein datenbankgestütztes Objekt möglicherweise als "fehlerhaft" markieren können, wenn es nicht gespeicherte Änderungen aufweist. Sie können dies tun, wenn alle Datenaktualisierungen SetFoo()Methoden durchlaufen .

Also: Mit getters / setters können Sie Strukturfelder ändern, während Sie eine kompatible API beibehalten, und Logik um property get / sets hinzufügen, da niemand einfach auf p.Name = "bob"Ihren Code verzichten kann.

Diese Flexibilität ist relevanter, wenn der Typ kompliziert ist (und die Codebasis groß ist). Wenn Sie eine haben PersonCollection, wird diese möglicherweise intern durch eine sql.Rows, eine []*Person, eine []uintDatenbank-ID oder was auch immer gesichert . Mit der richtigen Schnittstelle können Sie Anrufer davor bewahren, sich darum zu kümmern, wie io.ReaderNetzwerkverbindungen und -dateien aussehen.

Eine bestimmte Sache: interfaces in Go haben die besondere Eigenschaft, dass Sie eine implementieren können, ohne das Paket zu importieren, das sie definiert. Dies kann Ihnen helfen , zyklische Importe zu vermeiden . Wenn Ihre Schnittstelle a zurückgibt *Person, müssen alle anstelle von Zeichenfolgen oder was auch immer PersonProvidersdas Paket importieren, in dem Persones definiert ist. Das kann in Ordnung oder sogar unvermeidlich sein; Es ist nur eine Konsequenz zu wissen.


Aber auch hier hat die Go-Community keine strenge Konvention gegen die Offenlegung von Datenelementen in der öffentlichen API Ihres Typs . Es bleibt Ihrem Urteil überlassen, ob es in einem bestimmten Fall sinnvoll ist, den öffentlichen Zugriff auf ein Attribut als Teil Ihrer API zu verwenden, anstatt eine Gefährdung zu verhindern, da dies möglicherweise eine spätere Implementierungsänderung erschweren oder verhindern könnte.

So lässt die stdlib beispielsweise eine http.Servermit Ihrer Konfiguration initialisieren und verspricht, dass eine Null bytes.Bufferverwendbar ist. Es ist in Ordnung, solche eigenen Sachen zu machen, und ich denke nicht, dass Sie Dinge präventiv abstrahieren sollten, wenn die konkretere, datenexponierende Version wahrscheinlich funktioniert. Es geht nur darum, sich der Kompromisse bewusst zu werden.

zwei
quelle
Eine weitere Sache: Der Einbettungsansatz ähnelt eher der Vererbung, oder? Sie erhalten alle Felder und Methoden, über die die eingebettete Struktur verfügt, und Sie können ihre Schnittstelle verwenden, damit sich jeder Aufbau qualifiziert, ohne die Schnittstellensätze erneut zu implementieren.
Matt Mc
Ja - sehr ähnlich wie die virtuelle Vererbung in anderen langs. Sie können die Einbettung verwenden, um eine Schnittstelle zu implementieren, unabhängig davon, ob sie als Getter und Setter oder als Zeiger auf die Daten definiert ist (oder als dritte Option für den schreibgeschützten Zugriff auf winzige Strukturen eine Kopie der Struktur).
Zwei
Ich muss sagen, dies gibt mir Rückblicke auf 1999 und das Lernen, Unmengen von Boilerplate Getter und Setter in Java zu schreiben.
Tom
Es ist schade, dass Go's eigene Standardbibliothek dies nicht immer tut. Ich bin gerade dabei, einige Anrufe bei os.Process für Unit-Tests zu verspotten. Ich kann das Prozessobjekt nicht einfach in eine Schnittstelle einbinden, da auf die Pid-Mitgliedsvariable direkt zugegriffen wird und Go-Schnittstellen keine Mitgliedsvariablen unterstützen.
Alex Jansen
1
@ Tom Das stimmt. Ich denke, Getter / Setter bieten mehr Flexibilität als das Freilegen eines Zeigers, aber ich denke auch nicht, dass jeder alles bekommen sollte (oder dass dies dem typischen Go-Stil entsprechen würde). Ich hatte vorher ein paar Worte, die darauf deuteten, aber den Anfang und das Ende überarbeitet, um es viel mehr zu betonen.
Zwei
2

Wenn ich das richtig verstehe, möchten Sie ein Strukturfeld in ein anderes füllen. Meine Meinung, keine Schnittstellen zum Erweitern zu verwenden. Sie können es leicht durch den nächsten Ansatz tun.

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

Hinweis Personin der BobErklärung. Dadurch wird das enthaltene Strukturfeld in der BobStruktur direkt mit etwas syntaktischem Zucker verfügbar .

Igor A. Melekhine
quelle