Penyematan struct

Komposisi alih-alih pewarisan dan field terpromosi.

Daftar isi

Go tidak memiliki pewarisan (inheritance) dan kelas, sehingga penyusunan tipe dari tipe lain dilakukan melalui komposisi, dan penyematan struct (struct embedding) adalah bentuk komposisi yang disediakan bahasa ini. Penyematan menempatkan sebuah tipe ke dalam struct tanpa nama field, lalu field dan metode tipe yang disematkan naik (promoted) ke struct pembungkusnya. Topik ini membahas mekanisme tersebut, penutupan metode terpromosi, serta batasan yang membedakannya dari pewarisan.

Penyematan field tanpa nama#

Field yang dideklarasikan tanpa nama, hanya menyebut tipenya, disebut field tersemat (embedded field). Nama field itu otomatis menjadi nama tipenya sendiri, sehingga struct berikut memiliki field bernama Identitas bertipe Identitas. Program berikut menyematkan tipe Identitas ke dalam struct Sensor:

go sensor.go
package main

import "fmt"

// Identitas menyimpan penanda satu alat pengukuran.
type Identitas struct {
	Kode   string
	Lokasi string
}

// Sensor menyimpan pengukuran suhu beserta identitas alatnya.
type Sensor struct {
	Identitas // penyematan: field bernama Identitas tanpa nama sendiri
	Suhu      float64
}

func main() {
	sensor := Sensor{
		Identitas: Identitas{Kode: "SN-01", Lokasi: "gudang"},
		Suhu:      27.5,
	}

	fmt.Println(sensor.Kode)   // field terpromosi dari Identitas
	fmt.Println(sensor.Lokasi) // field terpromosi dari Identitas
	fmt.Println(sensor.Suhu)
}
text Keluaran
SN-01
gudang
27.5

Field Kode dan Lokasi tidak dideklarasikan pada Sensor, tetapi baris sensor.Kode tetap sah karena keduanya adalah field terpromosi (promoted fields), yaitu field dari tipe tersemat yang dapat diakses seolah milik struct pembungkusnya. Akses eksplisit melalui jalurnya juga sah, misalnya sensor.Identitas.Kode, dan bentuk inilah yang dipakai ketika struktur penyematannya perlu dinyatakan. Penulisan literal struct tetap menyebut field tersemat dengan nama tipenya, sebagaimana terlihat pada Identitas: Identitas{Kode: "SN-01", Lokasi: "gudang"}.

Keputusan antara field bernama biasa dan penyematan ditentukan oleh cara pemakaiannya. Field bernama ident Identitas sama-sama menyimpan data identitas, tetapi aksesnya selalu menulis jalur penuh, sedangkan penyematan memperpendek jalur akses menjadi sensor.Kode. Penyematan dipilih ketika struct pembungkus dianggap membawa seluruh ciri tipe yang disematkan, misalnya Sensor yang memang menampilkan dirinya sebagai alat lengkap dengan identitasnya.

Metode terpromosi dan penutupannya#

Selain field, metode dari tipe tersemat juga terpromosi, sehingga pemanggilan langsung pada struct pembungkusnya diteruskan ke tipe yang disematkan. Apabila struct pembungkus mendeklarasikan metode dengan nama yang sama, metode miliknya yang dipilih, dan metode terpromosi tetap dapat dicapai melalui nama tipe tersematnya:

go sensor-ringkas.go
package main

import "fmt"

// Identitas menyimpan kode satu alat pengukuran.
type Identitas struct {
	Kode string
}

// ringkas pada Identitas merangkai kode alat.
func (i Identitas) Ringkas() string {
	return fmt.Sprintf("identitas %s", i.Kode)
}

// Sensor menyimpan pengukuran suhu beserta identitas alatnya.
type Sensor struct {
	Identitas
	Suhu float64
}

// ringkas pada Sensor menutupi metode terpromosi dari Identitas.
func (s Sensor) Ringkas() string {
	return fmt.Sprintf("sensor %s suhu %.1f", s.Kode, s.Suhu)
}

func main() {
	sensor := Sensor{Identitas: Identitas{Kode: "SN-01"}, Suhu: 27.5}
	fmt.Println(sensor.Ringkas())
	fmt.Println(sensor.Identitas.Ringkas())
}
text Keluaran
sensor SN-01 suhu 27.5
identitas SN-01

