Cara menggunakan antarmuka di Go



Di waktu luangnya dari pekerjaan utama, penulis materi berkonsultasi pada Go dan mengurai kode. Tentu saja, dalam kegiatan seperti itu, ia membaca banyak kode yang ditulis oleh orang lain. Baru-baru ini, penulis artikel ini memiliki kesan (ya, kesan, tidak ada statistik) bahwa programmer lebih mungkin untuk bekerja dengan antarmuka dalam "gaya Java".

Posting ini berisi rekomendasi dari penulis tentang penggunaan optimal antarmuka di Go, berdasarkan pengalamannya dalam menulis kode.

Pada contoh postingan ini kita akan menggunakan dua paket animal dan circus . Banyak hal dalam posting ini menggambarkan bekerja dengan kode yang berbatasan dengan penggunaan reguler paket.

Bagaimana tidak


Fenomena yang sangat umum yang saya amati:

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

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

Ini adalah apa yang disebut penggunaan antarmuka gaya Java. Ini dapat ditandai dengan langkah-langkah berikut:

  1. Tentukan antarmuka.
  2. Tentukan satu jenis yang memenuhi perilaku antarmuka.
  3. Tetapkan metode yang memenuhi implementasi antarmuka.

Singkatnya, kita berurusan dengan "tipe penulisan yang memenuhi antarmuka." Kode ini memiliki bau yang berbeda, menyarankan pemikiran berikut:

  • Hanya satu jenis yang memenuhi antarmuka, tanpa ada niat untuk memperluasnya lebih jauh.
  • Fungsi biasanya mengambil tipe konkret alih-alih jenis antarmuka.

Bagaimana cara melakukannya


Antarmuka di Go mendorong pendekatan malas, yang bagus. Alih-alih menulis jenis yang memenuhi antarmuka, Anda harus menulis antarmuka yang memenuhi persyaratan praktis nyata.

Yang berarti: alih-alih mendefinisikan Animal dalam paket animals , tentukan pada titik penggunaan, yaitu paket circus * .

 package animals 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() } 

Cara yang lebih alami untuk melakukan ini adalah seperti ini:

  1. Tentukan Jenis
  2. Tentukan antarmuka pada titik penggunaan.

Pendekatan ini mengurangi ketergantungan pada komponen paket animals . Mengurangi ketergantungan adalah cara yang tepat untuk membangun perangkat lunak yang tahan terhadap kesalahan.

Hukum Tempat Tidur


Ada satu prinsip yang baik untuk menulis perangkat lunak yang baik. Ini adalah hukum Postel , yang sering dirumuskan sebagai berikut:
“Jadilah konservatif tentang apa yang Anda rujuk dan liberal tentang apa yang Anda terima”
Dalam hal Go, hukumnya adalah:

“Terima antarmuka, kembalikan struktur”

Secara keseluruhan, ini adalah aturan yang sangat baik untuk merancang hal-hal yang toleran dan stabil * . Unit utama kode dalam Go adalah fungsi. Saat mendesain fungsi dan metode, penting untuk mematuhi pola berikut:

 func funcName(a INTERFACETYPE) CONCRETETYPE 

Di sini kami menerima semua yang mengimplementasikan antarmuka yang bisa apa saja, termasuk yang kosong. Nilai tipe tertentu disarankan. Tentu saja, membatasi apa yang bisa masuk akal. Seperti kata pepatah Go:

"Antarmuka kosong tidak mengatakan apa-apa," Rob Pike

Oleh karena itu, sangat disarankan untuk mencegah fungsi menerima interface{} .

Contoh Aplikasi: Imitasi


Contoh mencolok tentang manfaat penerapan hukum Postel adalah kasus uji. Katakanlah Anda memiliki fungsi yang terlihat seperti ini:

 func Takes(db Database) error 

Jika Database adalah antarmuka, maka dalam kode uji Anda dapat dengan mudah memberikan imitasi implementasi Database tanpa harus melewati objek DB nyata.

Ketika definisi antarmuka terlebih dahulu dapat diterima


Sejujurnya, pemrograman adalah cara yang cukup gratis untuk mengekspresikan ide. Tidak ada aturan yang tak tergoyahkan. Tentu saja, Anda selalu dapat mendefinisikan antarmuka terlebih dahulu, tanpa takut ditangkap oleh polisi kode. Dalam konteks banyak paket, jika Anda mengetahui fungsi Anda dan berniat untuk menerima antarmuka spesifik dalam paket, maka lakukanlah.

