Back to blog

Singleton Pattern: Một Instance Duy Nhất Thống Trị Tất Cả

oopcreational-patternssingletondesign-patternstypescriptpythonjava
Singleton Pattern: Một Instance Duy Nhất Thống Trị Tất Cả

Giới thiệu

Bạn đã học các nguyên tắc SOLID—hướng dẫn cách cấu trúc class và mối quan hệ giữa chúng. Bây giờ là lúc học design patterns: các giải pháp đã được chứng minh, có thể tái sử dụng cho những vấn đề xuất hiện lặp đi lặp lại trong thiết kế phần mềm.

Chúng ta bắt đầu với creational pattern đơn giản nhất và cũng gây tranh cãi nhất: Singleton.

Singleton pattern đảm bảo một class có đúng một instance và cung cấp điểm truy cập global đến nó. Nghe đơn giản, đúng không? Nhưng ít pattern nào gây tranh luận nhiều hơn. Dùng đúng cách, nó giải quyết vấn đề thực tế. Dùng bất cẩn, nó trở thành nguồn gốc của tight coupling, hidden dependencies và code không thể test.

Bạn Sẽ Học Được

✅ Hiểu vấn đề mà Singleton pattern giải quyết
✅ Triển khai Singleton trong TypeScript, Python và Java
✅ So sánh eager vs lazy vs thread-safe initialization
✅ Dùng ES6 module pattern như giải pháp TypeScript hiện đại
✅ Hiểu tại sao Singleton bị gọi là anti-pattern bởi một số người
✅ Vượt qua thách thức testing với dependency injection

Điều kiện tiên quyết


Vấn đề: Tại sao cần Singleton?

Một số tài nguyên trong ứng dụng chỉ nên tồn tại một:

Tài nguyênTại sao chỉ một?
LoggerTất cả phần của app nên ghi vào cùng một log
ConfigurationSettings được load một lần, dùng ở mọi nơi
Database connection poolMột pool chia sẻ cho tất cả services
CacheMột cache chung để tránh trùng lặp dữ liệu
Thread poolMột pool để quản lý concurrency

Không có Singleton, bạn có nguy cơ:

  1. Trùng lặp instance — Hai logger ghi vào file khác nhau, hai cache với dữ liệu cũ
  2. Lãng phí tài nguyên — Mỗi instance tiêu tốn bộ nhớ và có thể là file handles hoặc connections
  3. Trạng thái không nhất quán — Nhiều config objects với giá trị khác nhau

Singleton pattern đảm bảo rằng một class chỉ có một instance duy nhất và cung cấp điểm truy cập global đến instance đó.


Cấu trúc Singleton

Các thành phần chính:

  1. Private constructor — Ngăn bất kỳ ai gọi new Singleton()
  2. Static instance field — Lưu trữ instance duy nhất
  3. Static getInstance() method — Tạo instance ở lần gọi đầu tiên, trả về instance hiện có ở các lần gọi sau

Triển khai: Lazy Initialization

Cách tiếp cận phổ biến nhất: chỉ tạo instance khi được yêu cầu lần đầu.

TypeScript

// TypeScript - Lazy Singleton
class Logger {
  private static instance: Logger;
  private logs: string[] = [];
 
  // Private constructor - không ai có thể gọi `new Logger()`
  private constructor() {
    console.log("Logger initialized");
  }
 
  public static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }
 
  public log(message: string): void {
    const entry = `[${new Date().toISOString()}] ${message}`;
    this.logs.push(entry);
    console.log(entry);
  }
 
  public getLogs(): string[] {
    return [...this.logs]; // Trả về bản sao để bảo vệ state nội bộ
  }
}
 
// Sử dụng
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
 
logger1.log("Application started");
logger2.log("User logged in");
 
console.log(logger1 === logger2);       // true — cùng instance
console.log(logger1.getLogs().length);   // 2 — cả hai log ở cùng một nơi

