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 PersonProvider
w Bob
, zamiast betonu Person
. Byłoby skompilować i nie przy starcie).
A teraz moje pytanie: czy to zgrabna sztuczka, czy powinienem zrobić to inaczej?
Odpowiedzi:
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ć,
Person
aby przechowywać nie tylko jedno pole „nazwa”, ale także imię / drugie / ostatnie / przedrostek; jeśli masz metodyName() string
iSetName(string)
, możeszPerson
zadowolić 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ą przezSetFoo()
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 ansql.Rows
, a[]*Person
, a[]uint
z identyfikatorów bazy danych lub cokolwiek innego. Korzystając z odpowiedniego interfejsu, możesz uchronić dzwoniących przed dbaniem o to, coio.Reader
sprawia, że połączenia sieciowe i pliki wyglądają podobnie.Jedna konkretna rzecz:
interface
s 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, wszyscyPersonProviders
muszą zaimportować pakiet, w którymPerson
jest 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.Server
z twoją konfiguracją i obiecuje, że zerobytes.Buffer
jest 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.źródło
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.
https://play.golang.org/p/aBJ5fq3uXtt
Uwaga
Person
wBob
deklaracji. W ten sposób włączone pole struct będzie dostępne wBob
strukturze bezpośrednio z pewnym cukrem syntaktycznym.źródło