Kebutuhan nyata jarang datang satu-satu: operasi file perlu membaca sekaligus menulis, pipeline perlu menerima dan mengirim. Bahasa lain cenderung menjawab dengan interface raksasa yang mendaftar puluhan method sekali jadi. Go menjawab dengan komposisi: interface bisa disematkan di dalam interface lain, sehingga kontrak besar dibangun dari kontrak-kontrak kecil yang sudah teruji. Chapter ini membahas aturan penyematan interface, menelusuri pola io.ReadWriter di standard library, lalu membandingkan pendekatan komposisi dengan pendekatan interface raksasa.
Menyematkan Interface di Dalam Interface #
Sebuah deklarasi interface boleh memuat baris bertipe interface lain. Baris seperti itu disebut embedded interface, dan method set interface hasilnya adalah gabungan (union) dari semua method yang disematkan:
type PembacaPenulis interface {
Pembaca
Penulis
}Deklarasi di atas sama artinya dengan menuliskan Baca() string dan Tulis(s string) secara manual — tetapi tanpa menggandakan signature, dan tetap merujuk kontrak aslinya. Tipe yang ingin memenuhi PembacaPenulis harus mengimplementasikan kedua method itu; tidak ada jalan pintas. Nama method dari tiap interface penyusun juga tidak boleh bertabrakan, karena dua method dengan nama sama namun signature berbeda akan ditolak compiler.
Pola io.ReadWriter di Standard Library #
Standard library memakai pola ini secara sistematis. Package io membangun seluruh kosakata operasi data dari dua interface inti:
type ReadWriter interface {
Reader
Writer
}io.ReadWriter menyematkan io.Reader dan io.Writer; io.ReadWriteCloser menambahkan io.Closer. Tipe *os.File memenuhi ketiganya sekaligus tanpa mendaftarkan diri ke mana pun, berkat implementasi implisit yang kita bahas sebelumnya. Fungsi yang hanya butuh membaca cukup meminta io.Reader; fungsi yang butuh keduanya meminta io.ReadWriter — dan pemanggil menaikkan *os.File atau *bytes.Buffer ke kontrak yang sesuai tanpa konversi.
Komposisi versus Interface Raksasa #
Perbedaan kedua pendekatan ini bukan gaya penulisan, melainkan daya dukung:
| Aspek | Interface raksasa | Komposisi interface kecil |
|---|---|---|
| Beban implementasi | semua method, sekaligus | sesuai kontrak yang dipegang pemakai |
| Implementasi tiruan (test) | berat, banyak method kosong | cukup satu-dua method |
| Pemakaian ulang bagian | tidak bisa | kontrak kecil berdiri sendiri |
| Ruang salah paham kontrak | luas | sempit dan eksplisit |
Interface raksasa memaksa setiap implementasi menyediakan semuanya, termasuk method yang tidak relevan; kontrak kecil yang dikomposisikan membiarkan tiap pemakai memotong kebutuhannya sendiri. Prinsip ini berlanjut ke chapter Kecilnya Interface yang membahas batas ukuran ideal.
Program Demo #
Berkas composition-demo.go membangun kontrak PembacaPenulis lalu menggunakannya pada tipe Catatan berbasis strings.Builder:
package main
import (
"fmt"
"strings"
)
// Pembaca dan Penulis adalah dua interface kecil yang berdiri sendiri.
type Pembaca interface {
Baca() string
}
type Penulis interface {
Tulis(s string)
}
// PembacaPenulis menggabungkan dua interface menjadi satu kontrak baru.
type PembacaPenulis interface {
Pembaca
Penulis
}
// Catatan mengimplementasikan Pembaca sekaligus Penulis.
type Catatan struct {
isi strings.Builder
}
func (c *Catatan) Tulis(s string) {
c.isi.WriteString(s)
}
func (c *Catatan) Baca() string {
return c.isi.String()
}
func isiCatatan(rw PembacaPenulis, potongan []string) string {
for _, p := range potongan {
rw.Tulis(p)
}
return rw.Baca()
}
func main() {
var rw PembacaPenulis = &Catatan{}
hasil := isiCatatan(rw, []string{"belajar ", "komposisi ", "interface"})
fmt.Println(hasil)
}Output yang Dihasilkan:
belajar komposisi interfaceMari kita telaah baris demi baris program di atas:
- Baris 9–15 (
Pembaca,Penulis): dua kontrak kecil, masing-masing satu method; keduanya tetap berharga berdiri sendiri untuk fungsi yang hanya membaca atau hanya menulis. - Baris 18–21 (
type PembacaPenulis interface): komposisi sesungguhnya — dua nama interface disematkan tanpa signature ditulis ulang; method set hasilnya adalahBacadanTulis. - Baris 24–26 (
type Catatan struct): struct menyimpanstrings.Builder; builder sendiri sudah mengimplementasikan penulisan, struct kita membungkusnya dengan method sendiri. - Baris 28–34 (
TulisdanBaca): dua method dengan pointer receiver ini membuat*CatatanmemenuhiPembaca,Penulis, danPembacaPenulissekaligus. - Baris 36–41 (
func isiCatatan): fungsi meminta kontrak gabungan karena memang butuh menulis lalu membaca — bukan karena mengikuti kebiasaan. - Baris 44 (
var rw PembacaPenulis = &Catatan{}): alamat struct ditugaskan ke interface gabungan; penugasan ini lulus pemeriksaan method set saat kompilasi.
Komposisi menjaga kontrak tetap terurai dari bagian-bagian kecil. Pertanyaan desain berikutnya lebih mendasar lagi: seberapa kecil seharusnya sebuah interface, dan siapa yang seharusnya mendefinisikannya? Kedua pertanyaan itu dibahas tuntas pada chapter berikutnya.
Referensi #
- Dokumentasi package io
— contoh nyata
ReadWriter,ReadCloser, dan turunan komposisinya. - Spesifikasi Go: Interface types — aturan resmi interface yang disematkan.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
composition-demo.go
Unduh berkaspackage main
import (
"fmt"
"strings"
)
// Pembaca dan Penulis adalah dua interface kecil yang berdiri sendiri.
type Pembaca interface {
Baca() string
}
type Penulis interface {
Tulis(s string)
}
// PembacaPenulis menggabungkan dua interface menjadi satu kontrak baru.
type PembacaPenulis interface {
Pembaca
Penulis
}
// Catatan mengimplementasikan Pembaca sekaligus Penulis.
type Catatan struct {
isi strings.Builder
}
func (c *Catatan) Tulis(s string) {
c.isi.WriteString(s)
}
func (c *Catatan) Baca() string {
return c.isi.String()
}
func isiCatatan(rw PembacaPenulis, potongan []string) string {
for _, p := range potongan {
rw.Tulis(p)
}
return rw.Baca()
}
func main() {
var rw PembacaPenulis = &Catatan{}
hasil := isiCatatan(rw, []string{"belajar ", "komposisi ", "interface"})
fmt.Println(hasil)
}