Pendahuluan
Halo rekan-rekan developer! Senang sekali bisa bertemu kembali di seri pembangunan Plugin System. Pada artikel sebelumnya, kita sudah membahas fondasi dasar mengenai apa itu Design Patterns dan mengapa kategori Creational Patterns sangat krusial dalam membangun sistem yang extensible. Kita belajar bahwa cara kita membuat objek akan menentukan seberapa fleksibel sistem kita di masa depan.
Pada kesempatan kali ini, kita akan masuk ke tahap implementasi teknis pertama kita. Kita akan mempelajari salah satu pola paling populer namun sering disalahpahami: Singleton Pattern. Kita tidak hanya akan belajar cara menulis kodenya, tetapi juga memahami kapan pola ini benar-benar dibutuhkan dan bagaimana menerapkannya dalam proyek Plugin System yang sedang kita bangun.
Tujuan utama kita dalam artikel ini adalah memahami bagaimana cara memastikan sebuah class hanya memiliki satu instance (objek) di seluruh aplikasi. Bayangkan jika Anda sedang membangun sistem pembayaran seperti Gojek atau Tokopedia; Anda tentu tidak ingin memiliki dua konfigurasi database yang berbeda yang berjalan secara bersamaan, bukan? Itulah inti dari apa yang akan kita pelastikkan hari ini.
Memahami Konsep Singleton
Singleton adalah design pattern yang menjamin bahwa sebuah class hanya memiliki satu instance tunggal selama aplikasi berjalan, dan menyediakan titik akses global ke instance tersebut.
Dalam pengembangan perangkat lunak, seringkali kita membutuhkan sebuah “sumber kebenaran tunggal” (single source of truth). Misalnya, sebuah objek ConfigurationManager yang menyimpan pengaturan API key untuk layanan seperti Midtrans atau Xendit. Jika kita membuat banyak objek konfigurasi, ada risiko satu bagian aplikasi menggunakan API key lama, sementara bagian lain menggunakan yang baru. Hal ini bisa menyebabkan error transaksi yang sangat sulit dilacak.
Mari kita lihat implementasi dasar Singleton menggunakan Python. Kita akan menggunakan metode __new__ untuk mengontrol pembuatan objek.
class ConfigurationManager:
_instance = None
def __new__(cls):
# Jika _instance belum ada (None), buat objek baru
if cls._instance is None:
print("Membuat instance ConfigurationManager baru...")
cls._instance = super(ConfigurationManager, cls).__new__(cls)
# Inisialisasi data default
cls._instance.api_key = "KEY-INDONESIA-123"
cls._instance.environment = "production"
return cls._instance
# Mari kita uji implementasinya
config_budi = ConfigurationManager()
config_siti = ConfigurationManager()
print(f"API Key Budi: {config_budi.api_key}")
print(f"API Key Siti: {config_siti.api_key}")
# Membuktikan bahwa keduanya adalah objek yang sama
if config_budi is config_siti:
print("Berhasil: Budi dan Siti menggunakan instance yang sama!")
else:
print("Gagal: Instance berbeda ditemukan.")
Pada kode di atas, meskipun Budi dan Siti mencoba membuat objek ConfigurationManager baru, Python akan mengecek apakah _instance sudah ada. Jika sudah ada, Python hanya akan mengembalikan referensi ke objek yang sudah ada sebelumnya.
Menangani Tantangan Multi-threading
Sebagai developer intermediate, kita tidak boleh hanya memikirkan skenario satu pengguna. Di dunia nyata, aplikasi seperti server API yang menangani ribuan transaksi per detik menggunakan multi-threading. Di sinilah masalah besar bisa muncul.
Ada kondisi yang disebut Race Condition. Bayangkan dua thread (misalnya satu thread untuk proses pembayaran BCA dan satu thread untuk proses Mandiri) masuk ke dalam blok if cls._instance is None pada saat yang bersama melakukanan. Keduanya akan melihat bahwa _instance masih None, dan keduanya akan mengeksekusi pembuatan objek baru. Hasilnya? Kita memiliki dua instance Singleton, yang merusak prinsip utama pola ini.
Untuk mengatasinya, kita perlu menggunakan Locking mechanism.
import threading
class ThreadSafeSingleton:
_instance = None
_lock = threading.Lock() # Objek lock untuk sinkronisasi
def __new__(cls):
# Teknik Double-Checked Locking
if cls._instance is None:
with cls._lock:
# Cek ulang setelah mendapatkan lock
if cls._instance is None:
print("Membuat instance yang aman dari thread...")
cls._instance = super(ThreadSafeSingleton, cls).__new__(cls)
cls._instance.db_connection = "Connected to PostgreSQL"
return cls._instance
# Simulasi banyak thread mencoba membuat instance secara bersamaan
def create_instance_task(name):
instance = ThreadSafeSingleton()
print(f"Thread {name} menggunakan instance: {id(instance)}")
threads = []
for i in range(5):
t = threading.Thread(target=create_instance_task, args=(f"Worker-{i}",))
threads.append(t)
t.start()
for t in threads:
t.join()
Dalam contoh ini, kita menggunakan threading.Lock(). Dengan Double-Checked Locking, kita hanya melakukan pengecekan kunci (lock) jika instance memang belum ada. Ini penting agar performa aplikasi tetap tinggi karena proses locking cukup mahal secara komputasi.
Singleton vs Global Variable
Banyak developer pemula bertanya: “Kenapa tidak pakai variabel global saja? Bukannya sama-sama bisa diakses dari mana saja?”
Ini adalah pertanyaan bagus. Meskipun terlihat mirip, Singleton menawarkan keunggulan yang tidak dimiliki variabel global:
- Lazy Initialization: Singleton hanya dibuat saat benar-benar dibutuhkan (on-demand). Variabel global biasanya dibuat saat aplikasi pertama kali dijalast (eagerly), yang bisa memperlambat waktu startup jika objeknya berat.
- Encapsulation: Singleton menyembunyikan logika internalnya. Anda bisa menambahkan validasi atau proteksi di dalam metode
__new__atau metode lainnya. - Control: Anda bisa mengontrol bagaimana objek tersebut diinisialisasi dan bagaimana ia merespons perubahan status, sesuatu yang sulit dilakukan pada variabel global biasa.
Implementasi dalam Proyek: Plugin Registry
Sekarang, mari kita terapkan konsep ini ke dalam proyek utama kita: Plugin System.
Dalam sistem plugin, kita membutuhkan sebuah pusat kendali yang mencatat semua plugin yang telah dimuat. Jika setiap plugin membuat “Registry” sendiri-sendali, maka plugin A tidak akan pernah tahu bahwa plugin B telah terpasang. Oleh karena itu, kita membutuhkan PluginRegistry yang bersifat Singleton.
class PluginRegistry:
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super(PluginRegistry, cls).__new__(cls)
cls._instance.plugins = {}
print("--- Plugin Registry Inisialisasi ---")
return cls._instance
def register_plugin(self, name, plugin_obj):
self.plugins[name] = plugin_obj
print(f"Plugin '{name}' berhasil didaftarkan.")
def get_plugin(self, name):
return self.plugins.get(name, None)
def list_all_plugins(self):
return list(self.set(self.plugins.keys()))
# --- Simulasi Penggunaan dalam Plugin System ---
class PaymentPluginBCA:
def execute(self):
return "Memproses pembayaran via BCA Virtual Account. Biaya: Rp 2.500"
class PromoPluginLebaran:
def execute(self):
return "Diskon Lebaran 15% telah diterapkan!"
# Inisialisasi Registry (Singleton)
registry = PluginRegistry()
# Plugin A (BCA) mendaftarkan dirinya
plugin_bca = PaymentPluginBCA()
registry.register_plugin("pembayaran_bca", plugin_bca)
# Plugin B (Promo) mendaftarkan dirinya
plugin_promo = PromoPluginLebaran()
registry.register_plugin("promo_lebaran", plugin_promo)
# Di bagian lain aplikasi (misalnya di Controller), kita akses registry yang sama
another_registry_reference = PluginRegistry()
print(f"Daftar plugin yang aktif: {list(another_registry_reference.plugins.keys())}")
# Menjalankan plugin
plugin_to_run = another_registry_reference.get_plugin("pembayaran_bca")
if plugin_to_run:
print(f"Output: {plugin_to_run.execute()}")
Dengan menggunakan Singleton, PluginRegistry bertindak sebagai jembatan tunggal. Tidak peduli di bagian mana dari kode Anda (apob di modul pembayaran, modul promo, atau modul pengiriman), semua akan merujuk ke satu sumber kebenaran (single source of truth) yang sama.
Kesimpulan
Singleton adalah pola yang sangat kuat namun harus digunakan dengan bijak. Jika digunakan secara berlebihan, ia bisa menjadi “global state” yang membuat unit testing menjadi sulit karena sulit untuk mengisolasi state antar test. Namun, untuk kasus seperti Registry, Configuration Manager, atau Database Connection Pool, Singleton adalah pilihan yang sangat tepat.
Dalam artikel berikutnya, kita akan membahas tentang Factory Pattern, yang akan membantu kita membuat objek secara lebih dinamis tanpa harus bergantung pada kelas konkretnya. Sampai jumpa!