Buka Bidang Antarmuka

105

Saya akrab dengan fakta bahwa, di Go, antarmuka menentukan fungsionalitas, bukan data. Anda meletakkan sekumpulan metode ke dalam antarmuka, tetapi Anda tidak dapat menentukan bidang apa pun yang akan diperlukan pada apa pun yang mengimplementasikan antarmuka itu.

Sebagai contoh:

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

Sekarang kita dapat menggunakan antarmuka dan implementasinya:

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

Sekarang, yang tidak dapat Anda lakukan adalah seperti ini:

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

Namun, setelah bermain-main dengan antarmuka dan struct tertanam, saya telah menemukan cara untuk melakukan ini, setelah mode:

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
}

Karena struct tertanam, Bob memiliki semua yang dimiliki Orang. Ini juga mengimplementasikan antarmuka PersonProvider, sehingga kita dapat meneruskan Bob ke dalam fungsi yang dirancang untuk menggunakan antarmuka itu.

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

Berikut adalah Go Playground yang mendemonstrasikan kode di atas.

Dengan menggunakan metode ini, saya dapat membuat antarmuka yang mendefinisikan data daripada perilaku, dan yang dapat diimplementasikan oleh struct apa pun hanya dengan menyematkan data itu. Anda dapat mendefinisikan fungsi yang secara eksplisit berinteraksi dengan data yang disematkan dan tidak mengetahui sifat dari struct luar. Dan semuanya diperiksa pada waktu kompilasi! (Satu-satunya cara Anda bisa mengacaukan, bahwa saya bisa melihat, akan embedding antarmuka PersonProviderdi Bob, daripada beton Person. Ini akan mengkompilasi dan gagal pada saat runtime.)

Sekarang, inilah pertanyaan saya: apakah ini trik yang rapi, atau haruskah saya melakukannya secara berbeda?

Matt Mc
sumber
4
"Saya dapat membuat antarmuka yang lebih mendefinisikan data daripada perilaku". Saya berpendapat bahwa Anda memiliki perilaku yang mengembalikan data.
jmaloney
Saya akan menulis jawaban; Saya pikir tidak masalah jika Anda membutuhkannya dan mengetahui konsekuensinya, tetapi ada konsekuensinya dan saya tidak akan melakukannya sepanjang waktu.
dua
@jmaloney Saya pikir Anda benar, jika Anda ingin melihatnya dengan jelas. Tapi secara keseluruhan, dengan potongan berbeda yang saya tunjukkan, semantiknya menjadi "fungsi ini menerima struct apa pun yang memiliki ___ dalam komposisinya". Setidaknya, itulah yang saya inginkan.
Matt Mc
1
Ini bukan materi "jawaban". Saya mendapat pertanyaan Anda dengan googling "antarmuka sebagai struct property golang". Saya menemukan pendekatan serupa dengan menetapkan struct yang mengimplementasikan antarmuka sebagai properti dari struct lain. Ini taman bermainnya, play.golang.org/p/KLzREXk9xo Terima kasih telah memberi saya beberapa ide.
Dale
1
Dalam retrospeksi, dan setelah 5 tahun menggunakan Go, jelas bagi saya bahwa hal di atas bukanlah Go idiomatik. Ini merupakan ketegangan terhadap obat generik. Jika Anda merasa tergoda untuk melakukan hal semacam ini, saya menyarankan Anda untuk memikirkan kembali arsitektur sistem Anda. Terima antarmuka dan kembalikan struct, bagikan dengan berkomunikasi, dan bersukacita.
Matt Mc

Jawaban:

55

Ini pasti trik yang bagus. Namun, mengungkap petunjuk tetap membuat akses langsung ke data tersedia, jadi itu hanya memberi Anda fleksibilitas tambahan terbatas untuk perubahan di masa mendatang. Selain itu, konvensi Go tidak mengharuskan Anda untuk selalu meletakkan abstraksi di depan atribut data Anda .

Mengambil hal-hal tersebut bersama-sama, saya akan cenderung ke satu ekstrim atau yang lain untuk kasus penggunaan tertentu: baik a) hanya membuat atribut publik (menggunakan embedding jika berlaku) dan meneruskan tipe konkret atau b) jika tampaknya mengekspos data akan menyebabkan masalah nanti, tampilkan pengambil / penyetel untuk abstraksi yang lebih kuat.

Anda akan menimbang ini pada basis per atribut. Misalnya, jika beberapa data spesifik untuk implementasi atau Anda berharap untuk mengubah representasi karena alasan lain, Anda mungkin tidak ingin mengekspos atribut secara langsung, sedangkan atribut data lainnya mungkin cukup stabil sehingga menjadikannya publik adalah keuntungan bersih.


