Antarmuka sebagai tipe data abstrak di Go

Belum lama ini, seorang kolega me-retweet pos yang sangat bagus How to Use Go Interfaces . Ini membahas beberapa bug saat menggunakan antarmuka di Go, dan juga memberikan beberapa rekomendasi tentang bagaimana mereka harus digunakan.

Dalam artikel yang disebutkan di atas, penulis mengutip antarmuka dari paket sortir dari perpustakaan standar sebagai contoh tipe data abstrak. Namun, bagi saya tampaknya contoh seperti itu tidak mengungkapkan ide dengan sangat baik ketika datang ke aplikasi nyata. Terutama tentang aplikasi yang menerapkan logika bidang bisnis atau memecahkan masalah dunia nyata.

Juga, ketika menggunakan antarmuka di Go, sering ada perdebatan tentang rekayasa ulang. Dan juga terjadi bahwa, setelah membaca rekomendasi semacam ini, orang-orang tidak hanya berhenti menyalahgunakan antarmuka, mereka mencoba untuk hampir sepenuhnya meninggalkannya, dengan demikian menghilangkan diri mereka menggunakan salah satu konsep pemrograman terkuat pada prinsipnya (dan salah satu kekuatan Go in khusus). Pada topik kesalahan khas di Go, omong-omong, ada laporan bagus dari Stive Francia dari Docker. Di sana, khususnya, antarmuka disebutkan beberapa kali.

Secara umum, saya setuju dengan penulis artikel. Namun demikian, tampaknya bagi saya bahwa topik menggunakan antarmuka sebagai tipe data abstrak di dalamnya diungkapkan agak dangkal, jadi saya ingin mengembangkannya sedikit dan merenungkan topik ini dengan Anda.

Lihat yang asli


Di awal artikel, penulis memberikan contoh kode kecil, dengan bantuan yang ia tunjukkan kesalahan ketika menggunakan antarmuka yang sering dibuat pengembang. Ini kodenya.

package animal type Animal interface { Speaks() string } // implementation of Animal type Dog struct{} func (a Dog) Speaks() string { return "woof" } 

 package circus import "animal" func Perform(a animal.Animal) string { return a.Speaks() } 

Penulis menyebut pendekatan ini "penggunaan antarmuka gaya Java" . Saat kami mendeklarasikan antarmuka, maka kami menerapkan satu-satunya jenis dan metode yang akan memenuhi antarmuka ini. Saya setuju dengan penulis, pendekatannya biasa-biasa saja. Kode yang lebih idiomatis dalam artikel asli adalah sebagai berikut:

 package animal // implementation of Animal type Dog struct{} func (a Dog) Speaks() string { return "woof" } 

 package circus type Speaker interface { Speaks() string } func Perform(a Speaker) string { return a.Speaks() } 

Di sini, secara umum, semuanya jelas dan dapat dimengerti. Gagasan dasar: "Pertama-tama deklarasikan jenisnya, dan baru kemudian deklarasikan antarmuka pada titik penggunaan . " Ini benar Tetapi sekarang mari kita kembangkan sedikit gagasan terkait dengan bagaimana Anda dapat menggunakan antarmuka sebagai tipe data abstrak. Penulis, secara kebetulan, menunjukkan bahwa dalam situasi seperti itu tidak ada yang salah dengan menyatakan antarmuka "dimuka" . Kami akan bekerja dengan kode yang sama.

Mari kita bermain dengan abstraksi


Jadi kami punya sirkus dan ada binatang. Di dalam sirkus, ada metode yang agak abstrak yang disebut `Perform` , yang mengambil antarmuka` Speaker` dan membuat hewan peliharaan membuat suara. Misalnya, ia akan membuat kulit anjing dari contoh di atas. Buat penjinak binatang. Karena dia tidak bodoh di sini, kita juga bisa membuatnya bersuara. Antarmuka kami cukup abstrak. :)

 package circus type Tamer struct{} func (t *Tamer) Speaks() string { return "WAT?" } 