Cách hoạt động:

  1. Lần gọi đầu tiên getInstance() — instance là undefined, nên Logger mới được tạo
  2. Mỗi lần gọi sau — instance đã tồn tại, trả về ngay lập tức
  3. private constructor ngăn new Logger() từ bên ngoài class

Python

# Python - Lazy Singleton dùng __new__
import threading
from datetime import datetime
 
 
class Logger:
    _instance = None
 
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._logs = []
            print("Logger initialized")
        return cls._instance
 
    def log(self, message: str) -> None:
        entry = f"[{datetime.now().isoformat()}] {message}"
        self._logs.append(entry)
        print(entry)
 
    def get_logs(self) -> list[str]:
        return self._logs.copy()
 
 
# Sử dụng
logger1 = Logger()
logger2 = Logger()
 
logger1.log("Application started")
logger2.log("User logged in")
 
print(logger1 is logger2)        # True — cùng instance
print(len(logger1.get_logs()))   # 2 — cả hai log ở cùng một nơi

__new__ vs __init__ trong Python: __new__ kiểm soát việc tạo instance (được gọi trước __init__). Bằng cách override nó, chúng ta chặn bước tạo và trả về instance hiện có thay vì tạo mới.

Java

// Java - Lazy Singleton
public class Logger {
    private static Logger instance;
    private final List<String> logs = new ArrayList<>();
 
    // Private constructor
    private Logger() {
        System.out.println("Logger initialized");
    }
 
    public static Logger getInstance() {
        if (instance == null) {
            instance = new Logger();
        }
        return instance;
    }
 
    public void log(String message) {
        String entry = "[" + java.time.Instant.now() + "] " + message;
        logs.add(entry);
        System.out.println(entry);
    }
 
    public List<String> getLogs() {
        return List.copyOf(logs); // Bản sao bất biến
    }
}
 
// Sử dụng
Logger logger1 = Logger.getInstance();
Logger logger2 = Logger.getInstance();
 
logger1.log("Application started");
logger2.log("User logged in");
 
System.out.println(logger1 == logger2);       // true — cùng instance
System.out.println(logger1.getLogs().size());  // 2

Triển khai: Eager Initialization

Tạo instance khi class được load, không phải khi được yêu cầu lần đầu.

TypeScript

// TypeScript - Eager Singleton
class AppConfig {
  // Instance được tạo ngay khi class được load
  private static readonly instance: AppConfig = new AppConfig();
 
  private settings: Map<string, string> = new Map();
 
  private constructor() {
    // Load settings mặc định
    this.settings.set("env", "production");
    this.settings.set("logLevel", "info");
    console.log("AppConfig loaded");
  }
 
  public static getInstance(): AppConfig {
    return AppConfig.instance;
  }
 
  public get(key: string): string | undefined {
    return this.settings.get(key);
  }
 
  public set(key: string, value: string): void {
    this.settings.set(key, value);
  }
}

Python

# Python - Eager Singleton dùng module-level instance
class _AppConfig:
    def __init__(self):
        self._settings: dict[str, str] = {
            "env": "production",
            "log_level": "info",
        }
        print("AppConfig loaded")
 
    def get(self, key: str) -> str | None:
        return self._settings.get(key)
 
    def set(self, key: str, value: str) -> None:
        self._settings[key] = value
 
 
# Module-level instance — được tạo khi module được import lần đầu
config = _AppConfig()
 
 
# Sử dụng (từ module khác):
# from config import config
# config.get("env")  # "production"

Java

// Java - Eager Singleton
public class AppConfig {
    // Instance được tạo khi class được load bởi JVM
    private static final AppConfig INSTANCE = new AppConfig();
 
    private final Map<String, String> settings = new HashMap<>();
 
    private AppConfig() {
        settings.put("env", "production");
        settings.put("logLevel", "info");
        System.out.println("AppConfig loaded");
    }
 
