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:

go
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:

go
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:

AspekInterface raksasaKomposisi interface kecil
Beban implementasisemua method, sekaligussesuai kontrak yang dipegang pemakai
Implementasi tiruan (test)berat, banyak method kosongcukup satu-dua method
Pemakaian ulang bagiantidak bisakontrak kecil berdiri sendiri
Ruang salah paham kontrakluassempit 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:

go
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:

text
belajar komposisi interface

Mari kita telaah baris demi baris program di atas:

  1. 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.
  2. Baris 18–21 (type PembacaPenulis interface): komposisi sesungguhnya — dua nama interface disematkan tanpa signature ditulis ulang; method set hasilnya adalah Baca dan Tulis.
  3. Baris 24–26 (type Catatan struct): struct menyimpan strings.Builder; builder sendiri sudah mengimplementasikan penulisan, struct kita membungkusnya dengan method sendiri.
  4. Baris 28–34 (Tulis dan Baca): dua method dengan pointer receiver ini membuat *Catatan memenuhi Pembaca, Penulis, dan PembacaPenulis sekaligus.
  5. Baris 36–41 (func isiCatatan): fungsi meminta kontrak gabungan karena memang butuh menulis lalu membaca — bukan karena mengikuti kebiasaan.
  6. 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

composition-demo.go

Unduh berkas
go
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)
}