Singleton — một mẫu tạo lập đảm bảo một phiên bản duy nhất của lớp và cung cấp điểm truy cập toàn cầu tới nó. Singleton được sử dụng rộng rãi trong phát triển di động cho các tài nguyên dùng chung: máy khách mạng, cơ sở dữ liệu, trình quản lý cài đặt. Mẫu này được mô tả trong cuốn sách kinh điển GoF (1994) và vẫn là một trong những mẫu dễ nhận biết nhất. Tìm hiểu thêm tại Refactoring Guru: Singleton.
Những điểm chính
Singleton — một mẫu thiết kế tạo lập được GoF (Gang of Four) mô tả vào năm 1994. Mẫu này giải quyết hai vấn đề: nó giới hạn việc tạo phiên bản của lớp thành một đối tượng duy nhất và cung cấp quyền truy cập toàn cầu tới đối tượng đó. Singleton hữu ích cho các tài nguyên phải là duy nhất: nhà máy phiên, bộ nhớ đệm hình ảnh, trình quản lý kết nối cơ sở dữ liệu, máy khách Crashlytics hoặc Analytics.
Triển khai Singleton yêu cầu hàm tạo riêng tư (ngăn tạo từ bên ngoài), trường tĩnh với phiên bản duy nhất và phương thức truy cập tĩnh (shared, instance, getInstance). Máy khách gọi Singleton.shared.method() mà không lo lắng về việc tạo đối tượng. Mẫu này phổ biến trong iOS và Android: URLSession.shared, UserDefaults.standard, FirebaseApp.sharedInstance — tất cả đều là Singleton. Tuy nhiên, việc sử dụng quá mức Singleton dẫn đến phản mẫu Global State.
Vấn đề của Singleton — phụ thuộc ẩn (các lớp phụ thuộc ngầm vào đối tượng Singleton), độ phức tạp kiểm thử (không thể thay thế phiên bản trong bài kiểm tra mà không có nỗ lực thêm), vi phạm Nguyên tắc Trách nhiệm Đơn lẻ (Singleton quản lý cả phiên bản của nó và logic nghiệp vụ). Phát triển di động hiện đại ưa chuộng DI (Dagger, Hilt, Swinject) để quản lý các phiên bản duy nhất — vùng chứa DI tạo đối tượng một lần và tiêm nó qua hàm tạo.
Swift Singleton được triển khai thông qua thuộc tính tĩnh shared với bộ khởi tạo riêng tư. Kể từ Swift 3, việc khởi tạo trễ của các thuộc tính tĩnh được đảm bảo an toàn luồng — trình biên dịch tự động thêm đồng bộ hóa qua dispatch_once. Chỉ cần khai báo static let shared = Class() và làm cho init() là riêng tư. Swift không yêu cầu đồng bộ hóa thêm cho truy cập đơn luồng sau khi khởi tạo.
final class NetworkManager {
// Singleton an toàn luồng
static let shared = NetworkManager()
private init() {
URLSessionConfiguration.default.timeoutIntervalForRequest = 30
}
private var cache = NSCache<NSString, NSData>()
func fetchData(from url: URL) async throws -> Data {
let key = url.absoluteString as NSString
if let cached = cache.object(forKey: key) {
return cached as Data
}
let (data, _) = try await URLSession.shared.data(from: url)
cache.setObject(data as NSData, forKey: key)
return data
}
}
// Sử dụng
let data = try await NetworkManager.shared.fetchData(from: url)
Apple Singleton — nhiều đối tượng iOS SDK sử dụng Singleton: UIApplication.shared, UIScreen.main, FileManager.default, NotificationCenter.default, UserDefaults.standard. Apple sử dụng Singleton cho các dịch vụ duy nhất về mặt vật lý (một màn hình, một ứng dụng). Các nhà phát triển sao chép mẫu này cho các dịch vụ của riêng họ. Trong SwiftUI, quyền truy cập toàn cầu vào Singleton được thay thế bằng Environment và @EnvironmentObject, cải thiện khả năng kiểm thử.
Kotlin Singleton — cách đơn giản nhất: từ khóa object khai báo một lớp singleton với khởi tạo trễ khi truy cập lần đầu. Kotlin object an toàn luồng và không yêu cầu đồng bộ hóa thêm. Nếu cần Singleton với tham số hàm tạo, sử dụng companion object với ủy quyền lazy. Trong Android, Singleton thường cần thiết cho ngữ cảnh Application và các dịch vụ khởi tạo qua Application.onCreate().
// Tùy chọn 1: object — Singleton đơn giản không tham số
object AppPreferences {
private val prefs = Application.instance
.getSharedPreferences("app", Context.MODE_PRIVATE)
var isFirstLaunch: Boolean
get() = prefs.getBoolean("first_launch", true)
set(value) = prefs.edit { putBoolean("first_launch", value) }
}
// Tùy chọn 2: companion object — Singleton có tham số
class ApiClient private constructor(baseUrl: String) {
companion object {
@Volatile
private var instance: ApiClient? = null
fun getInstance(baseUrl: String): ApiClient {
return instance ?: this.synchronized {
instance ?: ApiClient(baseUrl).also { instance = it }
}
}
}
fun request(endpoint: String): String { /* ... */ }
}
Singleton Android SDK — nhiều dịch vụ hệ thống Android triển khai Singleton: context.getSystemService(), Room.databaseBuilder(), Retrofit.Builder(). Ví dụ bao gồm SharedPreferences, MediaPlayer, AudioManager. Trong ứng dụng Android, Singleton thường được sử dụng cho kho lưu trữ, trình quản lý và nhà máy. Google khuyến nghị thay thế Singleton bằng DI (Hilt, Koin), nơi phạm vi Singleton (Scope.Singleton hoặc @Singleton) được quản lý bởi vùng chứa trong khi lớp vẫn có thể kiểm thử được.
Thread safety — một yêu cầu quan trọng đối với Singleton trong môi trường đa luồng. Không có đồng bộ hóa, hai luồng có thể đồng thời kiểm tra instance == null và tạo hai phiên bản. Giải pháp là khóa trong khi tạo lần đầu và giải phóng sau khi khởi tạo. Trong Swift, các thuộc tính tĩnh (static let) an toàn luồng theo mặc định. Trong Kotlin, object an toàn luồng. Đối với kiểu Java trong Kotlin, sử dụng synchronized hoặc @Volatile + double-check locking.
| Ngôn ngữ | Cơ chế | An toàn luồng | Khởi tạo trễ |
|---|---|---|---|
| Swift | static let | dispatch_once (tự động) | Có, khi truy cập lần đầu |
| Kotlin object | Khai báo object | Trình khởi tạo lớp an toàn luồng | Có, khi truy cập lần đầu |
| Kotlin companion | synchronized + @Volatile | Double-checked locking | Có, qua lazy hoặc synchronized |
| Java | synchronized + volatile | Double-checked locking | Có, trong getInstance() |
Double-checked locking — một mẫu cho khởi tạo trễ Singleton. Kiểm tra đầu tiên không đồng bộ hóa (nhanh nếu phiên bản đã tồn tại), lần thứ hai bên trong synchronized (chỉ một luồng tạo). @Volatile đảm bảo khả năng hiển thị thay đổi cho tất cả các luồng. Không có volatile, một luồng khác có thể thấy đối tượng được xây dựng một phần. Trong Kotlin, ủy quyền lazy với LazyThreadSafetyMode.SYNCHRONIZED tự động triển khai double-checked locking.
Dependency Injection — một giải pháp thay thế cho Singleton để quản lý các phiên bản duy nhất. Vùng chứa DI (Dagger, Hilt, Koin, Swinject) tạo đối tượng một lần trong phạm vi Singleton và tiêm nó qua hàm tạo. Lớp không biết về trạng thái Singleton của nó — vùng chứa quyết định. Mã trở nên có thể kiểm thử: mô-đun DI được thay thế bằng mô-đun giả trong bài kiểm tra. Ưu điểm của DI: phụ thuộc rõ ràng trong hàm tạo, khả năng ghi đè, vòng đời thống nhất.
Khi nào Singleton được biện minh — đối tượng cấp hệ thống: Crashlytics, Analytics, Logging. Các dịch vụ này được khởi tạo một lần trong AppDelegate/Application và được sử dụng ở khắp mọi nơi. DI là quá mức đối với chúng. Singleton cũng thuận tiện cho bộ nhớ đệm hình ảnh (NSCache, Coil, Glide), nơi truy cập toàn cầu được biện minh bởi hiệu suất. Đối với mọi thứ khác, DI được ưa chuộng hơn: nó làm cho các phụ thuộc hiển thị, đơn giản hóa kiểm thử và tái cấu trúc.
Phương pháp kết hợp — Singleton với khả năng ghi đè cho kiểm thử. Trong Swift, một giao thức + thuộc tính tĩnh mà bài kiểm tra có thể thay thế (ví dụ, qua URLProtocol cho URLSession). Trong Kotlin, một lớp mở với thuộc tính có thể tiêm, nơi bài kiểm tra đặt giả qua phản xạ hoặc setter. Phương pháp này giữ sự đơn giản của Singleton nhưng cung cấp khả năng kiểm thử. Google khuyến nghị Hilt cho Android, Apple không áp đặt DI cho iOS — sự lựa chọn phụ thuộc vào nhóm.
Các câu hỏi thường gặp
Không, Singleton là một mẫu GoF, nhưng việc sử dụng sai thường xuyên biến nó thành phản mẫu Global State. Singleton được biện minh cho các tài nguyên duy nhất về mặt vật lý (màn hình, máy in, hệ thống tệp). Vấn đề phát sinh khi Singleton được sử dụng để quản lý dữ liệu: phụ thuộc ẩn, độ phức tạp kiểm thử, vi phạm Nguyên tắc Trách nhiệm Đơn lẻ. Giải pháp thay thế hiện đại là DI với phạm vi Singleton.
Ba phương pháp: (1) qua giao thức — Singleton triển khai giao thức, bài kiểm tra thay đổi triển khai; (2) qua DI — Singleton được tiêm như phụ thuộc qua hàm tạo; (3) qua phương thức reset — Singleton có phương thức để đặt lại trạng thái trong bài kiểm tra (chỉ cho bản dựng kiểm thử). Phương pháp đầu tiên được ưa chuộng hơn, phương pháp thứ ba nguy hiểm cho sản xuất. Swift cho phép thay thế thuộc tính shared qua thao tác thời gian chạy trong bài kiểm tra.
Kotlin object là một cấu trúc ngôn ngữ tạo Singleton ở cấp bytecode. Không giống như triển khai Java với hàm tạo riêng tư và getInstance(), object đảm bảo an toàn luồng, khởi tạo trễ và cấm kế thừa. Java Singleton yêu cầu đồng bộ hóa thủ công (synchronized) và volatile để hoạt động đúng trong môi trường đa luồng. Kotlin object là cách an toàn nhất và ngắn gọn nhất trong Android.
Kế thừa Singleton phá vỡ mẫu: nếu một lớp Singleton có thể được kế thừa, một lớp con có thể tạo phiên bản thứ hai, vi phạm tính duy nhất. Trong Swift, final class cấm kế thừa. Kotlin object không thể được kế thừa (object là sealed). Nếu cần Singleton với tính biến đổi, hãy sử dụng vùng chứa DI với phạm vi Singleton: nó đảm bảo một phiên bản duy nhất và hỗ trợ kế thừa qua giao diện.
Tham số được truyền qua init(context: Application) hoặc getInstance(param). Kotlin object không chấp nhận tham số — sử dụng companion object với phương thức nhà máy getInstance(param). Hilt giải quyết vấn đề: @Singleton + @Inject constructor(context: Application) — vùng chứa DI tự động tiêm ngữ cảnh Application. Đối với máy khách Retrofit, các tham số (baseUrl, interceptors) được truyền qua builder trong mô-đun DI.
Tóm tắt
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm