Perangkat lunak tumbuh dengan menambah perilaku, dan pertanyaan klasiknya adalah bagaimana perilaku itu dibagikan antar bagian. Bahasa berorientasi objek klasik menjawab dengan pewarisan berjenjang: kelas anak mewarisi kelas induk bertingkat-tingkat. Go memilih jalan lain — composition over inheritance — perilaku dirakit dari struct kecil yang tersemat dan interface yang kecil, bukan diturunkan dari hierarki. Pada chapter penutup section ini, kita rangkai layanan nyata dari Logger tersemat, lalu lihat mengapa pendekatan ini lebih mudah diuji dan dikembangkan.
Komposisi Menggantikan Inheritance #
Dua alat menjadi pilar desain Go. Pertama, struct embedding yang kita bahas pada chapter Embedded Struct : satu tipe memuat tipe lain dan mewarisi akses singkat ke field serta method-nya. Kedua, interface: kontrak perilaku yang dipenuhi tipe apa pun tanpa hubungan keturunan. Kombinasinya menggantikan dua fungsi pewarisan sekaligus — penggunaan ulang implementasi lewat embedding, dan polimorfisme lewat interface.
Program berikut membangun dua layanan yang berbagi satu Logger tanpa satu pun hubungan “induk-anak”:
package main
import "fmt"
type Logger struct {
Awalan string
}
func (l *Logger) Catat(pesan string) {
fmt.Println("[" + l.Awalan + "] " + pesan)
}
type Pencatat interface {
Catat(pesan string)
}
type Pengolah struct {
*Logger
Nama string
}
func (p *Pengolah) Proses(berkas string) {
p.Catat("mulai memproses " + berkas)
p.Catat("selesai memproses " + berkas)
}
func main() {
logger := &Logger{Awalan: "utama"}
a := &Pengolah{Logger: logger, Nama: "pengolah-A"}
b := &Pengolah{Logger: logger, Nama: "pengolah-B"}
a.Proses("laporan.csv")
b.Proses("foto.png")
var pencatat Pencatat = a
pencatat.Catat("dipanggil lewat interface")
}Jalankan program dari direktori chapter:
go run composition-demo.goOutput yang Dihasilkan:
[utama] mulai memproses laporan.csv
[utama] selesai memproses laporan.csv
[utama] mulai memproses foto.png
[utama] selesai memproses foto.png
[utama] dipanggil lewat interfaceMari kita telaah baris demi baris program di atas:
- Baris 5–11 (
Loggerdan methodCatat): mendefinisikan struct Logger dengan method pointer receiver yang mencetak pesan berprefiks[utama]dari fieldAwalan. - Baris 13–15 (
type Pencatat interface): kontrak satu method — interface sekecil ini menjadi perekat komposisi di Go. - Baris 17–20 (
type Pengolah struct): menyematkan*Logger(embedded pointer) di samping fieldNama; promosi method membuatp.Catatlangsung tersedia. - Baris 22–25 (
func (p *Pengolah) Proses): layanan inti yang memakai Logger tersemat untuk mencatat awal dan akhir proses. - Baris 28–31 (
loggerdibagikan): satu instance Logger disuntikkan ke duaPengolah— kedua layanan mencetak dengan prefiksutamayang sama tanpa menggandakan kode. - Baris 33–34 (
a.Prosesdanb.Proses): dua layanan berjalan dengan perilaku Logging identik — buah penggunaan ulang lewat komposisi, bukan pewarisan. - Baris 36–37 (
var pencatat Pencatat = a):*Pengolahotomatis memenuhi interfacePencatatkarena methodCatatterpromosi dari*Logger.
Interface Kecil Menjaga Desain Longgar #
Baris terakhir program menunjukkan kunci komposisi: Pengolah tidak pernah mendeklarasikan dirinya sebagai Pencatat — kecocokan terjadi otomatis karena method yang tepat tersedia. Fungsi yang menerima Pencatat menerima tipe apa pun yang bisa mencatat, sehingga kita bisa menyuntikkan Logger lain berprefiks berbeda tanpa menyentuh Pengolah sama sekali. Prinsip interface satu-dua method ini dibedah tuntas pada chapter Kecilnya Interface
.
Keunggulan Komposisi #
- Hubungan eksplisit: kode membaca langsung tipe apa yang dirakit; tidak ada perilaku tersembunyi dari induk tiga tingkat di atas.
- Mudah diuji: Logger sungguhan bisa diganti tiruan sederhana tanpa struktur pewarisan.
- Bentrok tertangkap kompilator: dua embedded field dengan nama method sama ditolak pesan
ambiguous selector, bukan menjadi diamond problem saat runtime. - Rakit-ulang bebas: satu struct bisa dilepas dari komposisi tanpa merombak hierarki turunannya.
Batasi Kedalaman Embedding #
Standard library sendiri memakai pola dua-level yang terkendali: bufio.ReadWriter menyematkan *Reader dan *Writer dalam satu struct — contoh nyata bahwa komposisi satu-dua tingkat sudah cukup untuk kebutuhan nyata. Dengan data yang dirakit lewat struct dan perilaku yang dilepas-lepas lewat interface, fondasi kita lengkap; saatnya melangkah ke section Interface
yang membedah kontrak perilaku ini sampai ke akarnya.
Referensi #
- Effective Go: Embedding — filosofi resmi embedding di Go.
- Effective Go: Interfaces — peran interface dalam komposisi.
- Dokumentasi package bufio
— contoh
ReadWriterdari standard library.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
composition-demo.go
Unduh berkaspackage main
import "fmt"
type Logger struct {
Awalan string
}
func (l *Logger) Catat(pesan string) {
fmt.Println("[" + l.Awalan + "] " + pesan)
}
type Pencatat interface {
Catat(pesan string)
}
type Pengolah struct {
*Logger
Nama string
}
func (p *Pengolah) Proses(berkas string) {
p.Catat("mulai memproses " + berkas)
p.Catat("selesai memproses " + berkas)
}
func main() {
logger := &Logger{Awalan: "utama"}
a := &Pengolah{Logger: logger, Nama: "pengolah-A"}
b := &Pengolah{Logger: logger, Nama: "pengolah-B"}
a.Proses("laporan.csv")
b.Proses("foto.png")
var pencatat Pencatat = a
pencatat.Catat("dipanggil lewat interface")
}