Pertimbangan Generics

Kapan generics membantu dibanding interface biasa.

Daftar isi

Generics dan interface sama-sama menghindari duplikasi kode, tetapi keduanya menyelesaikan persoalan yang berbeda. Topik ini menimbang kapan generics membantu, kapan interface biasa tetap lebih tepat, dan biaya apa yang dibayar untuk masing-masing. Setelah membacanya, pemilihan antara parameter tipe dan interface dapat dilandasi kriteria yang jelas pada saat mendesain fungsi dan tipe.

Kapan generics membantu#

Generics membantu ketika logikanya identik untuk banyak tipe dan tipe konkretnya perlu diketahui di dalam kode. Tanda-tanda yang paling sering muncul adalah fungsi yang mengembalikan tipe sejenis dengan masukannya, operasi perbandingan atau aritmetika atas bilangan, serta kontainer yang menyimpan satu jenis nilai. Pada ketiganya, pendekatan interface memaksa asersi tipe atau salinan kode, sedangkan parameter tipe mempertahankan informasi tipe tanpa biaya tambahan saat program berjalan. Generics juga menghindari penyimpanan nilai ke dalam interface pada beberapa kasus, sehingga alokasi tambahan dapat dikurangi.

go terbesar-generik.go
package main

import (
	"cmp"
	"fmt"
)

// terbesar mengembalikan elemen terbesar dari slice yang tidak kosong.
func terbesar[T cmp.Ordered](x []T) T {
	m := x[0]
	for _, v := range x[1:] {
		if v > m {
			m = v
		}
	}
	return m
}

func main() {
	skor := []int{78, 91, 66, 84}
	mataUji := []string{"aljabar", "statistika", "fisika"}

	fmt.Println(terbesar(skor))
	fmt.Println(terbesar(mataUji))
}
text Keluaran
91
statistika

Fungsi terbesar menerima constraint cmp.Ordered dari paket cmp, yaitu himpunan tipe yang mendukung perbandingan urutan seperti bilangan dan string. Satu definisi melayani []int dan []string, dan nilai kembaliannya bertipe sama dengan elemen masukannya. Bila fungsi yang sama ditulis dengan any, hasilnya harus berasersi kembali dan kesalahan tipe baru terlihat saat program berjalan. Kasus yang tidak cocok sama pentingnya untuk dikenali. Bila badan fungsi memerlukan perilaku khusus per tipe, parameter tipe hanya memindahkan percabangan ke constraint yang dipaksakan, dan kodenya menjadi lebih rumit daripada dua fungsi konkret.

Kapan interface lebih tepat#

Interface lebih tepat ketika yang disatukan adalah perilaku, bukan logika yang sama. Kumpulan data yang isinya bercampur tipe, batas antar paket yang bergantung pada perilaku seminimal mungkin, dan fungsi yang menerima banyak implementasi berbeda merupakan wilayah yang cocok untuk interface. Parameter tipe justru memaksa seluruh item bertipe sama pada satu pemanggilan, sehingga polimorfisme yang dikehendaki hilang. Pustaka standar menggunakan pola ini secara luas, seperti io.Reader yang melayani berkas, koneksi jaringan, dan penyangga memori dengan satu deklarasi. Ukuran yang berguna untuk menimbang keduanya adalah frekuensi perubahan, karena kontrak interface yang kecil jarang berubah dan mudah ditambah implementasi, sedangkan constraint yang terus tumbuh menandai abstraksi yang dipaksakan.

go sapaan-interface.go
package main

import "fmt"

// Penyapa menggambarkan perilaku menyapa tanpa memandang tipenya.
type Penyapa interface {
	Sapa() string
}

// Siswa adalah satu peserta kelas.
type Siswa struct {
	Nama string
}

// Sapa menyapa dengan gaya ringan.
func (s Siswa) Sapa() string {
	return "Halo, " + s.Nama
}

// Guru adalah satu pengajar kelas.
type Guru struct {
	Nama string
}

// Sapa menyapa dengan gaya formal.
func (g Guru) Sapa() string {
	return "Selamat pagi, " + g.Nama
}

