Singleton Pattern memastikan sebuah class hanya punya satu instance, dan menyediakan satu titik akses global ke instance itu, jadi bagian mana pun dari aplikasi memakai object yang sama. Bayangin mesin nomor antrean di bank. Semua nasabah mengambil nomor dari mesin yang sama, jadi nomornya urut dan gak ada yang kembar. Kalau bank memasang dua mesin yang sama-sama mulai dari 001, dua orang bisa memegang nomor 001 dan dipanggil bersamaan.
🧪 Tebakan cepat: dua test memakai counter resi singleton yang sama. Test pertama memesan satu resi. Test kedua memesan satu resi lalu mengecek hasilnya
KCL-0001. Test kedua lulus atau gagal? Tebak dulu, jawabannya ada di bagian "Harganya: state global yang tersembunyi".
Masalahnya: dua counter, dua KCL-0001
Kurir Kancil punya dua pintu masuk paket: kasir di loket dan aplikasi di HP. Masing-masing membuat counter resinya sendiri:
class ResiCounter {
constructor() {
this.count = 0
}
next() {
this.count = this.count + 1
return "KCL-" + String(this.count).padStart(4, "0")
}
}
const counterKasir = new ResiCounter() // di kasir.js
const counterAplikasi = new ResiCounter() // di aplikasi.js
console.log(counterKasir.next()) // KCL-0001
console.log(counterAplikasi.next()) // KCL-0001 lagi!
Tiap new membuat object baru dengan count sendiri yang mulai dari 0. Dua mesin antrean, dua nomor 001. Counter resi adalah sesuatu yang memang harus tepat satu.
Singleton versi class: getInstance
class ResiCounter {
static instance = null // milik class, bukan milik tiap object
static getInstance() {
if (!ResiCounter.instance) {
ResiCounter.instance = new ResiCounter() // dibuat sekali, saat pertama diminta
}
return ResiCounter.instance
}
constructor() {
this.count = 0
}
next() {
this.count = this.count + 1
return "KCL-" + String(this.count).padStart(4, "0")
}
}
const a = ResiCounter.getInstance() // di kasir.js
const b = ResiCounter.getInstance() // di aplikasi.js
console.log(a === b) // true
console.log(a.next()) // KCL-0001
console.log(b.next()) // KCL-0002
static instanceadalah properti milik classResiCountersendiri. Semua yang memanggilResiCounter.getInstance()membaca properti yang sama.getInstancemembuat object hanya kalauinstancemasihnull. Panggilan berikutnya mengembalikan object yang sudah ada.a === bbernilaitruekarena===pada object membandingkan apakah keduanya object yang sama persis, bukan isinya.- Itulah dua bagian Singleton: satu instance, dan satu titik akses global (
getInstance, yang bisa dipanggil dari file mana pun).
Di JavaScript: module sudah cukup
Sebuah ES module dijalankan sekali, dan semua file yang meng-import-nya mendapat object yang sama. Jadi object yang di-export dari module sudah singleton, tanpa class dan tanpa getInstance:
// resiCounter.js
let count = 0
export const resiCounter = {
next() {
count = count + 1
return "KCL-" + String(count).padStart(4, "0")
},
}
// kasir.js dan aplikasi.js sama-sama menulis:
import { resiCounter } from "./resiCounter.js"
Kasir memanggil resiCounter.next() dan dapat KCL-0001, aplikasi memanggilnya dan dapat KCL-0002. Cara ini juga gak bisa di-new. Di banyak codebase JavaScript, singleton memang ditulis begini.
Harganya: state global yang tersembunyi
Satu object yang dipakai bersama juga berarti satu state yang dipakai bersama, termasuk oleh test:
function test(name, fn) {
try {
fn()
console.log("lulus: " + name)
} catch (e) {
console.log("gagal: " + name + ", " + e.message)
}
}
test("pesan resi pertama", () => {
const r = resiCounter.next()
if (r !== "KCL-0001") throw new Error("dapat " + r)
})
test("resi pertama hari ini KCL-0001", () => {
const r = resiCounter.next()
if (r !== "KCL-0001") throw new Error("dapat " + r)
})
// lulus: pesan resi pertama
// gagal: resi pertama hari ini KCL-0001, dapat KCL-0002
Test kedua gagal, padahal kodenya sama persis dengan test pertama. Itu jawaban tebakannya. count sudah 1 sisa test sebelumnya. Dijalankan sendirian, test kedua lulus. Dijalankan bareng, gagal. Urutan test jadi menentukan hasil, dan itu susah dilacak.
Masalah yang sama muncul saat debugging: siapa pun di file mana pun bisa mengubah state-nya, dan function yang memakai resiCounter di dalamnya gak menunjukkan itu di parameternya. Jalan keluarnya: oper counter sebagai parameter (function createParcel(counter, kg)), jadi tiap test bisa membuat counter baru, atau sediakan cara mereset sebelum tiap test. Lesson Dependency Injection nanti membahas cara pertama lebih dalam.
Kapan pakai, kapan jangan
- Pakai untuk yang memang harus tepat satu: counter nomor resi, config yang dibaca sekali saat aplikasi mulai, logger, pool koneksi database.
- Jangan untuk data per pengguna di server. Di Node.js, module dijalankan sekali untuk seluruh server, jadi keranjang belanja yang disimpan di object module dipakai bersama oleh semua pengguna.
- Jangan cuma karena "biar gampang diakses dari mana saja". Itu variabel global dengan nama lain.
⚠️ Jebakan yang sering ketemu
newdi tempat lain. VersigetInstancegak mencegahnew ResiCounter(). Satu baris itu melahirkan instance kedua dan nomor resi kembar lagi.- Test yang lulus sendirian tapi gagal bareng. Tanda ada singleton yang membawa state dari test sebelumnya. Reset sebelum tiap test, atau oper object-nya sebagai parameter.
- Dua salinan module. Satu module dijalankan sekali per salinan. Kalau package yang sama terpasang dalam dua versi, ada dua instance.
TL;DR: Singleton memastikan sebuah class hanya punya satu instance dan menyediakan akses global ke instance itu. Versi class-nya memakai static instance dan getInstance(), yang membuat object sekali lalu mengembalikan object yang sama (a === b bernilai true). Di JavaScript, object yang di-export dari ES module sudah singleton karena module dijalankan sekali. Kelemahan utamanya: state global yang tersembunyi, yang membuat testing dan debugging lebih sulit, misalnya test kedua dapat KCL-0002. Pakai hanya untuk yang memang harus satu (counter resi, config, logger), dan jangan untuk data per pengguna di server.