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
- Hoàn thành Nguyên tắc SOLID
- Hoàn thành Encapsulation và Information Hiding
- Hoàn thành Đa hình và Interfaces
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ên | Tại sao chỉ một? |
|---|---|
| Logger | Tất cả phần của app nên ghi vào cùng một log |
| Configuration | Settings được load một lần, dùng ở mọi nơi |
| Database connection pool | Một pool chia sẻ cho tất cả services |
| Cache | Một cache chung để tránh trùng lặp dữ liệu |
| Thread pool | Một pool để quản lý concurrency |
Không có Singleton, bạn có nguy cơ:
- Trùng lặp instance — Hai logger ghi vào file khác nhau, hai cache với dữ liệu cũ
- 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
- 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:
- Private constructor — Ngăn bất kỳ ai gọi
new Singleton() - Static instance field — Lưu trữ instance duy nhất
- 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ơiCách hoạt động:
- Lần gọi đầu tiên
getInstance()—instancelàundefined, nênLoggermới được tạo - Mỗi lần gọi sau —
instanceđã tồn tại, trả về ngay lập tức private constructorngănnew 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()); // 2Triể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ạnh | Lazy Initialization | Eager Initialization |
|---|---|---|
| Instance được tạo | Khi gọi getInstance() lần đầu | Khi class được load |
| Chi phí khởi động | Trì hoãn (startup nhanh hơn) | Trả trước (startup chậm hơn) |
| Thread safety | Cần xử lý thêm | Inherently thread-safe |
| Sử dụng tài nguyên | Chỉ khi cần | Luôn được cấp phát |
| Phù hợp cho | Tài nguyên nặng hoặc ít dùng | Tà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
Holderkhông được load cho đến khi gọigetInstance() - Thread-safe — JVM đảm bảo class loading là thread-safe
- Không có synchronization overhead — Không cần
synchronizedhayvolatile - Đơn giản — Code sạch, dễ hiểu
So sánh Thread Safety
| Cách tiếp cận | Thread-Safe? | Hiệu năng | Độ phức tạp |
|---|---|---|---|
| Lazy (không sync) | Không | Nhanh nhất | Đơn giản |
| Synchronized method | Có | Chậm nhất (lock mỗi lần gọi) | Đơn giản |
| Double-checked locking | Có | Nhanh (lock chỉ lần đầu) | Phức tạp |
| Bill Pugh (inner class) | Có | Nhanh | Đơn giản |
| Eager initialization | Có | 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.tsTạ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ăng | Class Singleton | Module Singleton |
|---|---|---|
| Boilerplate | Private constructor, static method | Chỉ export functions/objects |
| Kiểm soát khởi tạo | getInstance() tường minh | Tự động (module caching) |
| TypeScript/JavaScript | Hoạt động nhưng dài dòng | Idiomatic và khuyến nghị |
| Testing | Khó reset state | Dễ mock hơn với jest.mock() |
| Ngôn ngữ khác | Pattern phổ quát | Chỉ 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 Case | Tại sao Singleton phù hợp |
|---|---|
| Framework infrastructure | Logging, metrics, config — thực sự là concern toàn app |
| Quản lý tài nguyên | Connection pools, thread pools — một pool chia sẻ là đúng |
| Caching | Một cache tránh trùng lặp dữ liệu |
| Truy cập phần cứng | Printer 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 Case | Ví dụ | Tại sao Singleton phù hợp |
|---|---|---|
| Logging | Logger toàn ứng dụng | Tất cả modules nên ghi vào một đích |
| Configuration | App settings từ env/file | Load một lần, đọc nhiều lần |
| Connection pools | Database, HTTP client pools | Một pool chia sẻ là thiết kế đúng |
| Caches | In-memory cache layer | Một cache ngăn không nhất quán dữ liệu |
| Feature flags | Runtime feature toggles | Một nguồn sự thật cho flags |
Khi KHÔNG nên dùng Singleton
| Tình huống | Tại sao không | Thay thế |
|---|---|---|
| "Tôi cần cái này ở mọi nơi" | Tiện lợi ≠ thiết kế tốt | Truyền qua constructor (DI) |
| Stateless services | Không có shared state cần bảo vệ | Tạo instances tự do |
| Phạm vi per-request | Web requests cần isolation | Request-scoped instances |
| Unit testing | Global state phá vỡ test isolation | Interface + DI |
| Nhiều configurations | DB pools khác nhau cho read/write | Factory pattern thay thế |
Sơ đồ Quyết định
Singleton vs Các Patterns Khác
| Pattern | Mục đích | Số Instance |
|---|---|---|
| Singleton | Đảm bảo một instance, truy cập global | Đúng 1 |
| Factory | Tạo objects không chỉ định class cụ thể | Nhiều (một mỗi lần gọi) |
| Dependency Injection | Cung cấp dependencies từ bên ngoài | Quản lý bởi container |
| Static class | Hàm tiện ích không trạng thái | Khô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/JS | ES6 module pattern (export functions/objects) |
| Python | Module-level instance hoặc override __new__ |
| Java | Bill Pugh inner static class |
Bài tập Thực hành
-
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 mockIConfigManagerinterface thay thế. -
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ớisynchronizedincrement/read methods. Trong Python, dùngthreading.Lock. -
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
- Lộ trình OOP & Design Patterns - Lộ trình học đầy đủ
- Bốn Trụ Cột của OOP - Nền tảng OOP
- Classes, Objects, và Abstraction - Các khối xây dựng của OOP
- Encapsulation và Information Hiding - Bảo vệ tính toàn vẹn dữ liệu
- Kế thừa và Composition - Chiến lược tái sử dụng code
- Đa hình và Interfaces - Một interface, nhiều hình thái
- Nguyên tắc SOLID - Hướng dẫn thiết kế
Bài viết Liên quan
- Java Phase 2: Lập trình Hướng đối tượng - OOP trong Java
- Python Phase 2: OOP & Tính năng Nâng cao - OOP trong Python
- Spring Boot Caching với Redis - Ví dụ thực tế Singleton cache
📬 Đă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.