    public static AppConfig getInstance() {
        return INSTANCE;
    }
 
    public String get(String key) {
        return settings.get(key);
    }
 
    public void set(String key, String value) {
        settings.put(key, value);
    }
}

Lazy vs Eager: Khi nào dùng

Khía cạnhLazy InitializationEager Initialization
Instance được tạoKhi gọi getInstance() lần đầuKhi class được load
Chi phí khởi độngTrì hoãn (startup nhanh hơn)Trả trước (startup chậm hơn)
Thread safetyCần xử lý thêmInherently thread-safe
Sử dụng tài nguyênChỉ khi cầnLuôn được cấp phát
Phù hợp choTài nguyên nặng hoặc ít dùngTài nguyên nhẹ, luôn cần

Thread Safety (Tập trung Java)

Lazy Singleton ở trên có vấn đề trong môi trường multithreaded:

// Điều gì xảy ra khi hai thread gọi getInstance() cùng lúc?
//
// Thread A: kiểm tra instance == null → true
// Thread B: kiểm tra instance == null → true (trước khi A tạo xong!)
// Thread A: tạo instance #1
// Thread B: tạo instance #2  ← Hai instance tồn tại!

Giải pháp 1: Synchronized Method

// Java - Thread-safe với synchronized
public class Logger {
    private static Logger instance;
 
    private Logger() {}
 
    // synchronized: chỉ một thread có thể vào tại một thời điểm
    public static synchronized Logger getInstance() {
        if (instance == null) {
            instance = new Logger();
        }
        return instance;
    }
}

Vấn đề: Mọi lần gọi getInstance() đều acquire lock, ngay cả sau khi instance đã được tạo. Điều này thêm overhead không cần thiết.

Giải pháp 2: Double-Checked Locking

// Java - Double-checked locking
public class Logger {
    // volatile đảm bảo tất cả threads thấy giá trị mới nhất
    private static volatile Logger instance;
 
    private Logger() {}
 
    public static Logger getInstance() {
        if (instance == null) {                // Kiểm tra lần 1 (không lock)
            synchronized (Logger.class) {      // Lock chỉ khi cần
                if (instance == null) {        // Kiểm tra lần 2 (có lock)
                    instance = new Logger();
                }
            }
        }
        return instance;
    }
}

Tại sao hai lần kiểm tra? Lần kiểm tra đầu tránh locking mỗi lần gọi. Lần kiểm tra thứ hai ngăn race condition đã mô tả ở trên. Từ khóa volatile đảm bảo instance được tạo hoàn chỉnh trước khi các thread khác nhìn thấy nó.

Giải pháp 3: Bill Pugh Singleton (Khuyến nghị cho Java)

// Java - Bill Pugh Singleton (dùng inner static class)
public class Logger {
    private final List<String> logs = new ArrayList<>();
 
    private Logger() {
        System.out.println("Logger initialized");
    }
 
    // Inner class không được load cho đến khi getInstance() được gọi
    private static class Holder {
        private static final Logger INSTANCE = new Logger();
    }
 
    public static Logger getInstance() {
        return Holder.INSTANCE;
    }
 
    public void log(String message) {
        String entry = "[" + java.time.Instant.now() + "] " + message;
        logs.add(entry);
        System.out.println(entry);
    }
}

Tại sao đây là cách tốt nhất cho Java:

  • Lazy — Inner class Holder không được load cho đến khi gọi getInstance()
  • Thread-safe — JVM đảm bảo class loading là thread-safe
  • Không có synchronization overhead — Không cần synchronized hay volatile
  • Đơn giản — Code sạch, dễ hiểu

So sánh Thread Safety

