Singleton Pattern
Pada article tentang NestJS Framework sempat menyinggung bahwa secara default Decorator Injectable akan mendeklarasikan instance sebagai Singleton Object. Tapi apa itu Singleton Object. Mengapa digunakan sebagai default dari decorator Injectable dari NestJS dan resiko apa yang mungkin terjadi.
Ketika developer membuat aplikasi, perlu dengan hati-hati menghitung beban resource dari aplikasi yang dibangunnya, salah satunya setiap kali membuat object, memerlukan sebuah object yang hanya diinialisasi sekali dengan tujuan agar tidak memakan banyak resource dan bisa dipakai secara global selama aplikasi berjalan. Maka developer bisa mengandalkan singleton pattern untuk hal tersebut.
Pada umumnya, Singleton Object biasa digunakan untuk beberapa hal seperti:
Configuration managers
Database Pool Connection
Logging service
Cache Mechanisms
Thread Pools
Sebagai contoh kasus sederhana dan naive seperti misal untuk logging service kecil ini, dimana seandainya destination dari logging adalah sebuah file dengan nama service_<runtime_timestamp>.log dan variable runtime_filename di-store di object.
import { appendFileSync } from 'fs';
/// log module
class LogModule {
private logFile: string = '';
constructor() {
const timestamp = Date.now();
this.logFile = `service_${timestamp}`;
}
static info(msg: string) {
const inst = new LogModule
return inst.log('info', msg)
}
log(type: string, msg: string) {
appendFileSync(this.logFile, msg);
}
}
// Service B
class AService {
static run() {
LogModule.info('Message service A')
}
}
// Service B
class BService {
static run() {
LogModule.info('Message service B')
}
}
// Main
AService.run()
BService.run()
Jika code tersebut dijalankan, yang terjadi adalah akan tebuat lebih dari satu file log dengan timestamp filename yang berbeda, mengapa? karena setiap kali module service dijalankan, maka akan membuat object log module baru juga yang mana pada constructor pada LogModule akan mengenerate fileLog dengan timestamp yang baru. Akibatnya dengan terbuatanya Log Module yang berbeda akan berakibat terbuatnya file log yang berbeda. Dari masalah kasus LogModule tersebut, maka akan cocok menggunakan Singleton Creational Pattern.
Kita coba implementasikan Singleton Pattern pada LogModule seperti berikut
class LogModule {
static instance: LogModule | null;
private logFile: string = '';
constructor() {
const timestamp = Date.now();
this.logFile = `service_${timestamp}`;
}
// static info(msg: string) {
// const inst = new LogModule
// return inst.log('info', msg)
// }
static init() {
if (!LogModule.instance) {
const inst = new LogModule;
LogModule.instance = inst;
}
return LogModule.instance;
}
// log(type: string, msg: string) {
// appendFileSync(this.logFile, msg);
// }
info(msg: string) {
appendFileSync(this.logFile, `${msg}\n`);
}
}
Seperti terlihat pada snippet code di atas, kita mengganti static info method dengan static init method. Lalu pada content method tersebut kita menambahkan conditional checker jika property static instance bernilai null atau belum terisi nilai apapun, maka buat instance baru dan simpan pada property static instance lalu return nilai tersebut.
if (!LogModule.instance) {
const inst = new LogModule;
LogModule.instance = inst;
}
Sehingga ketika kita mengeksekusi method LogModule.init, kita tidak membuat instance baru dan tidak menjalankan constructor pada LogModule. Kita akan mendapatkan reference dari instance yang sudah dibuat sebelumnya.
Code setelah mengimplementasikan Singleton Pattern akan seperti ini.
import { appendFileSync } from 'fs';
class LogModule {
static instance: LogModule | null;
private logFile: string = '';
constructor() {
const timestamp = Date.now();
this.logFile = `service_${timestamp}`;
}
static init() {
if (!LogModule.instance) {
const inst = new LogModule;
LogModule.instance = inst;
}
return LogModule.instance;
}
info(msg: string) {
appendFileSync(this.logFile, `${msg}\n`);
}
}
// Service B
class AService {
static run() {
LogModule.init().info('Message service A')
}
}
// Service B
class BService {
static run() {
LogModule.init().info('Message service B')
}
}
// Main
AService.run()
BService.run()
Dengan begitu, sebanyak apapun kita menjalankan method init pada LogModule tersebut, LogModule tidak akan membuat object baru. Hal tersebut akan meringankan beban penggunaan memory pada aplikasi.
Beberapa keunggulan dari Singleton Creational Pattern
Bisa dipastikan bahwa class atau module tersebut hanya memiliki satu instance.
Bisa akses secara global dimodule manapun, dengan syarat tidak memiliki mutable property pada class atau module tersebut
Resource effisient
Selain itu juga ada kekurangannya
Bisa ada hidden dependency
Bisa jadi bermasalah pada concurency jika tidak hati-hati
Unittest atua automatic testing akan cukup sulit, tapi bisa diatasi dengan dependency injection
references


