Tidak ada kebetulan di balik io.Reader yang hanya berisi satu method, padahal package io memegang peran sentral di seluruh ekosistem Go. Ukuran kecil itu justru kuncinya: interface yang ramping diterapkan lebih luas, diganti lebih mudah, dan dipahami lebih cepat. Chapter ini membahas prinsip yang dipegang komunitas Go tentang ukuran interface, lokasi ideal interface dideklarasikan — di sisi pemakai, bukan implementasi — serta idiom accept interface, return struct yang mengikuti dari prinsip tersebut.
Semakin Besar, Semakin Lemah #
“The bigger the interface, the weaker the abstraction.” — Rob Pike
Kutipan di atas bukan slogan kosong. Setiap method yang ditambahkan ke interface menuntut semua implementasi menyediakannya dan membatasi semua pemakai menganggapnya penting. Interface satu method bisa dipenuhi ratusan tipe berbeda; interface dua puluh method hanya cocok untuk segelintir struct yang kebetulan hidupnya mirip. Abstraksi yang hanya bisa dipakai di satu tempat bukan abstraksi — itu duplikasi deklarasi.
Ukuran kecil juga menentukan kualitas pengujian. Pengganti tiruan (fake) untuk io.Reader cukup mengembalikan byte; pengganti untuk interface bermethod belasan akan memaksa Anda menulis stub kosong berbaris-baris. Ketika menulis interface baru, mulailah dari nol method lalu tambahkan hanya yang benar-benar dipanggil kode pemakai — sering kali jawaban akhirnya satu atau dua.
Interface Didefinisikan di Sisi Pemakai #
Aturan kedua sering terasa mundur arah bagi pemrogram yang datang dari bahasa lain: interface dideklarasikan bersama kode yang menggunakannya, bukan bersama tipe yang mengimplementasikannya. Pepatah Go menyebutnya singkat: the interface goes where it is used. Karena implementasi bersifat implisit, paket EmailNotifier tidak perlu mengimpor paket tempat Notifier dideklarasikan; hubungan hanya hidup di titik pemakaian.
Konsekuensinya: jangan mendefinisikan interface “sebagai antisipasi” pada setiap struct. Buat interface ketika ada kode nyata yang membutuhkan variasi — fungsi yang perlu diganti saat pengujian, atau dua implementasi yang memang hidup berdampingan. Sebelum kebutuhan itu ada, struct konkret lebih jujur: field-nya terbaca, method-nya bisa dievaluasi langsung, dan refactoring ke interface tinggal dilakukan begitu kebutuhan muncul.
Accept Interface, Return Struct #
Pola yang lahir dari dua aturan di atas dirangkum dalam satu kalimat: accept interface, return struct. Parameter fungsi sebaiknya bertipe interface — sering mungkin — supaya pemanggil bebas memasukkan implementasi apa pun yang memenuhi kontrak. Nilai balik sebaiknya bertipe struct konkret supaya pemanggil melihat field, membaca method, dan tetap bisa membungkusnya sendiri ke interface pilihannya. Nilai balik bertipe interface justru menyembunyikan informasi: pemanggil tidak tahu bentuk data yang diterimanya dan tidak bisa mengonstruksinya kembali.
Program Demo: Notifier #
Berkas notifier-demo.go menerapkan prinsip interface kecil dan deklarasi di sisi pemakai pada satu kontrak pengiriman pesan:
package main
import "fmt"
// Notifier adalah kontrak pengiriman pesan: cukup satu method.
type Notifier interface {
Kirim(penerima, pesan string) string
}
// EmailNotifier dan SmsNotifier sama-sama kecil dan mudah diganti.
type EmailNotifier struct {
Domain string
}
func (e EmailNotifier) Kirim(penerima, pesan string) string {
return "email ke " + penerima + "@" + e.Domain + ": " + pesan
}
type SmsNotifier struct {
Gerbang string
}
func (s SmsNotifier) Kirim(penerima, pesan string) string {
return "sms via " + s.Gerbang + " ke " + penerima + ": " + pesan
}
// broadcast mendefinisikan Notifier di sisi pemakai, bukan di sisi implementasi.
func broadcast(n Notifier, penerima []string, pesan string) {
for _, p := range penerima {
fmt.Println(n.Kirim(p, pesan))
}
}
func main() {
penerima := []string{"budi", "siti"}
broadcast(EmailNotifier{Domain: "example.com"}, penerima, "server siap")
broadcast(SmsNotifier{Gerbang: "gateway-1"}, penerima, "server siap")
}Output yang Dihasilkan:
email ke [email protected]: server siap
email ke [email protected]: server siap
sms via gateway-1 ke budi: server siap
sms via gateway-1 ke siti: server siapMari kita telaah baris demi baris program di atas:
- Baris 6–8 (
type Notifier interface): kontrak satu methodKirim; satu method membuat fake pengujian cukup satu fungsi pendek. - Baris 11–17 (
EmailNotifier): implementasi email hanya tahu domain; tidak ada field yang dipaksa mengikuti kebutuhan interface. - Baris 19–25 (
SmsNotifier): implementasi kedua dengan field berbeda total; keduanya memenuhi kontrak yang sama tanpa saling kenal. - Baris 27–32 (
func broadcast): pemakai kontrak berada satu berkas dengan deklarasinya; fungsi hanya memanggilKirim, sehingga interface tidak pernah melebihi kebutuhan nyata. - Baris 36–37 (dua pemanggilan
broadcast): pemanggil memilih implementasi per panggilan; menambah channel ketiga — misalnya webhook — tidak menyentuhbroadcastsama sekali.
Interface kecil yang didefinisikan di sisi pemakai menghasilkan satu efek samping yang sangat berharga: dependensi antarmodul bisa dibalik arah. Modul level atas berhenti mengenal detail modul level bawah — prinsip dependency inversion yang kita bahas sebagai penutup section ini.
Referensi #
- Effective Go: Interfaces — filosofi interface kecil dan pemakaiannya.
- Dokumentasi package io
— bukti nyata kekuatan interface satu method
io.Reader.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
notifier-demo.go
Unduh berkaspackage main
import "fmt"
// Notifier adalah kontrak pengiriman pesan: cukup satu method.
type Notifier interface {
Kirim(penerima, pesan string) string
}
// EmailNotifier dan SmsNotifier sama-sama kecil dan mudah diganti.
type EmailNotifier struct {
Domain string
}
func (e EmailNotifier) Kirim(penerima, pesan string) string {
return "email ke " + penerima + "@" + e.Domain + ": " + pesan
}
type SmsNotifier struct {
Gerbang string
}
func (s SmsNotifier) Kirim(penerima, pesan string) string {
return "sms via " + s.Gerbang + " ke " + penerima + ": " + pesan
}
// broadcast mendefinisikan Notifier di sisi pemakai, bukan di sisi implementasi.
func broadcast(n Notifier, penerima []string, pesan string) {
for _, p := range penerima {
fmt.Println(n.Kirim(p, pesan))
}
}
func main() {
penerima := []string{"budi", "siti"}
broadcast(EmailNotifier{Domain: "example.com"}, penerima, "server siap")
broadcast(SmsNotifier{Gerbang: "gateway-1"}, penerima, "server siap")
}