Cách tiếp cậnThread-Safe?Hiệu năngĐộ phức tạp
Lazy (không sync)KhôngNhanh nhấtĐơn giản
Synchronized methodCóChậm nhất (lock mỗi lần gọi)Đơn giản
Double-checked lockingCóNhanh (lock chỉ lần đầu)Phức tạp
Bill Pugh (inner class)CóNhanhĐơn giản
Eager initializationCóNhanh (cấp phát khi load class)Đơn giản nhất

ES6 Module Pattern (TypeScript/JavaScript)

Trong JavaScript/TypeScript hiện đại, modules mặc định là Singletons. Khi bạn import một module, Node.js cache nó — các import sau trả về cùng một cached instance.

// logger.ts — ES6 module Singleton (không cần class!)
 
const logs: string[] = [];
 
export function log(message: string): void {
  const entry = `[${new Date().toISOString()}] ${message}`;
  logs.push(entry);
  console.log(entry);
}
 
export function getLogs(): string[] {
  return [...logs];
}
// app.ts
import { log, getLogs } from "./logger";
 
log("Application started");
console.log(getLogs().length); // 1
// userService.ts
import { log } from "./logger";
 
log("User logged in");
// Ghi vào CÙNG mảng logs như app.ts

Tại sao hoạt động: Node.js đánh giá module một lần và cache kết quả. Mọi file import logger.ts đều nhận cùng mảng logs và cùng functions. Không cần class, không getInstance(), không private constructor.

Class-Based vs Module-Based Singleton

Tính năngClass SingletonModule Singleton
BoilerplatePrivate constructor, static methodChỉ export functions/objects
Kiểm soát khởi tạogetInstance() tường minhTự động (module caching)
TypeScript/JavaScriptHoạt động nhưng dài dòngIdiomatic và khuyến nghị
TestingKhó reset stateDễ mock hơn với jest.mock()
Ngôn ngữ khácPattern phổ quátChỉ JS/TS

Best practice cho TypeScript/JavaScript: Ưu tiên module pattern hơn class-based Singleton. Dành class-based cho khi bạn cần implement interface hoặc làm việc với frameworks yêu cầu class instances.


Ví dụ Thực tế: Database Connection Pool

Hãy xây dựng một Singleton thực tế — database connection pool được chia sẻ trong toàn bộ ứng dụng.

TypeScript

// TypeScript - Database Connection Pool Singleton
interface ConnectionConfig {
  host: string;
  port: number;
  database: string;
  maxConnections: number;
}
 
class DatabasePool {
  private static instance: DatabasePool;
  private connections: string[] = [];
  private config: ConnectionConfig;
 
  private constructor(config: ConnectionConfig) {
    this.config = config;
    this.initializePool();
    console.log(
      `Database pool created: ${config.host}:${config.port}/${config.database}`
    );
  }
 
  public static initialize(config: ConnectionConfig): DatabasePool {
    if (!DatabasePool.instance) {
      DatabasePool.instance = new DatabasePool(config);
    }
    return DatabasePool.instance;
  }
 
  public static getInstance(): DatabasePool {
    if (!DatabasePool.instance) {
      throw new Error(
        "DatabasePool not initialized. Call initialize() first."
      );
    }
    return DatabasePool.instance;
  }
 
  private initializePool(): void {
    for (let i = 0; i < this.config.maxConnections; i++) {
      this.connections.push(`conn-${i}`);
    }
  }
 
  public getConnection(): string {
    const conn = this.connections.pop();
    if (!conn) {
      throw new Error("No available connections in pool");
    }
    console.log(`Checked out connection: ${conn}`);
    return conn;
  }
 
  public releaseConnection(conn: string): void {
    this.connections.push(conn);
    console.log(`Released connection: ${conn}`);
  }
 
  public getAvailableCount(): number {
    return this.connections.length;
  }
}
 
// Khởi động ứng dụng
DatabasePool.initialize({
  host: "localhost",
  port: 5432,
  database: "myapp",
  maxConnections: 10,
});
 
