Vai ai campi dell'interfaccia


105

Ho familiarità con il fatto che, in Go, le interfacce definiscono la funzionalità, piuttosto che i dati. Metti una serie di metodi in un'interfaccia, ma non sei in grado di specificare alcun campo che sarebbe richiesto su qualsiasi cosa che implementi quell'interfaccia.

Per esempio:

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

Ora possiamo usare l'interfaccia e le sue implementazioni:

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

Quello che non puoi fare è qualcosa del genere:

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

Tuttavia, dopo aver giocato con interfacce e strutture incorporate, ho scoperto un modo per farlo, in un certo senso:

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
}

A causa della struttura incorporata, Bob ha tutto ciò che ha Person. Implementa anche l'interfaccia PersonProvider, quindi possiamo passare Bob in funzioni progettate per usare quell'interfaccia.

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

Ecco un Go Playground che dimostra il codice sopra.

Usando questo metodo, posso creare un'interfaccia che definisce i dati piuttosto che il comportamento e che può essere implementata da qualsiasi struttura semplicemente incorporando quei dati. È possibile definire funzioni che interagiscono esplicitamente con i dati incorporati e non sono consapevoli della natura della struttura esterna. E tutto viene controllato in fase di compilazione! (L'unico modo per rovinare, che posso vedere, sarebbe incorporare l'interfaccia PersonProviderin Bob, piuttosto che una concreta Person. Sarebbe la compilazione e non riescono a runtime.)

Ora, ecco la mia domanda: è un bel trucco o dovrei farlo in modo diverso?


3
"Posso creare un'interfaccia che definisca i dati piuttosto che il comportamento". Direi che hai un comportamento che restituisce dati.
jmaloney

Scriverò una risposta; Penso che vada bene se ne hai bisogno e conosci le conseguenze, ma ci sono conseguenze e io non lo farei sempre.
twotwotwo

@jmaloney Penso che tu abbia ragione, se volessi guardarlo chiaramente. Ma nel complesso, con i diversi pezzi che ho mostrato, la semantica diventa "questa funzione accetta qualsiasi struttura che abbia un ___ nella sua composizione". Almeno, questo è quello che intendevo.
Matt Mc,

1
Questo non è materiale "risposta". Sono arrivato alla tua domanda cercando su Google "interface as struct property golang". Ho trovato un approccio simile impostando una struttura che implementa un'interfaccia come proprietà di un'altra struttura. Ecco il parco giochi, play.golang.org/p/KLzREXk9xo Grazie per avermi dato alcune idee.
Dale

1
In retrospettiva, e dopo 5 anni di utilizzo di Go, è chiaro che quanto sopra non è un Go idiomatico. È una tensione verso i generici. Se ti senti tentato di fare questo genere di cose, ti consiglio di ripensare l'architettura del tuo sistema. Accetta interfacce e restituisci strutture, condividi comunicando e gioisci.
Matt Mc

Risposte:


55

È sicuramente un bel trucco. Tuttavia, l'esposizione dei puntatori rende ancora disponibile l'accesso diretto ai dati, quindi ti acquista solo una flessibilità aggiuntiva limitata per modifiche future. Inoltre, le convenzioni Go non richiedono di inserire sempre un'astrazione davanti agli attributi dei dati .