Sejauh ini, sangat bagus. Kita melangkah lebih jauh. Ayo ajari penjinak kita untuk memberi perintah pada hewan peliharaan? Sejauh ini, kita akan memiliki satu perintah suara . :)

 package circus const ( ActVoice = iota ) func (t *Tamer) Command(action int, a Speaker) string { switch action { case ActVoice: return a.Speaks() } return "" } 

 package main import ( "animal" "circus" ) func main() { d := &animal.Dog{} t := &circus.Tamer{} t2 := &circus.Tamer{} t.Command(circus.ActVoice, d) // woof t.Command(circus.ActVoice, t2) // WAT? } 

Mmmm, menarik bukan? Tampaknya kolega kita tidak senang dia menjadi hewan peliharaan dalam konteks ini? : D Apa yang harus dilakukan? Speaker sepertinya abstraksi tidak cocok di sini. Kami akan membuat yang lebih cocok (atau lebih tepatnya, kami akan mengembalikan versi pertama dari "contoh yang salah" ), setelah itu kami akan mengubah notasi metode.

 package circus type Animal interface { Speaker } func (t *Tamer) Command(action int, a Animal) string { /* ... */ } 

Ini tidak mengubah apa pun, Anda berkata, kode akan tetap dieksekusi, karena kedua antarmuka menerapkan satu metode, dan Anda akan benar secara umum.

Namun, contoh ini menangkap ide penting. Ketika kita berbicara tentang tipe data abstrak, konteks sangat penting. Pengenalan antarmuka baru, setidaknya, membuat kode urutan lebih jelas dan mudah dibaca.

Ngomong-ngomong, salah satu cara untuk memaksa penjinak untuk tidak mengeksekusi perintah "suara" adalah dengan hanya menambahkan metode yang tidak seharusnya dia miliki. Mari kita tambahkan metode seperti itu, itu akan memberikan informasi tentang apakah hewan peliharaan itu dapat dilatih.

 package circus type Animal interface { Speaker IsTrained() bool } 

Sekarang penjinak tidak bisa tergelincir di tempat hewan peliharaan.

Perbanyak Perilaku


Kami akan memaksa hewan peliharaan kami, untuk melakukan perubahan, untuk menjalankan perintah lain, sebagai tambahan, mari kita tambahkan kucing.

 package animal type Dog struct{} func (d Dog) IsTrained() bool { return true } func (d Dog) Speaks() string { return "woof" } func (d Dog) Jump() string { return "jumps" } func (d Dog) Sit() string { return "sit" } type Cat struct{} func (c Cat) IsTrained() bool { return false } func (c Cat) Speaks() string { return "meow!" } func (c Cat) Jump() string { return "meow!!" } func (c Cat) Sit() string { return "meow!!!" } 

 package circus const ( ActVoice = iota ActSit ActJump ) type Animal interface { Speaker IsTrained() bool Jump() string Sit() string } func (t *Tamer) Command(action int, a Animal) string { switch action { case ActVoice: return a.Speaks() case ActSit: return a.Sit() case ActJump: return a.Jump() } return "" } 

Hebat, sekarang kita bisa memberikan perintah yang berbeda kepada hewan kita, dan mereka akan melaksanakannya. Pada tingkat tertentu ...: D

 package main import ( "animal" "circus" ) func main() { t := &circus.Tamer{} d := &animal.Dog{} t.Command(circus.ActVoice, d) // "woof" t.Command(circus.ActJump, d) // "jumps" t.Command(circus.ActSit, d) // "sit" t2 := &circus.Tamer{} c := &animal.Cat{} t2.Command(circus.ActVoice, c) // "meow" t2.Command(circus.ActJump, c) // "meow!!" t2.Command(circus.ActSit, c) // "meow!!!" } 

Kucing rumah tangga kami tidak bisa menerima pelatihan. Oleh karena itu, kami akan membantu penjinak dan memastikan bahwa ia tidak menderita bersama mereka.

 package circus func (t *Tamer) Command(action int, a Animal) string { if !a.IsTrained() { panic("Sorry but this animal doesn't understand your commands") } // ... } 

Itu lebih baik. Tidak seperti antarmuka Animal awal, yang menduplikasi Speaker , kami sekarang memiliki antarmuka `Animal` (yang pada dasarnya adalah tipe data abstrak) yang mengimplementasikan perilaku yang cukup bermakna.

Mari kita bahas ukuran antarmuka


Sekarang mari kita renungkan masalah seperti menggunakan antarmuka yang luas.

Ini adalah situasi di mana kami menggunakan antarmuka dengan sejumlah besar metode. Dalam hal ini, rekomendasinya kira-kira seperti ini: "Fungsi harus menerima antarmuka yang berisi metode yang mereka butuhkan . "

Secara umum, saya setuju bahwa antarmuka harus kecil, tetapi dalam kasus ini, konteksnya lagi penting. Mari kita kembali ke kode kita dan mengajar penjinak kita untuk "memuji" peliharaannya.

Menanggapi pujian, hewan peliharaan akan mengeluarkan suara.

 package circus func (t *Tamer) Praise(a Speaker) string { return a.Speaks() } 

Tampaknya semuanya baik-baik saja, kami menggunakan antarmuka minimum yang diperlukan. Tidak ada yang berlebihan. Tapi di sini lagi masalahnya. Sialan, sekarang kita bisa "memuji" pelatih lain dan dia akan "memberikan suara" . : D Raih? .. Konteks selalu penting.

 package main import ( "animal" "circus" ) func main() { t := &circus.Tamer{} t2 := &circus.Tamer{} d := &animal.Dog{} c := &animal.Cat{} t.Praise(d) // woof t.Praise(c) // meow! t.Praise(t2) // WAT? } 

Kenapa saya? Dalam hal ini, solusi terbaik masih akan menggunakan antarmuka yang lebih luas (mewakili tipe data abstrak "pet" ). Karena kami ingin belajar cara memuji hewan peliharaan, bukan makhluk apa pun yang dapat mengeluarkan suara.

 package circus // Now we are using Animal interface here. func (t *Tamer) Praise(a Animal) string { return a.Speaks() } 

Jauh lebih baik. Kita bisa memuji hewan peliharaan, tetapi kita tidak bisa memuji penjinaknya. Kode kembali menjadi lebih sederhana dan lebih jelas.

Sekarang sedikit tentang Hukum Tempat Tidur


Poin terakhir yang ingin saya sentuh adalah rekomendasi bahwa kita harus menerima tipe abstrak dan mengembalikan struktur tertentu. Dalam artikel aslinya, penyebutan ini diberikan pada bagian yang menggambarkan apa yang disebut Hukum Postel .

Penulis mengutip hukum itu sendiri:.
"Jadilah konservatif dengan apa yang Anda lakukan, menjadi liberal dengan Anda menerima"

Dan menafsirkannya dalam kaitannya dengan bahasa Go
"Go": "Terima antarmuka, kembalikan struct"
func funcName(a INTERFACETYPE) CONCRETETYPE

Anda tahu, secara umum, saya setuju, ini adalah praktik yang baik. Namun, saya ingin menekankan lagi. Jangan menerimanya secara harfiah. Iblis ada dalam perinciannya. Seperti biasa, konteks itu penting.
Tidak selalu suatu fungsi harus mengembalikan tipe tertentu. Yaitu jika Anda membutuhkan tipe abstrak, kembalikan. Tidak perlu mencoba menulis ulang kode sambil menghindari abstraksi.

Ini adalah contoh kecil. Seekor gajah muncul di sirkus "Afrika" di dekatnya, dan Anda meminta pemilik sirkus untuk meminjamkan seekor gajah ke pertunjukan baru. Bagi Anda, dalam hal ini penting, hanya gajah yang dapat melakukan semua perintah yang sama seperti hewan peliharaan lainnya. Ukuran gajah atau keberadaan belalai dalam konteks ini tidak masalah.

 package african import "circus" type Elephant struct{} func (e Elephant) Speaks() string { return "pawoo!" } func (e Elephant) Jump() string { return "o_O" } func (e Elephant) Sit() string { return "sit" } func (e Elephant) IsTrained() bool { return true } func GetElephant() circus.Animal { return &Elephant{} } 

 package main import ( "african" "circus" ) func main() { t := &circus.Tamer{} e := african.GetElephant() t.Command(circus.ActVoice, e) // "pawoo!" t.Command(circus.ActJump, e) // "o_O" t.Command(circus.ActSit, e) // "sit" } 

Seperti yang Anda lihat, karena parameter khusus gajah yang membedakannya dari hewan peliharaan lain tidak penting bagi kami, kami mungkin menggunakan abstraksi, dan mengembalikan antarmuka dalam hal ini akan sangat tepat.

Untuk meringkas


Konteks sangat penting dalam hal abstraksi. Jangan abaikan abstraksi dan takut pada mereka, sama seperti Anda seharusnya tidak menyalahgunakan mereka. Anda tidak boleh mengambil rekomendasi sebagai aturan. Ada pendekatan yang telah diuji oleh waktu, ada pendekatan yang belum diuji. Saya harap saya bisa membuka sedikit lebih dalam topik menggunakan antarmuka sebagai tipe data abstrak, dan menjauh dari contoh biasa dari perpustakaan standar.

Tentu saja, bagi sebagian orang posting ini mungkin tampak terlalu jelas, dan contoh-contoh disedot dari jari. Bagi yang lain, pikiran saya mungkin kontroversial, dan argumennya tidak meyakinkan. Namun demikian, seseorang mungkin terinspirasi dan mulai berpikir sedikit lebih dalam tidak hanya tentang kode, tetapi juga tentang esensi hal-hal, serta abstraksi pada umumnya.

Hal utama, teman, adalah Anda terus-menerus mengembangkan dan menerima kesenangan sejati dari pekerjaan. Baik untuk semua!

PS. Kode contoh dan versi final dapat ditemukan di GitHub .

Source: https://habr.com/ru/post/id450386/


All Articles