// Trong bất kỳ service nào — nhận cùng pool
const pool = DatabasePool.getInstance();
const conn = pool.getConnection();
// ... sử dụng connection ...
pool.releaseConnection(conn);

Python

# Python - Database Connection Pool Singleton
from dataclasses import dataclass
from collections import deque
 
 
@dataclass
class ConnectionConfig:
    host: str
    port: int
    database: str
    max_connections: int
 
 
class DatabasePool:
    _instance = None
 
    def __new__(cls, config: ConnectionConfig | None = None):
        if cls._instance is None:
            if config is None:
                raise RuntimeError(
                    "DatabasePool not initialized. Pass config on first call."
                )
            cls._instance = super().__new__(cls)
            cls._instance._config = config
            cls._instance._connections = deque(
                f"conn-{i}" for i in range(config.max_connections)
            )
            print(
                f"Database pool created: "
                f"{config.host}:{config.port}/{config.database}"
            )
        return cls._instance
 
    def get_connection(self) -> str:
        if not self._connections:
            raise RuntimeError("No available connections in pool")
        conn = self._connections.pop()
        print(f"Checked out connection: {conn}")
        return conn
 
    def release_connection(self, conn: str) -> None:
        self._connections.append(conn)
        print(f"Released connection: {conn}")
 
    @property
    def available_count(self) -> int:
        return len(self._connections)
 
 
# Khởi động ứng dụng
pool = DatabasePool(ConnectionConfig("localhost", 5432, "myapp", 10))
 
# Trong bất kỳ module nào — cùng instance
pool = DatabasePool()
conn = pool.get_connection()
# ... sử dụng connection ...
pool.release_connection(conn)

Java

// Java - Database Connection Pool Singleton (Bill Pugh pattern)
public class DatabasePool {
    private final Queue<String> connections = new LinkedList<>();
    private final ConnectionConfig config;
 
    private DatabasePool() {
        this.config = ConnectionConfig.load(); // Load từ environment/file
        for (int i = 0; i < config.maxConnections(); i++) {
            connections.add("conn-" + i);
        }
        System.out.printf(
            "Database pool created: %s:%d/%s%n",
            config.host(), config.port(), config.database()
        );
    }
 
    private static class Holder {
        private static final DatabasePool INSTANCE = new DatabasePool();
    }
 
    public static DatabasePool getInstance() {
        return Holder.INSTANCE;
    }
 
    public synchronized String getConnection() {
        if (connections.isEmpty()) {
            throw new RuntimeException("No available connections in pool");
        }
        String conn = connections.poll();
        System.out.println("Checked out connection: " + conn);
        return conn;
    }
 
    public synchronized void releaseConnection(String conn) {
        connections.add(conn);
        System.out.println("Released connection: " + conn);
    }
 
    public synchronized int getAvailableCount() {
        return connections.size();
    }
}
 
// Sử dụng trong bất kỳ service nào
DatabasePool pool = DatabasePool.getInstance();
String conn = pool.getConnection();
// ... sử dụng connection ...
pool.releaseConnection(conn);

Cuộc tranh luận Anti-Pattern

Singleton là pattern bị chỉ trích nhiều nhất trong Gang of Four patterns. Đây là lý do:

Lập luận Chống Singleton

1. Hidden Dependencies (Phụ thuộc ẩn)

// Xấu — UserService lấy database từ đâu?
class UserService {
  createUser(name: string): void {
    // Phụ thuộc ẩn — không thể biết nếu không đọc code
    const db = DatabasePool.getInstance();
    db.query(`INSERT INTO users (name) VALUES ('${name}')`);
  }
}
 
// Tốt — Dependencies tường minh
class UserService {
  constructor(private db: DatabasePool) {}
 
  createUser(name: string): void {
    this.db.query(`INSERT INTO users (name) VALUES ('${name}')`);
  }
}

2. Global Mutable State (Trạng thái toàn cục có thể thay đổi)