Peristiwa pada program di atas disebut penutupan (shadowing), karena nama Ringkas pada Sensor menutupi nama yang sama dari Identitas. Mekanisme ini berbeda dari overriding pada bahasa berorientasi objek, karena tidak ada pemanggilan dinamis yang mengganti perilaku metode milik Identitas itu sendiri. Pemanggilan sensor.Identitas.Ringkas() tetap menjalankan versi Identitas apa adanya, dan versi tersebut tidak terpengaruh oleh keberadaan metode dengan nama serupa pada struct pembungkus.

Promosi ini berlaku berjenjang apabila penyematannya bertingkat. Misalnya, struct luar menyematkan Sensor, dan Sensor sendiri menyematkan Identitas. Dalam susunan tersebut, struct luar tetap dapat mengakses Kode karena promosi nama menyusuri seluruh rantai penyematannya. Kedalaman seperti ini perlu dijaga tetap dangkal, karena rantai yang panjang menyulitkan pembaca menemukan letak asli sebuah field.

Komposisi, bukan pewarisan#

Penyematan menghasilkan hubungan memiliki (has-a), bukan hubungan menjadi (is-a). Sensor yang menyematkan Identitas tidak menjadi subjenis Identitas, dan keduanya tetap dua tipe yang berbeda tanpa ikatan turunan. Penugasan berikut gagal dikompilasi dengan pesan yang menyatakan ketidakcocokan tipe:

go
// tidak-dijalankan
var i Identitas = sensor // gagal dikompilasi: cannot use sensor (variable of struct type Sensor) as Identitas value in variable declaration

Batasan ini sengaja dirancang. Pewarisan menautkan dua tipe sedemikian rupa sehingga perubahan tipe induk mengubah perilaku seluruh turunannya secara tersembunyi. Komposisi menjaga tiap tipe berdiri sendiri, dan kerja sama dinyatakan secara eksplisit di tempat penyematannya. Kebutuhan polimorfisme, yaitu kemampuan satu nilai menggantikan nilai lain yang setipe, ditangani Go melalui interface, bukan melalui hierarki tipe.

Polanya tetap memberi ruang yang luas. Satu lembar pencatatan dapat memuat banyak Sensor, dan tiap Sensor membawa Identitas beserta Suhu miliknya sendiri. Hubungan has-a semacam ini tidak memerlukan ikatan turunan, sehingga dua tipe yang bekerja sama dapat berubah masing-masing tanpa menyeret yang lain. Ketika perilaku bersama memang dibutuhkan, interface menetapkannya melalui sekumpulan metode yang dipenuhi secara implisit.

Kesalahan umum#

  • Membaca penyematan sebagai pewarisan. Penyebabnya adalah kebiasaan dari bahasa berorientasi objek, dan akibatnya kode mengharapkan perilaku turunan yang tidak pernah dijanjikan Go. Perbaikannya, perlakukan tipe tersemat sebagai field biasa yang field dan metodenya dapat diakses langsung, lalu gunakan interface bila polimorfisme dibutuhkan.
  • Menyematkan dua tipe yang sama-sama memuat nama metode, lalu memanggilnya tanpa jalur eksplisit. Kompilasi gagal dengan pesan ambiguous selector alat.Ringkas karena pemilihan metodenya ambigu. Perbaikannya, sebut jalurnya secara eksplisit, misalnya alat.Identitas.Ringkas().
  • Memberi nama pada field yang dimaksudkan sebagai penyematan. Bentuk Identitas Identitas adalah field bernama biasa, sehingga promosi field dan metodenya tidak terjadi. Perbaikannya, tulis cukup Identitas tanpa nama field agar penyematan terbentuk.
  • Mengira field terpromosi terpisah dari struct tersematnya saat disalin. Penugasan struct menyalin seluruh field, termasuk struct tersemat beserta isinya, sehingga salinan sensor memiliki salinan Identitas-nya sendiri. Perbaikannya, gunakan receiver pointer atau field bertipe pointer bila dua salinan harus berbagi satu keadaan yang sama.

Ringkasan#

  • Penyematan mendeklarasikan field tanpa nama, dan nama fieldnya otomatis menjadi nama tipe yang disematkan.
  • Field dan metode tipe tersemat naik menjadi field dan metode terpromosi pada struct pembungkusnya.
  • Metode dengan nama sama pada struct pembungkus menutupi metode terpromosi, dan jalur eksplisit mengakses versi tersembunyi.
  • Penyematan adalah komposisi, bukan pewarisan, karena tidak ada hubungan subjenis yang ikut terbentuk.

Referensi#