Pola interfejsu Go

106

Jestem zaznajomiony z faktem, że w Go interfejsy definiują funkcjonalność, a nie dane. Umieszczasz zestaw metod w interfejsie, ale nie możesz określić żadnych pól, które byłyby wymagane na czymkolwiek, co implementuje ten interfejs.

Na przykład:

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

Teraz możemy skorzystać z interfejsu i jego implementacji:

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

Teraz nie możesz zrobić czegoś takiego:

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

Jednak po zabawie z interfejsami i strukturami osadzonymi odkryłem sposób na zrobienie tego, po pewnym czasie:

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
}

Ze względu na osadzoną strukturę Bob ma wszystko, co ma Person. Implementuje również interfejs PersonProvider, więc możemy przekazać Boba do funkcji zaprojektowanych do używania tego interfejsu.

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

Oto Go Playground, który demonstruje powyższy kod.

Korzystając z tej metody, mogę stworzyć interfejs, który definiuje dane, a nie zachowanie, i który może być implementowany przez dowolną strukturę po prostu przez osadzenie tych danych. Możesz zdefiniować funkcje, które jawnie współdziałają z tymi osadzonymi danymi i nie są świadome charakteru zewnętrznej struktury. I wszystko jest sprawdzane w czasie kompilacji! (Tylko w ten sposób można bałagan, że widzę, byłoby umieszczenie interfejsu PersonProviderw Bob, zamiast betonu Person. Byłoby skompilować i nie przy starcie).

A teraz moje pytanie: czy to zgrabna sztuczka, czy powinienem zrobić to inaczej?

Matt Mc
źródło
4
„Potrafię stworzyć interfejs, który definiuje dane, a nie zachowanie”. Twierdzę, że masz zachowanie, które zwraca dane.
jmaloney
Napiszę odpowiedź; Myślę, że to dobrze, jeśli tego potrzebujesz i znasz konsekwencje, ale są konsekwencje i nie robiłbym tego cały czas.
twotwotwo
@jmaloney Myślę, że masz rację, jeśli chcesz spojrzeć na to otwarcie. Ale ogólnie rzecz biorąc, z różnymi utworami, które pokazałem, semantyka to „ta funkcja akceptuje każdą strukturę, która ma w swoim składzie ___”. Przynajmniej tak właśnie zamierzałem.
Matt Mc
1
To nie jest materiał na „odpowiedź”. Dotarłem do twojego pytania, wpisując w google „interface as struct property golang”. Znalazłem podobne podejście, ustawiając strukturę, która implementuje interfejs jako właściwość innej struktury. Oto plac zabaw, play.golang.org/p/KLzREXk9xo Dziękuję za kilka pomysłów.
Dale
1
Z perspektywy czasu i po 5 latach używania Go, jest dla mnie jasne, że powyższe nie jest idiomatycznym Go. To nacisk na leki generyczne. Jeśli masz ochotę zrobić coś takiego, radzę przemyśleć architekturę swojego systemu. Akceptuj interfejsy i zwracaj struktury, udostępniaj przez komunikację i raduj się.
Matt Mc

Odpowiedzi:

55

To zdecydowanie fajna sztuczka. Jednak ujawnienie wskaźników nadal zapewnia bezpośredni dostęp do danych, więc kupuje tylko ograniczoną dodatkową elastyczność na przyszłe zmiany. Ponadto konwencje Go nie wymagają, aby zawsze umieszczać abstrakcję przed atrybutami danych .

Biorąc te rzeczy razem, dążyłbym do jednej lub drugiej skrajności dla danego przypadku użycia: albo a) po prostu utwórz atrybut publiczny (używając osadzania, jeśli ma to zastosowanie) i przekazuj konkretne typy dookoła lub b) jeśli wydaje się, że ujawnienie danych powodować problemy później, ujawnij metodę pobierającą / ustawiającą, aby uzyskać bardziej solidną abstrakcję.

Będziesz to rozważać na podstawie poszczególnych atrybutów. Na przykład, jeśli niektóre dane są specyficzne dla implementacji lub spodziewasz się zmiany reprezentacji z innego powodu, prawdopodobnie nie chcesz bezpośrednio ujawniać atrybutu, podczas gdy inne atrybuty danych mogą być wystarczająco stabilne, że upublicznienie ich jest wygraną.


Ukrywanie właściwości za metodami pobierającymi i ustawiającymi zapewnia dodatkową elastyczność przy późniejszym wprowadzaniu zmian zgodnych wstecz. Powiedzmy, że pewnego dnia chcesz zmienić, Personaby przechowywać nie tylko jedno pole „nazwa”, ale także imię / drugie / ostatnie / przedrostek; jeśli masz metody Name() stringi SetName(string), możesz Personzadowolić obecnych użytkowników interfejsu, dodając nowe, bardziej szczegółowe metody. Możesz też chcieć móc oznaczyć obiekt wspierany przez bazę danych jako „brudny”, gdy ma niezapisane zmiany; możesz to zrobić, gdy wszystkie aktualizacje danych przechodzą przez SetFoo()metody.