Singletons thực chất là biến toàn cục được bọc trong class. Bất kỳ code nào ở bất kỳ đâu đều có thể thay đổi state:

// Module A thay đổi config
AppConfig.getInstance().set("timeout", "5000");
 
// Module B đọc config sau — bị ảnh hưởng bởi Module A
const timeout = AppConfig.getInstance().get("timeout");
// Là "5000"? Giá trị mặc định? Ai biết nếu không trace mọi lần gọi?

3. Vi phạm Single Responsibility Principle

Singleton class quản lý hai thứ: business logic thực tế và lifecycle của chính nó (tạo và truy cập). Điều này vi phạm SRP từ nguyên tắc SOLID bạn vừa học.

4. Tight Coupling (Liên kết chặt)

Mọi class gọi Singleton.getInstance() đều bị coupled với class cụ thể đó. Bạn không thể thay thế bằng implementation khác mà không sửa code gọi.

Lập luận Ủng hộ Singleton

Use CaseTại sao Singleton phù hợp
Framework infrastructureLogging, metrics, config — thực sự là concern toàn app
Quản lý tài nguyênConnection pools, thread pools — một pool chia sẻ là đúng
CachingMột cache tránh trùng lặp dữ liệu
Truy cập phần cứngPrinter spooler, device driver — một tài nguyên vật lý

Kết luận

Đừng tránh Singleton hoàn toàn — hãy tránh dùng nó như mặc định. Nếu bạn dùng Singleton vì tiện lợi, hãy dừng lại. Nếu bạn dùng vì thực sự chỉ cần một instance duy nhất, hãy tiến hành cẩn thận và cân nhắc dependency injection.


Thách thức Testing và Giải pháp

Vấn đề thực tế lớn nhất với Singletons là testing. Vì instance tồn tại qua các test, một test có thể ảnh hưởng test khác.

Vấn đề

// Test 1: Thêm entries vào logger
test("should log messages", () => {
  const logger = Logger.getInstance();
  logger.log("test message");
  expect(logger.getLogs()).toHaveLength(1); // ✅ Pass
});
 
// Test 2: Mong đợi logger mới — nhưng nhận state của Test 1!
test("should start with empty logs", () => {
  const logger = Logger.getInstance();
  expect(logger.getLogs()).toHaveLength(0); // ❌ FAIL — có 1 entry từ Test 1
});

Giải pháp 1: Thêm Reset Method (Chỉ cho Testing)

class Logger {
  private static instance: Logger;
  private logs: string[] = [];
 
  private constructor() {}
 
  public static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }
 
  public log(message: string): void {
    this.logs.push(`[${new Date().toISOString()}] ${message}`);
  }
 
  public getLogs(): string[] {
    return [...this.logs];
  }
 
  // Chỉ cho testing — cho phép reset state giữa các test
  public static resetInstance(): void {
    Logger.instance = undefined as unknown as Logger;
  }
}
 
// Trong tests
afterEach(() => {
  Logger.resetInstance();
});

Giải pháp 2: Dependency Injection (Khuyến nghị)

Thay vì truy cập Singleton trực tiếp, inject nó như dependency:

// Định nghĩa interface
interface ILogger {
  log(message: string): void;
  getLogs(): string[];
}
 
// Singleton implement interface
class Logger implements ILogger {
  private static instance: Logger;
  private logs: string[] = [];
 
  private constructor() {}
 
  public static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }
 
  public log(message: string): void {
    this.logs.push(`[${new Date().toISOString()}] ${message}`);
  }
 
  public getLogs(): string[] {
    return [...this.logs];
  }
}
 
// Service phụ thuộc vào interface, không phải Singleton
class OrderService {
  constructor(private logger: ILogger) {}
 
  placeOrder(item: string): void {
    this.logger.log(`Order placed: ${item}`);
  }
}
 
// Production — inject Singleton
const orderService = new OrderService(Logger.getInstance());
 