Prendendo insieme queste cose, tenderei verso un estremo o l'altro per un determinato caso d'uso: a) crea semplicemente un attributo pubblico (usando l'incorporamento se applicabile) e passa tipi concreti o b) se sembra che esporre i dati lo farebbe causare problemi in seguito, esporre un getter / setter per un'astrazione più robusta.

Lo valuterai in base agli attributi. Ad esempio, se alcuni dati sono specifici dell'implementazione o prevedi di modificare le rappresentazioni per qualche altro motivo, probabilmente non vuoi esporre l'attributo direttamente, mentre altri attributi dei dati potrebbero essere abbastanza stabili da renderli pubblici è una vittoria netta.


Nascondere le proprietà dietro getter e setter ti offre una maggiore flessibilità per apportare modifiche compatibili con le versioni precedenti in un secondo momento. Supponiamo che un giorno tu voglia cambiare Personper memorizzare non solo un singolo campo "nome" ma il primo / mezzo / ultimo / prefisso; se disponi di metodi Name() stringe SetName(string), puoi mantenere Personfelici gli utenti esistenti dell'interfaccia aggiungendo nuovi metodi più dettagliati. Oppure potresti voler contrassegnare un oggetto supportato da database come "sporco" quando presenta modifiche non salvate; puoi farlo quando tutti gli aggiornamenti dei dati passano attraverso SetFoo()metodi.

Quindi: con getter / setters, puoi modificare i campi della struttura mantenendo un'API compatibile e aggiungere logica attorno a get / set di proprietà poiché nessuno può fare a p.Name = "bob"meno di passare attraverso il tuo codice.

Questa flessibilità è più rilevante quando il tipo è complicato (e la base di codice è grande). Se disponi di un PersonCollection, potrebbe essere supportato internamente da un sql.Rows, a []*Person, a []uintdi ID database o altro. Utilizzando la giusta interfaccia, puoi evitare che i chiamanti si preoccupino di ciò che è, il modo in cui io.Readerle connessioni di rete ei file si assomigliano.

Una cosa specifica: interfaces in Go hanno la peculiare proprietà di poterlo implementare senza importare il pacchetto che lo definisce; che può aiutarti a evitare le importazioni cicliche . Se la tua interfaccia restituisce un *Person, invece di stringhe o altro, tutti PersonProvidersdevono importare il pacchetto in cui Personè definito. Potrebbe andare bene o addirittura inevitabile; è solo una conseguenza da conoscere.


Ma ancora una volta, la comunità Go non ha una forte convenzione contro l'esposizione dei membri dei dati nell'API pubblica del tuo tipo . È lasciato al tuo giudizio se è ragionevole utilizzare l'accesso pubblico a un attributo come parte della tua API in un determinato caso, piuttosto che scoraggiare qualsiasi esposizione perché potrebbe complicare o impedire una modifica all'implementazione in un secondo momento.

Quindi, ad esempio, lo stdlib fa cose come lasciarti inizializzare un http.Servercon la tua configurazione e promette che uno zero bytes.Bufferè utilizzabile. Va bene fare le tue cose in questo modo e, in effetti, non penso che dovresti astrarre le cose preventivamente se la versione più concreta che espone i dati sembra funzionare. Si tratta solo di essere consapevoli dei compromessi.


Un'altra cosa: l'approccio di incorporamento è un po 'più simile all'ereditarietà, giusto? Ottieni tutti i campi e i metodi della struttura incorporata e puoi usare la sua interfaccia in modo che qualsiasi sovrastruttura si qualifichi, senza reimplementare set di interfacce.
Matt Mc,

Sì, molto simile all'ereditarietà virtuale in altre lingue. Puoi usare l'incorporamento per implementare un'interfaccia sia che sia definita in termini di getter e setter o un puntatore ai dati (o, una terza opzione per l'accesso in sola lettura a piccole strutture, una copia della struttura).
twotwotwo

Devo dire che questo mi sta dando dei flashback al 1999 e sto imparando a scrivere risme di getter e setter standard in Java.
Tom

Peccato che la libreria standard di Go non lo faccia sempre. Sono nel bel mezzo del tentativo di deridere alcune chiamate a os.Process per unit test. Non posso semplicemente avvolgere l'oggetto del processo in un'interfaccia poiché si accede direttamente alla variabile membro Pid e le interfacce Go non supportano le variabili membro.
Alex Jansen

1
@ Tom è vero. Penso getter / setter aggiungono maggiore flessibilità rispetto esporre un puntatore, ma anche non credo che tutti dovrebbero getter / setter tutto-ify (o quello che sarebbe abbinare tipico stile Go). In precedenza avevo poche parole che indicavano questo, ma ho rivisto l'inizio e la fine per enfatizzarlo molto di più.
due il

2

Se ho capito correttamente che vuoi popolare un campo struct in un altro. La mia opinione è di non utilizzare le interfacce per estendere. Puoi farlo facilmente con il prossimo approccio.

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

Nota Personnella Bobdichiarazione. In questo modo il campo struct incluso sarà disponibile in Bobstruttura direttamente con un po 'di zucchero sintattico.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.