cv.cm

Pemrograman · Pola desain

Pola Singleton dijelaskan dengan contoh

5 menit baca

Jawaban singkat

Singleton adalah pola desain creational yang memastikan sebuah kelas hanya punya satu instance dan menyediakan satu titik akses global ke instance itu. Di TypeScript, Anda mengimplementasikannya dengan konstruktor private dan method static yang membuat instance saat pertama kali dipakai lalu mengembalikan objek yang sama sesudahnya. Pola ini berguna untuk sumber daya yang benar-benar unik, seperti objek konfigurasi atau connection pool, tetapi bertindak seperti state global, sehingga banyak tim lebih memilih meneruskan satu instance bersama secara eksplisit.

Tujuan

Memastikan sebuah kelas hanya punya satu instance, dan menyediakan satu titik akses global ke instance itu. Pola Singleton adalah salah satu dari lima pola creational dalam buku Gang of Four (GoF).

Masalah yang diselesaikan

Beberapa objek harus ada tepat satu kali dalam program. Bayangkan konfigurasi aplikasi yang dimuat dari environment variable, cache dalam proses, atau pool koneksi database. Jika setiap modul memanggil new Config(), masing-masing akan membaca environment lagi, cache terpecah menjadi banyak salinan, dan batas pool menjadi tidak berarti.

Anda butuh dua hal: jaminan bahwa instance kedua tidak bisa terbuat secara tidak sengaja, dan cara mudah bagi kode mana pun untuk menjangkau satu instance itu.

Struktur dan peserta

PesertaTanggung jawab
Kelas SingletonMenyembunyikan konstruktornya agar kode lain tidak bisa memanggil new secara langsung.
Field instance staticMenyimpan satu-satunya instance setelah dibuat.
Method akses static (sering bernama getInstance)Membuat instance pada panggilan pertama (lazy initialization) dan mengembalikan objek yang sama pada setiap panggilan.
KlienMendapatkan instance hanya lewat method akses.

Implementasi TypeScript

Konstruktor private membuat new AppConfig() menjadi error saat kompilasi di luar kelas. Method static membuat objeknya secara lazy:

class AppConfig {
  private static instance: AppConfig | undefined;
  readonly settings: Record<string, string>;
  private constructor() {
    console.log("loading config once");
    this.settings = { mode: "production", region: "eu" };
  }
  static getInstance(): AppConfig {
    if (!AppConfig.instance) {
      AppConfig.instance = new AppConfig();
    }
    return AppConfig.instance;
  }
}
const a = AppConfig.getInstance(); // loading config once
const b = AppConfig.getInstance();
console.log(a === b);          // true
console.log(a.settings.mode);  // production
// new AppConfig(); // error: constructor is private

Di JavaScript dan TypeScript, modul ES dievaluasi sekali per program, jadi mengekspor instance dari sebuah modul adalah singleton paling sederhana. Setiap import menerima objek yang sama:

// logger.ts
class Logger {
  private lines: string[] = [];
  log(msg: string): void { this.lines.push(msg); console.log(msg); }
  count(): number { return this.lines.length; }
}
export const logger = new Logger();
// elsewhere: import { logger } from "./logger";
logger.log("started");      // started
console.log(logger.count()); // 1

Inisialisasi async

Jika pembuatan instance bersifat asinkron, seperti membuka koneksi database, dua pemanggil bisa sama-sama melihat "belum ada instance" dan memulai dua koneksi sebelum yang pertama selesai. Cache promise-nya, bukan hasilnya:

class Database {
  private static ready: Promise<Database> | undefined;
  private constructor(readonly url: string) {}
  static get(): Promise<Database> {
    if (!Database.ready) {
      Database.ready = (async () => {
        await new Promise((r) => setTimeout(r, 10)); // simulate connecting
        console.log("connected once");
        return new Database("db://main");
      })();
    }
    return Database.ready;
  }
}
const [x, y] = await Promise.all([Database.get(), Database.get()]);
console.log(x === y); // true

Pada bahasa multi-thread seperti Java dan C#, pembuatan lazy membutuhkan sinkronisasi karena alasan yang sama. Solusi umumnya adalah inisialisasi eager di field static, kelas lazy holder, atau, di Java, enum dengan satu elemen.