A więc: dzięki getterom / setterom możesz zmieniać pola strukturalne, zachowując kompatybilny interfejs API, i dodawać logikę do właściwości get / sets, ponieważ nikt nie może się obejść p.Name = "bob"bez przeglądania kodu.

Ta elastyczność jest bardziej istotna, gdy typ jest skomplikowany (a baza kodu jest duża). Jeśli masz PersonCollection, może być wewnętrznie wspierany przez an sql.Rows, a []*Person, a []uintz identyfikatorów bazy danych lub cokolwiek innego. Korzystając z odpowiedniego interfejsu, możesz uchronić dzwoniących przed dbaniem o to, co io.Readersprawia, że ​​połączenia sieciowe i pliki wyglądają podobnie.

Jedna konkretna rzecz: interfaces w Go ma specyficzną właściwość, którą można zaimplementować bez importowania pakietu, który ją definiuje; które pomogą Ci uniknąć cyklicznego importu . Jeśli twój interfejs zwraca a *Person, zamiast tylko łańcuchów lub cokolwiek innego, wszyscy PersonProvidersmuszą zaimportować pakiet, w którym Personjest zdefiniowany. To może być dobre lub nawet nieuniknione; to tylko konsekwencja, o której trzeba wiedzieć.


Ale znowu społeczność Go nie ma silnej konwencji przeciwko ujawnianiu elementów członkowskich danych w publicznym interfejsie API Twojego typu . To od Ciebie zależy, czy rozsądne jest korzystanie z publicznego dostępu do atrybutu jako części interfejsu API w danym przypadku, zamiast zniechęcać do jakiegokolwiek ujawnienia, ponieważ może to skomplikować lub uniemożliwić późniejszą zmianę implementacji.

Na przykład stdlib robi takie rzeczy, jak pozwala zainicjować an http.Serverz twoją konfiguracją i obiecuje, że zero bytes.Bufferjest użyteczne. Robienie własnych rzeczy w ten sposób jest w porządku i, rzeczywiście, nie sądzę, abyś mógł odciągać rzeczy prewencyjnie, jeśli bardziej konkretna, eksponująca dane wersja wydaje się działać. Chodzi tylko o to, aby być świadomym kompromisów.

twotwotwo
źródło
Jeszcze jedna rzecz: podejście osadzające przypomina bardziej dziedziczenie, prawda? Otrzymujesz wszystkie pola i metody, które ma osadzona struktura, i możesz użyć jej interfejsu, aby każda superstruktura kwalifikowała się, bez ponownego implementowania zestawów interfejsów.
Matt Mc
Tak - podobnie jak dziedziczenie wirtualne w innych językach. Możesz użyć osadzania do zaimplementowania interfejsu, niezależnie od tego, czy jest on zdefiniowany w kategoriach pobierających i ustawiających, czy też wskaźnika do danych (lub trzeciej opcji dostępu tylko do odczytu do małych struktur, kopii struktury).
twotwotwo
Muszę powiedzieć, że to daje mi retrospekcje do 1999 roku i naukę pisania ryz gotowych metod pobierających i ustawiających w Javie.
Tom
Szkoda, że ​​standardowa biblioteka Go nie zawsze to robi. Jestem w trakcie próby wyśmiewania niektórych wywołań procesu os.Proces testów jednostkowych. Nie mogę po prostu zawinąć obiektu procesu w interfejs, ponieważ zmienna składowa Pid jest dostępna bezpośrednio, a interfejsy Go nie obsługują zmiennych składowych.
Alex Jansen
1
@Tom To prawda. Myślę, że metody pobierające / ustawiające dodają więcej elastyczności niż eksponowanie wskaźnika, ale nie uważam też , że każdy powinien pobierać / ustawiać wszystko (lub to, co pasowałoby do typowego stylu Go). Wcześniej kilka słów wskazywało na to, ale zrewidowałem początek i koniec, aby podkreślić to znacznie bardziej.
twotwotwo
2

Jeśli dobrze rozumiem, chcesz wypełnić jedno pole strukturalne innym. Moim zdaniem nie używać interfejsów do rozszerzenia. Możesz to łatwo zrobić następnym podejściem.

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

Uwaga Personw Bobdeklaracji. W ten sposób włączone pole struct będzie dostępne w Bobstrukturze bezpośrednio z pewnym cukrem syntaktycznym.

Igor A. Melechine
źródło