Builder Pattern merakit object yang punya banyak bagian opsional selangkah demi selangkah, lewat method yang namanya menyebut bagian itu, lalu build() di akhir memeriksa rakitannya dan menyerahkan hasilnya. Bayangin formulir kirim paket di loket. Kamu cuma mencentang kotak yang perlu: asuransi, packing kayu, COD, catatan. Tiap kotak ada labelnya. Petugas memeriksa formulirnya sekali sebelum stiker dicetak. Bandingkan dengan formulir berisi delapan kotak kosong tanpa label, yang artinya cuma ketahuan dari urutannya.
🧪 Tebakan cepat: penulis kode ini ingin paketnya pakai packing kayu:
new Shipment("KCL-0001", "kargo", 12, true, false, 150000, 0, ["Isi kaca"]). Paketnya dapat packing kayu atau gak? Tebak dulu, jawabannya ada di bagian "Masalahnya: delapan argumen berurutan".
Masalahnya: delapan argumen berurutan
Kiriman Kurir Kancil punya tiga bagian wajib (resi, layanan, berat) dan lima bagian opsional:
class Shipment {
constructor(resi, service, kg, asuransi, packingKayu, cod, nilaiBarang, catatan) {
Object.assign(this, { resi, service, kg, asuransi, packingKayu, cod, nilaiBarang, catatan })
}
}
const s = new Shipment("KCL-0001", "kargo", 12, true, false, 150000, 0, ["Isi kaca"])
console.log(s.asuransi, s.packingKayu) // true false
Argumen keempat adalah asuransi, kelima packingKayu. Penulisnya menukar dua true/false, jadi paketnya dapat asuransi dan gak dapat packing kayu. Itu jawaban tebakannya. Gak ada error: dua boolean yang tertukar tetap boolean.
- Arti tiap argumen cuma ketahuan dari posisinya.
true, falsedi tempat pemanggil gak bilang apa-apa. - Bagian opsional yang gak dipakai tetap harus diisi
falseatau0supaya bisa sampai ke argumen sesudahnya. - Bagian kesembilan berarti mengubah tiap pemanggilan
new Shipment(...).
Langkah pertama di JavaScript: satu object berisi opsi
JavaScript punya obat bawaan: kirim satu object dan ambil isinya dengan destructuring, lengkap dengan nilai bawaan.
function createShipment({ resi, service = "reguler", kg = 1, asuransi = false, nilaiBarang = 0, packingKayu = false, cod = 0, catatan = [] }) {
return { resi, service, kg, asuransi, nilaiBarang, packingKayu, cod, catatan }
}
const s = createShipment({ resi: "KCL-0001", service: "kargo", kg: 12, packingKayu: true, cod: 150000 })
console.log(s.asuransi, s.packingKayu) // false true
- Tiap nilai bernama, urutannya bebas, dan yang gak ditulis memakai nilai bawaan (
= false,= 0). - Kalau semua bagian sudah diketahui di satu tempat, ini sudah cukup. Banyak kode JavaScript berhenti di sini, dan itu wajar.
Builder: dirakit selangkah demi selangkah
Builder baru berguna saat object-nya diisi bertahap (halaman checkout mengisi layanan dan berat, langkah berikutnya menambah asuransi, langkah terakhir COD), ada bagian yang bisa ditambah berkali-kali (catatan), dan ada aturan gabungan yang dicek sekali di akhir.
class ShipmentBuilder {
constructor(resi) {
this.data = { resi, service: "reguler", kg: 1, asuransi: false, nilaiBarang: 0, packingKayu: false, cod: 0, catatan: [] }
}
service(name) { this.data.service = name; return this }
kg(n) { this.data.kg = n; return this }
asuransi(nilaiBarang) { this.data.asuransi = true; this.data.nilaiBarang = nilaiBarang; return this }
packingKayu() { this.data.packingKayu = true; return this }
cod(nominal) { this.data.cod = nominal; return this }
catatan(text) { this.data.catatan.push(text); return this }
build() {
if (this.data.packingKayu && this.data.service !== "kargo") {
throw new Error("Packing kayu cuma untuk kargo")
}
return { ...this.data, catatan: [...this.data.catatan] }
}
}
const shipment = new ShipmentBuilder("KCL-0001")
.service("kargo")
.kg(12)
.packingKayu()
.cod(150000)
.catatan("Isi kaca")
.catatan("Jangan dibanting")
.build()
console.log(shipment.packingKayu, shipment.asuransi) // true false
console.log(shipment.catatan.join(", ")) // Isi kaca, Jangan dibanting
constructormengisi nilai bawaan. Bagian yang gak dipanggil tetap di nilai bawaannya.- Tiap method diakhiri
return this.thisadalah builder itu sendiri, jadi.service("kargo")mengembalikan builder yang sama, dan.kg(12)bisa langsung dipanggil di belakangnya. Rantai seperti ini disebut fluent API: pemanggilannya terbaca seperti kalimat. catatan()boleh dipanggil berkali-kali. Tiap panggilan menambah satu ke array.build()adalah satu-satunya tempat aturan dicek.new ShipmentBuilder("KCL-0002").service("kilat").packingKayu().build()berhenti denganError: Packing kayu cuma untuk kargo, sebelum kiriman setengah jadi masuk ke sistem.build()mengembalikan salinan ({ ...this.data }dan array baru), jadi perubahan pada builder sesudahnya gak ikut mengubah kiriman yang sudah jadi.
Jadi untuk object dengan banyak bagian opsional, builder memberi dua hal: tempat pemanggilannya terbaca (tiap bagian disebut namanya), dan pemeriksaannya terkumpul di build().
Contoh kecil yang sama prinsipnya: method add(x) yang mem-push x lalu return this, dipanggil .add("a").add("b").add("c"), lalu build() yang menjalankan join("-"), menghasilkan a-b-c.
Latihan: URLBuilder
Latihan nanti memakai bentuk yang sama untuk alamat URL. Starter sudah berisi class URLBuilder dengan method kosong protocol(p), host(h), path(p), query(k, v), dan build():
- Protokol bawaannya
"https", diisi diconstructor. query(k, v)boleh dipanggil berkali-kali. Simpan pasangannya di array, misalnyathis._query.push([k, v]).build()menyusunprotocol + "://" + host + path, lalu"?"dan pasangank=vyang digabung dengan"&", tapi tanda tanyanya cuma kalau ada query.
Hasil yang diharapkan: https://api.example.com/users, https://example.com/search?q=laravel&page=2, dan http://localhost/.
Kapan gak perlu builder
- Object dengan dua bagian.
{ lat: -6.2, lng: 106.8 }sudah jelas.new PointBuilder().lat(-6.2).lng(106.8).build()menambah satu class dan tiga pemanggilan untuk hal yang sama. - Semua bagian diketahui di satu tempat. Pakai satu object berisi opsi.
- Gak ada aturan gabungan dan gak ada bagian berulang. Builder cuma jadi kode tambahan tanpa manfaat.
⚠️ Jebakan yang sering ketemu
- Lupa
return this. Method itu mengembalikanundefined, dan pemanggilan berikutnya di rantai berhenti denganTypeError: Cannot read properties of undefined (reading 'catatan'). - Lupa
.build(). Yang kamu pegang masih builder-nya, bukan kiriman.shipment.packingKayujadi method, danif (shipment.packingKayu)selalu dianggap benar. build()mengembalikanthis.datalangsung. Kiriman yang sudah jadi masih berbagi arraycatatandengan builder. Kembalikan salinan.- Tanda tanya yang selalu ditulis. URL tanpa query jadi
https://toko.id/cart?. Tambahkan"?"hanya kalau array query-nya gak kosong.
TL;DR: Constructor dengan banyak argumen berurutan gampang tertukar tanpa error. Di JavaScript, langkah pertama adalah satu object berisi opsi dengan nilai bawaan. Builder dipakai saat object dirakit bertahap, ada bagian yang berulang, atau ada aturan yang dicek di akhir: constructor mengisi nilai bawaan, tiap method mengisi satu bagian lalu return this supaya bisa dirantai (fluent API), dan build() memeriksa semuanya di satu tempat lalu mengembalikan salinan. Untuk object dua bagian, cukup object literal.