Mendefinisikan antarmuka biasanya bernada over-engineering, tetapi ada situasi di mana Anda harus melakukan hal itu. Secara khusus, contoh-contoh berikut muncul dalam pikiran:

  • Antarmuka tertutup
  • Tipe data abstrak
  • Antarmuka Rekursif

Selanjutnya, kami secara singkat mempertimbangkan masing-masing dari mereka.

Antarmuka tertutup


Antarmuka tertutup hanya dapat didiskusikan dalam konteks beberapa paket. Antarmuka yang disegel adalah antarmuka dengan metode yang tidak diekspor. Ini berarti bahwa pengguna di luar paket ini tidak dapat membuat jenis yang memenuhi antarmuka ini. Ini berguna untuk mengemulasi tipe varian agar dapat mencari tipe yang memuaskan antarmuka secara mendalam.

Jika Anda mendefinisikan sesuatu seperti ini:

 type Fooer interface { Foo() sealed() } 

Hanya paket yang ditentukan oleh Fooer dapat menggunakannya dan membuat sesuatu yang bernilai darinya. Ini memungkinkan Anda membuat operator switch brute-force untuk tipe.

Antarmuka yang disegel juga memungkinkan alat analisis untuk dengan mudah mengambil kecocokan pola non-tabrakan. Paket sumtypes BurntSushi bertujuan untuk mengatasi masalah ini.

Tipe data abstrak


Kasus lain mendefinisikan antarmuka sebelumnya melibatkan pembuatan tipe data abstrak. Mereka dapat disegel atau tidak disegel.

Contoh yang baik dari ini adalah paket sort , yang merupakan bagian dari pustaka standar. Ini mendefinisikan koleksi yang bisa diurutkan sebagai berikut

 type Interface interface { // Len —    . Len() int // Less      //   i     j. Less(i, j int) bool // Swap     i  j. Swap(i, j int) } 

Sepotong kode ini mengecewakan banyak orang, karena jika Anda ingin menggunakan paket sort Anda harus menerapkan metode untuk antarmuka. Banyak yang tidak suka harus menambahkan tiga baris kode tambahan.

Namun, saya menemukan ini sebagai bentuk generik yang sangat elegan di Go. Penggunaannya harus lebih sering didorong.

Pilihan desain alternatif dan pada saat yang sama akan membutuhkan jenis pesanan yang lebih tinggi. Dalam posting ini kami tidak akan mempertimbangkannya.

Antarmuka Rekursif


Ini mungkin contoh lain dari kode dengan stok, tetapi ada kalanya tidak mungkin untuk menghindari menggunakannya. Manipulasi sederhana memungkinkan Anda untuk mendapatkan sesuatu seperti

 type Fooer interface { Foo() Fooer } 

Pola antarmuka rekursif jelas akan memerlukan definisi di muka. Rekomendasi definisi antarmuka titik penggunaan tidak berlaku di sini.

Pola ini berguna untuk membuat konteks dengan pekerjaan selanjutnya di dalamnya. Kode yang dimuat oleh konteks biasanya membungkus dirinya di dalam paket dengan hanya mengekspor konteks (ala paket tensor ), oleh karena itu, dalam praktiknya, saya tidak sering menemukan kasus ini. Saya dapat memberi tahu Anda hal lain tentang pola kontekstual, tetapi tinggalkan untuk posting lain.

Kesimpulan


Terlepas dari kenyataan bahwa salah satu judul pos berbunyi "Bagaimana tidak melakukannya," saya sama sekali tidak mencoba untuk melarang apa pun. Sebaliknya, saya ingin membuat pembaca lebih sering berpikir tentang kondisi perbatasan, karena dalam kasus seperti itulah berbagai situasi darurat muncul.

Saya menemukan prinsip deklarasi penggunaan titik sangat berguna. Sebagai hasil penerapannya dalam praktik, saya tidak menemui masalah yang muncul jika saya mengabaikannya.

Namun, saya juga sesekali menulis antarmuka gaya Java. Biasanya, ini terjadi jika, tak lama sebelumnya, saya menulis banyak kode dalam Java atau Python. Keinginan untuk terlalu rumit dan "menyajikan segala sesuatu dalam bentuk kelas" kadang-kadang memanifestasikan dirinya dengan sangat kuat, terutama jika Anda menulis Go-code setelah menulis banyak kode berorientasi objek.

Dengan demikian, posting ini juga berfungsi sebagai pengingat untuk diri sendiri tentang bagaimana jalan untuk menulis kode yang nantinya tidak akan menyebabkan sakit kepala. Menunggu komentar Anda!

gambar

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


All Articles