2014-09-24 11 views
39

Ho familiarità con il fatto che, in Go, le interfacce definiscono la funzionalità piuttosto che i dati. Hai messo un insieme di metodi in un'interfaccia, ma non sei in grado di specificare alcun campo che sarebbe richiesto su tutto ciò che implementa quell'interfaccia.Vai campi di interfaccia

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

Ora, quello che non puoi fare è qualcosa di simile:

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 le interfacce e incorporato le strutture, ho scoperto un modo per fare questo, in un certo modo:

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 struct incorporato, Bob ha tutto persona ha. Implementa anche l'interfaccia PersonProvider, in modo tale che possiamo trasferire Bob in funzioni progettate per l'utilizzo di tale 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) 
} 

Here is a Go Playground che dimostra il codice precedente.

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

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

+1

"Posso creare un'interfaccia che definisce i dati anziché il comportamento". Direi che hai un comportamento che restituisce i dati. – jmaloney

+0

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

+0

@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, è quello che intendevo. –

risposta

21

È sicuramente un trucco accurato e funziona fintanto che sei in grado di accedere a questi campi come parte della tua API. L'alternativa che prenderei in considerazione è mantenere la struttura embeddable/interface setup, ma definire l'interfaccia in termini di getter e setter.

Le proprietà di occultamento dietro getter e setter offrono maggiore flessibilità per apportare modifiche all'indietro in un secondo momento. Dite che un giorno vorrete cambiare Person per memorizzare non solo un singolo campo "nome" ma primo/medio/ultimo/prefisso; Se si dispone dei metodi Name() string e SetName(string), è possibile mantenere soddisfatti gli utenti esistenti dell'interfaccia Person aggiungendo nuovi metodi a grana fine. Oppure potresti voler contrassegnare un oggetto con backup del database come "sporco" quando ha modifiche non salvate; puoi farlo quando tutti gli aggiornamenti dei dati passano attraverso i metodi SetFoo().

Quindi: con getter/setter, è possibile modificare i campi struct mantenendo un'API compatibile e aggiungere la logica attorno a get/set di proprietà poiché nessuno può semplicemente eseguire p.Name = "bob" senza passare attraverso il codice.

Questa flessibilità è più pertinente quando il tuo tipo fa qualcosa di più complicato. Se si dispone di un PersonCollection, potrebbe essere internamente supportato da un sql.Rows, un []*Person, un []uint di ID di database o qualsiasi altra cosa.Utilizzando l'interfaccia corretta, è possibile salvare i chiamanti dalla cura che è, il modo in cui io.Reader rende le connessioni di rete e file simili.

Una cosa specifica: interface s in Go ha la proprietà peculiare che è possibile implementare senza importare il pacchetto che lo definisce; che può aiutarti avoid cyclic imports. Se la tua interfaccia restituisce un *Person, anziché solo stringhe o altro, tutto il PersonProviders deve importare il pacchetto in cui è definito Person. Potrebbe andare bene o addirittura inevitabile; è solo una conseguenza da sapere.

Detto questo, non esiste una convenzione Go per cui è necessario nascondere tutti i dati. (Questa è una gradita differenza, ad esempio, in C++.) Lo stdlib fa cose come ti permette di inizializzare un http.Server con la tua configurazione e promette che uno zero bytes.Buffer è utilizzabile. Va bene fare le tue cose in quel modo, e, in effetti, non penso che tu debba fare un'astrazione prematura se la versione più concreta, che espone i dati funziona. Si tratta solo di essere a conoscenza dei compromessi.

+0

Ben affermato, grazie. –

+0

Leggendo, la mia risposta sembrava davvero prescrittiva perché stavo dando solo i lati negativi. A differenza del C++, Go non ha dogmi contro l'esposizione dei dati se funziona per te; ho provato a modificare per enfatizzarlo di più. – twotwotwo

+0

Un'altra cosa: l'approccio di incorporamento è un po 'più simile all'ereditarietà, giusto? Si ottengono tutti i campi e i metodi che ha la struttura incorporata e si può usare la sua interfaccia in modo tale che qualsivoglia superstruct possa qualificarsi, senza ri-implementare insiemi di interfacce. –