// Testing — inject mock
class MockLogger implements ILogger {
  public messages: string[] = [];
 
  log(message: string): void {
    this.messages.push(message);
  }
 
  getLogs(): string[] {
    return this.messages;
  }
}
 
test("should log when order is placed", () => {
  const mockLogger = new MockLogger();
  const service = new OrderService(mockLogger);
 
  service.placeOrder("Laptop");
 
  expect(mockLogger.messages).toHaveLength(1);
  expect(mockLogger.messages[0]).toContain("Laptop");
});
# Python - Dependency Injection với Singleton
from abc import ABC, abstractmethod
 
 
class ILogger(ABC):
    @abstractmethod
    def log(self, message: str) -> None: ...
 
    @abstractmethod
    def get_logs(self) -> list[str]: ...
 
 
class Logger(ILogger):
    _instance = None
 
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._logs = []
        return cls._instance
 
    def log(self, message: str) -> None:
        self._logs.append(message)
 
    def get_logs(self) -> list[str]:
        return self._logs.copy()
 
 
class OrderService:
    def __init__(self, logger: ILogger):
        self._logger = logger
 
    def place_order(self, item: str) -> None:
        self._logger.log(f"Order placed: {item}")
 
 
# Production
service = OrderService(Logger())
 
# Testing
class MockLogger(ILogger):
    def __init__(self):
        self.messages: list[str] = []
 
    def log(self, message: str) -> None:
        self.messages.append(message)
 
    def get_logs(self) -> list[str]:
        return self.messages
 
 
def test_place_order():
    mock = MockLogger()
    service = OrderService(mock)
    service.place_order("Laptop")
    assert len(mock.messages) == 1
    assert "Laptop" in mock.messages[0]
// Java - Dependency Injection với Singleton
public interface ILogger {
    void log(String message);
    List<String> getLogs();
}
 
public class Logger implements ILogger {
    private static class Holder {
        private static final Logger INSTANCE = new Logger();
    }
 
    private final List<String> logs = new ArrayList<>();
    private Logger() {}
 
    public static Logger getInstance() {
        return Holder.INSTANCE;
    }
 
    @Override
    public void log(String message) {
        logs.add("[" + java.time.Instant.now() + "] " + message);
    }
 
    @Override
    public List<String> getLogs() {
        return List.copyOf(logs);
    }
}
 
public class OrderService {
    private final ILogger logger;
 
    // Inject dependency — không gắn với Logger.getInstance()
    public OrderService(ILogger logger) {
        this.logger = logger;
    }
 
    public void placeOrder(String item) {
        logger.log("Order placed: " + item);
    }
}
 
// Production
OrderService service = new OrderService(Logger.getInstance());
 
// Testing
ILogger mockLogger = mock(ILogger.class);
OrderService testService = new OrderService(mockLogger);
testService.placeOrder("Laptop");
verify(mockLogger).log(contains("Laptop"));

Cách tiếp cận này cho bạn điều tốt nhất của cả hai thế giới: một instance trong production (Singleton), nhưng hoàn toàn testable (qua interface).


Khi nào dùng Singleton

Use Cases phù hợp

Use CaseVí dụTại sao Singleton phù hợp
LoggingLogger toàn ứng dụngTất cả modules nên ghi vào một đích
ConfigurationApp settings từ env/fileLoad một lần, đọc nhiều lần
Connection poolsDatabase, HTTP client poolsMột pool chia sẻ là thiết kế đúng
CachesIn-memory cache layerMột cache ngăn không nhất quán dữ liệu
Feature flagsRuntime feature togglesMột nguồn sự thật cho flags

Khi KHÔNG nên dùng Singleton

