Aplikasi sering menyediakan beberapa varian objek untuk satu kebutuhan yang sama: laporan CSV dan JSON, kompresor untuk beberapa algoritma, penyimpanan lokal dan cloud. Tanpa pola, pemanggil terpaksa mengenal setiap varian beserta cara membangunnya satu per satu. Factory pattern memindahkan keputusan tersebut ke satu fungsi pabrik: pemanggil menyebut apa yang diinginkan, fungsi pabrik yang menentukan bagaimana objek dibangun. Di Go pola ini tidak memerlukan hierarki class — cukup fungsi NewX yang mengembalikan interface, lengkap dengan error saat permintaan tidak bisa dipenuhi.
Factory di Go Adalah Fungsi #
Dalam OOP klasik, factory adalah abstract class yang harus diturunkan agar subclass menentukan produknya. Go mengambil jalan yang lebih ringan: konstruksi objek adalah tugas fungsi biasa. Effective Go bahkan mengajarkan konvensi penamaannya sejak awal — fungsi NewX mengembalikan nilai siap pakai, dan pemanggil tidak perlu tahu proses di baliknya.
Fungsi pabrik menyembunyikan tiga hal sekaligus: tipe konkret yang dipilih, langkah penyusunannya, dan validasi yang harus lolos sebelum objek terbentuk. Pemanggil cukup yakin satu hal: nilai yang kembali siap dipakai, atau error menjelaskan mengapa tidak.
Mengembalikan Interface, Bukan Struct Konkret #
Kunci pola ini ada pada tipe nilai kembalian. Constructor biasa mengembalikan struct konkret; factory mengembalikan interface:
// Pemanggil tidak tahu — dan tidak perlu tahu — struct apa yang kembali.
lap, err := NewLaporan("csv", data)Dengan interface sebagai nilai kembali, pemanggil hanya melihat perilaku yang tersedia. Kita bebas menambah varian baru, menghapus varian lama, atau mengubah isi struct tanpa mengubah satu baris pun di sisi pemanggil. Kesalahan pun dilaporkan di tempat yang sama, sehingga pemanggil tidak pernah menerima objek setengah jadi.
Variasi lewat Registry Map #
Ketika varian masih dua atau tiga, cabang switch di dalam factory sudah memadai. Masalah muncul saat varian bertambah: switch panjang harus dibuka dan diedit setiap kali varian baru masuk. Solusi idiomatiknya adalah registry berupa map yang memetakan kunci — string atau tipe enum — ke fungsi pabrik masing-masing varian. Menambah varian berarti menambah satu entri map, bukan membedah logika pemilihan.
Demo: Pembuat Laporan CSV dan JSON #
Berkas factory-demo.go pada bagian bawah halaman membangun pabrik laporan lengkap dengan registry map dan sentinel error. Pemanggil mencoba tiga format: csv dan json berhasil, sedangkan pdf ditolak factory. Kode lengkapnya adalah sebagai berikut:
package main
import (
"errors"
"fmt"
"strings"
)
// Laporan adalah interface yang diketahui pemanggil.
// Format konkret (csv/json) disembunyikan di balik constructor.
type Laporan interface {
Bentuk() string
Render() string
}
// DataPembelian adalah input mentah yang sama untuk semua format.
type DataPembelian struct {
Pembeli string
Items []string
}
// laporanCSV adalah tipe konkret yang tidak diekspor.
type laporanCSV struct {
data DataPembelian
}
func (l laporanCSV) Bentuk() string { return "csv" }
func (l laporanCSV) Render() string {
// koma di dalam item diganti titik koma agar tetap satu kolom.
items := make([]string, len(l.data.Items))
for i, v := range l.data.Items {
items[i] = strings.ReplaceAll(v, ",", ";")
}
return "pembeli,items\n" + l.data.Pembeli + ",\"" + strings.Join(items, ",") + "\""
}
// laporanJSON adalah tipe konkret kedua dengan API render sendiri.
type laporanJSON struct {
data DataPembelian
}
func (l laporanJSON) Bentuk() string { return "json" }
func (l laporanJSON) Render() string {
quoted := make([]string, len(l.data.Items))
for i, v := range l.data.Items {
quoted[i] = fmt.Sprintf("%q", v)
}
return fmt.Sprintf(`{"pembeli": %q, "items": [%s]}`, l.data.Pembeli, strings.Join(quoted, ", "))
}
// registry memetakan format ke fungsi pabrik.
// Package lain bisa mendaftarkan format baru tanpa menyentuh NewLaporan.
var registry = map[string]func(DataPembelian) Laporan{
"csv": func(d DataPembelian) Laporan { return laporanCSV{d} },
"json": func(d DataPembelian) Laporan { return laporanJSON{d} },
}
// NewLaporan adalah factory: menerima format, mengembalikan interface.
// Kesalahan format terdeteksi di sini, bukan di tangan pemanggil.
func NewLaporan(format string, data DataPembelian) (Laporan, error) {
pabrik, ok := registry[format]
if !ok {
return nil, fmt.Errorf("%q: %w", format, ErrFormat)
}
return pabrik(data), nil
}
// ErrFormat adalah sentinel error untuk format yang tidak dikenal.
var ErrFormat = errors.New("format laporan tidak dikenal")
func main() {
data := DataPembelian{Pembeli: "Budi", Items: []string{"Buku Go", "Pensil 2B"}}
for _, format := range []string{"csv", "json", "pdf"} {
lap, err := NewLaporan(format, data)
if err != nil {
fmt.Printf("[%s] gagal: %v\n", format, err)
continue
}
fmt.Printf("[%s]\n%s\n", lap.Bentuk(), lap.Render())
}
}Output yang Dihasilkan:
[csv]
pembeli,items
Budi,"Buku Go,Pensil 2B"
[json]
{"pembeli": "Budi", "items": ["Buku Go", "Pensil 2B"]}
[pdf] gagal: "pdf": format laporan tidak dikenalMari kita telaah baris demi baris program di atas:
- Baris 11–14 (
type Laporan interface): kontrak dua method yang menjadi satu-satunya hal yang dilihat pemanggil; tidak ada method lain yang bocor keluar. - Baris 22–36 (
laporanCSV): tipe konkret tanpa huruf kapital sehingga tidak bisa diimpor dari luar;Rendermengganti koma menjadi titik koma agar item tetap satu kolom. - Baris 38–51 (
laporanJSON): varian kedua dengan logika render sendiri; keduanya mengisiDataPembelianyang sama. - Baris 55–58 (
registry): map dari nama format ke fungsi pabrik; menambah format baru cukup menambah satu entri di sini. - Baris 62–68 (
NewLaporan): pintu masuk tunggal — mencari pabrik di registry, mengembalikan error untuk kunci yang tidak ada, lalu memanggil pabrik yang ditemukan. - Baris 71 (
ErrFormat): sentinel error yang dibungkusfmt.Errorf, sehingga pemanggil bisa memeriksanya denganerrors.Is. - Baris 73–84 (
main): tiga format dicoba berurutan; dua berhasil dirender, formatpdfgagal dan pesannya dicetak tanpa menghentikan program.
Factory Method lewat Closure #
Bentuk klasik factory method membiarkan subclass memilih produk. Go menggantinya dengan closure: fungsi yang mengembalikan fungsi pabrik, lengkap dengan data yang terikat di dalamnya.
// PabrikLaporan mengembalikan constructor yang sudah mengikat data.
func PabrikLaporan(data DataPembelian) func(string) (Laporan, error) {
return func(format string) (Laporan, error) {
return NewLaporan(format, data)
}
}Nilai kembalian PabrikLaporan adalah sebuah constructor siap pakai. Closure menyimpan data, sehingga fungsi hasil hanya butuh satu argumen format. Pola ini menggantikan abstract factory tanpa satu pun class tambahan — hierarki creator digantikan komposisi fungsi.
Factory memutuskan objek mana yang dibangun. Chapter berikutnya menghadapi masalah sepupunya: objek sudah ada, tetapi API-nya tidak cocok dengan yang diharapkan. Adapter Pattern akan menjembatani dua antarmuka yang tidak kompatibel.
Referensi #
- Effective Go
— konvensi constructor
NewXdan komposisi di Go. - Dokumentasi package errors
— sentinel error dan pemeriksaan
errors.Is.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
factory-demo.go
Unduh berkaspackage main
import (
"errors"
"fmt"
"strings"
)
// Laporan adalah interface yang diketahui pemanggil.
// Format konkret (csv/json) disembunyikan di balik constructor.
type Laporan interface {
Bentuk() string
Render() string
}
// DataPembelian adalah input mentah yang sama untuk semua format.
type DataPembelian struct {
Pembeli string
Items []string
}
// laporanCSV adalah tipe konkret yang tidak diekspor.
type laporanCSV struct {
data DataPembelian
}
func (l laporanCSV) Bentuk() string { return "csv" }
func (l laporanCSV) Render() string {
// koma di dalam item diganti titik koma agar tetap satu kolom.
items := make([]string, len(l.data.Items))
for i, v := range l.data.Items {
items[i] = strings.ReplaceAll(v, ",", ";")
}
return "pembeli,items\n" + l.data.Pembeli + ",\"" + strings.Join(items, ",") + "\""
}
// laporanJSON adalah tipe konkret kedua dengan API render sendiri.
type laporanJSON struct {
data DataPembelian
}
func (l laporanJSON) Bentuk() string { return "json" }
func (l laporanJSON) Render() string {
quoted := make([]string, len(l.data.Items))
for i, v := range l.data.Items {
quoted[i] = fmt.Sprintf("%q", v)
}
return fmt.Sprintf(`{"pembeli": %q, "items": [%s]}`, l.data.Pembeli, strings.Join(quoted, ", "))
}
// registry memetakan format ke fungsi pabrik.
// Package lain bisa mendaftarkan format baru tanpa menyentuh NewLaporan.
var registry = map[string]func(DataPembelian) Laporan{
"csv": func(d DataPembelian) Laporan { return laporanCSV{d} },
"json": func(d DataPembelian) Laporan { return laporanJSON{d} },
}
// NewLaporan adalah factory: menerima format, mengembalikan interface.
// Kesalahan format terdeteksi di sini, bukan di tangan pemanggil.
func NewLaporan(format string, data DataPembelian) (Laporan, error) {
pabrik, ok := registry[format]
if !ok {
return nil, fmt.Errorf("%q: %w", format, ErrFormat)
}
return pabrik(data), nil
}
// ErrFormat adalah sentinel error untuk format yang tidak dikenal.
var ErrFormat = errors.New("format laporan tidak dikenal")
func main() {
data := DataPembelian{Pembeli: "Budi", Items: []string{"Buku Go", "Pensil 2B"}}
for _, format := range []string{"csv", "json", "pdf"} {
lap, err := NewLaporan(format, data)
if err != nil {
fmt.Printf("[%s] gagal: %v\n", format, err)
continue
}
fmt.Printf("[%s]\n%s\n", lap.Bentuk(), lap.Render())
}
}