Menyembunyikan properti di balik getter dan setter memberi Anda beberapa fleksibilitas ekstra untuk membuat perubahan yang kompatibel dengan mundur nanti. Katakanlah Anda suatu hari ingin mengubah Personuntuk menyimpan tidak hanya satu bidang "nama" tetapi juga depan / tengah / terakhir / awalan; jika Anda memiliki metode Name() stringdan SetName(string), Anda dapat membuat pengguna Personantarmuka yang ada senang sambil menambahkan metode baru yang lebih terperinci. Atau Anda mungkin ingin menandai objek yang didukung database sebagai "kotor" jika ada perubahan yang belum disimpan; Anda dapat melakukannya ketika pembaruan data semua melalui SetFoo()metode.

Jadi: dengan getter / setter, Anda dapat mengubah bidang struct sambil mempertahankan API yang kompatibel, dan menambahkan logika di sekitar get / set properti karena tidak ada yang bisa melakukannya p.Name = "bob"tanpa melalui kode Anda.

Fleksibilitas tersebut lebih relevan jika jenisnya rumit (dan basis kodenya besar). Jika Anda memiliki PersonCollection, mungkin secara internal didukung oleh sql.Rows, a []*Person, []uintID database, atau apa pun. Menggunakan antarmuka yang tepat, Anda dapat menyelamatkan penelepon dari kepedulian, cara io.Readermembuat koneksi jaringan dan file terlihat sama.

Satu hal yang spesifik: interfaces di Go memiliki properti khas yang bisa Anda implementasikan tanpa mengimpor paket yang mendefinisikannya; yang dapat membantu Anda menghindari impor siklik . Jika antarmuka Anda mengembalikan a *Person, bukan hanya string atau apa pun, semua PersonProvidersharus mengimpor paket tempat Personyang ditentukan. Itu mungkin bagus atau bahkan tidak bisa dihindari; itu hanya konsekuensi yang perlu diketahui.


Tapi sekali lagi, komunitas Go tidak memiliki konvensi yang kuat terhadap pemaparan anggota data di API publik tipe Anda . Ini tersisa untuk penilaian Anda apakah itu masuk akal untuk menggunakan akses masyarakat terhadap atribut sebagai bagian dari API Anda dalam kasus tertentu, bukan mengecilkan setiap paparan karena mungkin bisa menyulitkan atau mencegah perubahan pelaksanaan nanti.

Jadi, misalnya, stdlib melakukan hal-hal seperti membiarkan Anda menginisialisasi an http.Serverdengan konfigurasi Anda dan menjanjikan bahwa nol bytes.Bufferdapat digunakan. Tidak apa-apa untuk melakukan hal-hal Anda sendiri seperti itu, dan, memang, saya tidak berpikir Anda harus mengabstraksikan hal-hal terlebih dahulu jika versi yang lebih konkret dan mengekspos data tampaknya akan berhasil. Ini hanya tentang menyadari pengorbanan.

dua dua
sumber
Satu hal tambahan: pendekatan embedding lebih mirip inheritance, bukan? Anda mendapatkan kolom dan metode apa pun yang dimiliki struct tertanam, dan Anda dapat menggunakan antarmukanya sehingga superstruct apa pun akan memenuhi syarat, tanpa mengimplementasikan ulang kumpulan antarmuka.
Matt Mc
Ya - sangat mirip dengan warisan virtual di bahasa lain. Anda dapat menggunakan embedding untuk mengimplementasikan antarmuka baik itu didefinisikan dalam istilah getter dan setter atau pointer ke data (atau, opsi ketiga untuk akses hanya baca ke struct kecil, salinan struct).
dua
Saya harus mengatakan, ini memberi saya kilas balik ke tahun 1999 dan belajar menulis rim getter dan setter boilerplate di Java.
Tom
Sayang sekali pustaka standar Go sendiri tidak selalu melakukan ini. Saya sedang mencoba mengejek beberapa panggilan ke os.Process untuk pengujian unit. Saya tidak bisa hanya membungkus objek proses dalam sebuah antarmuka karena variabel anggota Pid diakses secara langsung dan antarmuka Go tidak mendukung variabel anggota.
Alex Jansen
1
@Twit Itu benar. Saya pikir getter / setter menambahkan lebih banyak fleksibilitas daripada mengekspos pointer, tetapi saya juga tidak berpikir setiap orang harus mendapatkan / setter-ify semuanya (atau yang akan cocok dengan gaya Go yang khas). Saya sebelumnya memiliki beberapa kata yang mengisyaratkan hal itu, tetapi merevisi awal dan akhir untuk lebih menekankannya.
dua
2

Jika saya benar-benar memahami Anda ingin mengisi satu bidang struct ke yang lain. Pendapat saya untuk tidak menggunakan antarmuka untuk memperluas. Anda dapat dengan mudah melakukannya dengan pendekatan selanjutnya.

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

Catatan Persondalam Bobdeklarasi. Ini akan membuat bidang struct yang disertakan tersedia dalam Bobstruktur secara langsung dengan beberapa gula sintaksis.

Igor A. Melekhine
sumber