func main() {
	ruangan := []Penyapa{
		Siswa{Nama: "Budi"},
		Guru{Nama: "Citra"},
		Siswa{Nama: "Dewi"},
	}

	for _, p := range ruangan {
		fmt.Println(p.Sapa())
	}
}
text Keluaran
Halo, Budi
Selamat pagi, Citra
Halo, Dewi

Slice ruangan bertipe []Penyapa menampung Siswa dan Guru sekaligus karena yang dituntut hanyalah metode Sapa. Setiap pemanggilan p.Sapa() menjalankan perilaku tipe konkretnya masing-masing saat program berjalan. Generics tidak dapat menggantikan pola ini, karena satu pemanggilan fungsi generik hanya melayani satu tipe.

Tabel berikut merangkum perbedaan keduanya.

AspekGenericsInterface
Pemeriksaan tipeSaat kompilasiTipe dinamis saat program berjalan
Keseragaman dataSatu tipe per instansiasiBanyak tipe dalam satu nilai interface
Yang disatukanLogika sama atas tipe berbedaPerilaku berbeda atas kontrak yang sama
Biaya penulisanDeklarasi parameter tipe dan constraintImplementasi metode pada setiap tipe
Contoh yang cocokPencarian, pengurutan, kontainerio.Reader, fmt.Stringer, batas antar paket

Panduan pemilihan#

Panduan resmi Go dalam artikel When To Use Generics menempatkan generics sebagai alat untuk kode yang logikanya identik antar tipe, dan bukan sebagai pengganti interface. Urutan praktisnya dimulai dari tipe konkret, karena tipe konkret memberi kesempatan melihat kebutuhan yang nyata sebelum abstraksi dibuat. Bila perilakunya beragam, gunakan interface kecil. Generics masuk ketika salinan kode untuk tipe kedua benar-benar muncul dan isinya sama persis.

Artikel tersebut juga menyarankan agar generics tidak dipakai hanya karena kodenya terlihat lebih rapi, karena abstraksi yang tidak dibutuhkan tetap dibaca oleh setiap pengguna API-nya. Pertimbangan yang sama berlaku untuk tipe generik, yaitu bila penampungnya hanya dipakai dengan satu tipe, deklarasi konkret sudah cukup. Kriteria itu juga menjaga API tetap ramping, karena setiap parameter tipe menambah hal yang harus dipahami pembaca tanda tangan fungsinya.

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul saat memilih antara generics dan interface. Ketiganya berpangkal pada keinginan menyatukan kode sebelum kebutuhannya benar-benar terlihat.

  • Mengganti interface dengan generics pada batas yang menuntut banyak implementasi. Fungsi generik hanya menerima satu tipe per pemanggilan, sehingga pengirim banyak implementasi sekaligus tidak terlayani. Perbaikannya, gunakan interface untuk batas perilaku dan simpan generics untuk logika yang seragam. Pertanyaan pemandunya sederhana, yaitu apakah tipe konkretnya perlu diketahui di dalam logika, ataukah yang dibutuhkan hanyalah perilakunya.
  • Menulis fungsi generik padahal parameternya hanya dipanggil dengan satu tipe. Deklarasi [T any] untuk fungsi yang selalu menerima string hanya menambah bunyi tanpa manfaat. Perbaikannya, tulis tipe konkretnya dan tunggu kebutuhan tipe kedua sebelum menggeneralisasi.
  • Memaksakan generics untuk logika yang sebenarnya berbeda antar tipe. Bila badan fungsi bercabang per tipe melalui type switch, keunggulan generics hilang. Perbaikannya, pisahkan logikanya per tipe atau satukan lewat metode pada interface.

Ringkasan#

  • Generics membantu untuk logika identik antar tipe yang membutuhkan tipe konkret, misalnya pencarian, pengurutan, dan kontainer.
  • Interface lebih tepat untuk menyatukan perilaku berbeda atas kontrak yang sama, termasuk kumpulan data yang bercampur tipe.
  • cmp.Ordered adalah constraint pustaka standar untuk tipe yang mendukung perbandingan urutan.
  • Urutan praktisnya mulai dari tipe konkret, lalu interface kecil untuk perilaku, dan generics ketika salinan untuk tipe kedua muncul.

Referensi#