Tình huốngTại sao khôngThay thế
"Tôi cần cái này ở mọi nơi"Tiện lợi ≠ thiết kế tốtTruyền qua constructor (DI)
Stateless servicesKhông có shared state cần bảo vệTạo instances tự do
Phạm vi per-requestWeb requests cần isolationRequest-scoped instances
Unit testingGlobal state phá vỡ test isolationInterface + DI
Nhiều configurationsDB pools khác nhau cho read/writeFactory pattern thay thế

Sơ đồ Quyết định


Singleton vs Các Patterns Khác

PatternMục đíchSố Instance
SingletonĐảm bảo một instance, truy cập globalĐúng 1
FactoryTạo objects không chỉ định class cụ thểNhiều (một mỗi lần gọi)
Dependency InjectionCung cấp dependencies từ bên ngoàiQuản lý bởi container
Static classHàm tiện ích không trạng tháiKhông có instance (toàn static)

Singleton vs Static Class:

Static class (utility class chỉ có static methods) không có state và không có instance. Dùng cho pure utility functions như Math.max() hay StringUtils.capitalize(). Dùng Singleton khi bạn cần state (như log buffer hoặc connection pool) được chia sẻ trong toàn ứng dụng.


Tổng kết và Điểm chính

Singleton Pattern

✅ Đảm bảo một class có đúng một instance với truy cập global
✅ Dùng private constructor và static getInstance() method
✅ Trong TypeScript/JS, ưu tiên ES6 module pattern hơn class-based Singleton
✅ Trong Java, ưu tiên Bill Pugh inner class pattern cho thread safety
✅ Luôn inject Singletons qua interfaces cho testability
✅ Dùng Singleton cho infrastructure concerns (logging, config, connection pools)

Lỗi thường gặp cần tránh

  • Đừng dùng Singleton chỉ vì tiện — Nếu bạn dùng vì "tôi cần cái này ở mọi nơi", hãy dùng dependency injection thay thế
  • Đừng ẩn dependencies — Làm cho việc dùng Singleton tường minh qua constructor injection
  • Đừng quên thread safety — Trong môi trường multithreaded (Java), dùng Bill Pugh hoặc eager initialization
  • Đừng lưu state riêng per-request — Singletons sống suốt đời ứng dụng, không phải per-request

Tham khảo Nhanh

Ngôn ngữCách tiếp cận Khuyến nghị
TypeScript/JSES6 module pattern (export functions/objects)
PythonModule-level instance hoặc override __new__
JavaBill Pugh inner static class

Bài tập Thực hành

  1. Xây dựng Configuration Manager: Tạo Singleton ConfigManager đọc settings từ dictionary/map (giả lập config file). Nó cần hỗ trợ get(key), set(key, value) và getAll(). Sau đó viết test dùng mock IConfigManager interface thay thế.

  2. Thread-Safe Counter: Triển khai Singleton RequestCounter đếm tổng số HTTP requests. Trong Java, làm thread-safe dùng Bill Pugh pattern với synchronized increment/read methods. Trong Python, dùng threading.Lock.

  3. Refactor bỏ Singleton: Lấy code này và refactor dùng dependency injection thay vì getInstance():

    class NotificationService {
      notify(userId: string, message: string): void {
        Logger.getInstance().log(`Notifying ${userId}: ${message}`);
        EmailService.getInstance().send(userId, message);
        MetricsService.getInstance().increment("notifications_sent");
      }
    }

Tiếp theo

Singleton đảm bảo một instance. Nhưng khi bạn cần tạo nhiều objects mà không biết chính xác type? Đó là lúc Factory patterns xuất hiện:

  • Factory Method — Định nghĩa interface cho việc tạo objects, để subclasses quyết định class nào được khởi tạo
  • Abstract Factory — Tạo các families of related objects mà không chỉ định concrete classes
  • Khi nào dùng Factory vs Abstract Factory vs Builder

Tiếp tục hành trình OOP: Factory và Abstract Factory Patterns


Tài liệu Bổ sung

Các Bài viết Trước trong Series

Bài viết Liên quan

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.