Kapan memakainya, dan kapan tidak

Gunakan pola Singleton ketika:

  • Harus ada tepat satu instance demi kebenaran, bukan sekadar kenyamanan (satu perangkat keras, satu registry tingkat proses).
  • Pembuatannya mahal dan objeknya stateless atau thread-safe setelah dibuat.

Hindari, atau batasi, ketika:

  • Anda terutama menginginkan akses global yang nyaman. Singleton adalah state global yang bisa diubah dengan nama yang lebih bagus: kode mana pun dapat mengubahnya, dan dependensinya tersembunyi dari signature fungsi.
  • Anda butuh pengujian. Tes yang berbagi singleton saling memengaruhi, dan sulit mengganti instance asli dengan tiruan.
  • Aturan "satu instance" mungkin berubah nanti, misalnya ketika Anda butuh satu cache per tenant.

Alternatif yang umum adalah dependency injection: buat satu instance di awal program (composition root) dan teruskan ke kelas yang membutuhkannya. Anda tetap punya satu instance, tetapi dependensinya terlihat dan dapat diganti.

class Cache {
  private data = new Map<string, string>();
  get(key: string): string | undefined { return this.data.get(key); }
  set(key: string, value: string): void { this.data.set(key, value); }
}
class UserService {
  constructor(private cache: Cache) {} // dependency is explicit
  remember(id: string, name: string): void { this.cache.set(id, name); }
}
const sharedCache = new Cache(); // the single instance, created once
const users = new UserService(sharedCache);
users.remember("u1", "Ada");
console.log(sharedCache.get("u1")); // Ada

Pemakaian di dunia nyata

  • Java: Runtime.getRuntime() mengembalikan satu objek Runtime untuk aplikasi Java yang berjalan.
  • Node.js men-cache modul setelah pemuatan pertama, sehingga objek yang diekspor sebuah modul dipakai bersama oleh setiap file yang me-require atau meng-import-nya.
  • Browser menyediakan objek global unik seperti window dan document untuk setiap halaman.
  • Framework dengan dependency injection, seperti Angular dan Spring, membuat service "singleton-scoped" secara default: satu instance per injector atau container, tanpa bagian akses static dari pola ini.

Pola terkait

  • Objek pola Abstract Factory, pola Builder, dan pola Prototype sering diimplementasikan sebagai singleton, karena satu factory sudah cukup.
  • Objek pola Facade sering berupa instance tunggal, karena satu pintu masuk ke subsistem biasanya sudah cukup.
  • Pola Flyweight juga berbagi objek, tetapi mengelola banyak instance bersama yang dikunci berdasarkan state-nya, sedangkan pola Singleton hanya punya satu.
  • Dependency injection, yang bukan pola GoF, adalah pengganti pola Singleton yang paling umum dalam kode modern.

FAQ

Apakah Singleton anti-pattern?

Sering disebut demikian, karena memperkenalkan state global tersembunyi dan mempersulit pengujian. Memiliki satu instance tidak masalah; masalahnya ada pada akses globalnya. Membuat satu instance dan meneruskannya secara eksplisit menghindari sebagian besar masalah.

Bagaimana membuat singleton di TypeScript?

Gunakan konstruktor private, field static private untuk instance, dan method static yang membuat instance pada panggilan pertama. Mengekspor satu instance dari sebuah modul adalah alternatif yang lebih sederhana.

Apakah modul JavaScript itu singleton?

Secara efektif ya. Modul dievaluasi sekali dan ekspornya dipakai bersama oleh setiap pengimpor dalam program yang sama, kecuali kode yang sama dimuat dari dua path berbeda.

Bagaimana menguji kode yang memakai singleton?

Buat singleton mengimplementasikan interface dan teruskan ke kode yang diuji, sehingga tes dapat memberikan tiruan. Jika desainnya tidak bisa diubah, tambahkan method reset yang hanya dipakai dalam pengujian.

Apa beda singleton dengan kelas static?

Singleton adalah objek sungguhan: dapat mengimplementasikan interface, diteruskan sebagai parameter, dan dibuat secara lazy. Kelas yang hanya berisi anggota static tidak dapat digantikan atau diteruskan sebagai objek.

Semua tutorial pemrograman