Singleton is a creational design pattern that lets you ensure that a class has only one instance and provide a global access point to this instance.

Analogy
Giả sử ta có một chương trình với một nút bật tắt dark mode. Ta không thể tạo ra hai thể hiện, một chỉ để bật và một chỉ để tắt. Thay vào đó, ta chỉ nên có duy nhất một thể hiện của lớp đối tượng DarkModeButton được sử dụng xuyên suốt chương trình.
Solution
Singleton Pattern đảm bảo chỉ có một và chỉ một thể hiện được tạo ra trong chương trình.
Khi implement Singleton pattern, ta cần phải xây dựng các thành phần sau:
- Private constructor để hạn chế truy cập từ bên ngoài.
- Tạo ra một biến private static để đảm bảo biến chỉ được khởi tạo trong class.
- Có một phương thức public static để trả về instance được khởi tạo ở trên.
Tùy từng loại ngôn ngữ mà có nhiều cách implement khác nhau. Chẳng hạn Java có thể sử dụng khởi tạo early/eager (khởi tạo instance bên ngoài phương thức), nhưng C++ thì không.
Lazy Initialization
Cách xây dựng này tạo ra instance chỉ khi nào chúng ta cần dùng đến (gọi hàm getInstance). Nó trái ngược với early/eager khi mà cách khởi tạo đó khởi tạo instance bất chấp chúng ta có sử dụng đến nó hay không.
Code
#include <iostream>
std::string DEFAULT_THEME = "dark";
class SingletonThemeButton {
private:
static SingletonThemeButton* _instance;
std::string _theme;
private:
SingletonThemeButton(std::string theme) {
_theme = theme;
std::cout << "Button is created\n";
}
public:
static SingletonThemeButton* getInstance() {
if (_instance == nullptr) {
_instance = new SingletonThemeButton(DEFAULT_THEME);
}
return _instance;
}
public:
void setTheme(std::string newTheme) { _theme = newTheme; };
std::string getTheme() { return _theme; }
};
SingletonThemeButton* SingletonThemeButton::_instance = nullptr;Lưu ý
Luôn phải khởi tạo giá trị cho instance pointer, vì instance chỉ được tạo ra khi có nhu cầu. Nếu không khởi tạo thì vùng nhớ mà instance trỏ đến sẽ là vùng nhớ rác, dẫn đến việc hao phí bộ nhớ không cần thiết.
Có thể dùng từ khóa inline (có ở C++17) để khởi tạo bên trong class:
inline static SingletonThemeButton* _instance = nullptr;Test
void TestSingleton() {
SingletonThemeButton* themeButton = SingletonThemeButton::getInstance();
std::cout << "Theme button's memory address: " << themeButton << std::endl;
std::cout << "Current theme: " << themeButton->getTheme() << std::endl;
SingletonThemeButton* secondThemeButton = SingletonThemeButton::getInstance();
std::cout << "The second theme button's memory address: " << secondThemeButton << std::endl;
themeButton->setTheme("light");
std::cout << "Current theme: " << themeButton->getTheme() << std::endl;
}Output
Button is created
Theme button's memory address: 000001CDF0F83A00
Current theme: dark
The second theme button's memory address: 000001CDF0F83A00
Current theme: lightCó thể thấy, dù cho hàm getInstance được gọi hai lần, nhưng vẫn chỉ có một constructor được tạo ra. Và đồng thời, nếu instance đã được tạo, hàm getInstance sẽ trả về đối tượng đã tạo. Địa chỉ của con trỏ themeButton và secondThemeButton đều là một, thể hiện cho việc chúng cùng trỏ vào một vùng nhớ.
Pointer Problem!
Class
SingletonThemeButtonđược tạo ra với con trỏ_instance, nhưng không có hàm hủy nào được gọi. Vì vẫn chưa có một object nào thực sự được tạo ra lúc run time (_instancesinh ra lúc compile time vì nằm trong khai báo class).
Để tự động giải phóng vùng nhớ cho con trỏ _instance, ta cần sử dụng con trỏ thông minh (C++ Smart Pointer) như sau:
#include <iostream>
#include <memory>
class SingletonThemeButton {
private:
inline static std::shared_ptr<SingletonThemeButton> _instance = nullptr;
std::string _theme;
private:
SingletonThemeButton(std::string theme) {
_theme = theme;
std::cout << "Button is created\n";
}
public:
static std::shared_ptr<SingletonThemeButton> getInstance() {
if (_instance == nullptr) {
_instance = std::shared_ptr<SingletonThemeButton>(new SingletonThemeButton(DEFAULT_THEME));
}
return _instance;
}
public:
void setTheme(std::string newTheme) { _theme = newTheme; };
std::string getTheme() { return _theme; }
};Thread Safe Initialization
Trường hợp đa luồng
Cách khởi tạo trên chỉ hoạt động trong môi trường đơn luồng. Giả sử có hai luồng chạy song song và cùng gọi đến hàm
getInstancetrong cùng một thời điểm, lúc này hàm sẽ đồng thời tạo ra hai instance pointer, dẫn đến việc vi phạm nguyên tắc của Singleton.
Để khắc phục vấn đề trên, ta không tạo ra instance pointer trong phần khai báo thuộc tính nữa. Thay vào đó, ta tạo luôn một instance hoặc instance pointer bên trong hàm getInstance.
Code minh họa:
class DarkModeButton {
// ...
public:
// Static method, include static instance pointer
static SingletonThemeButton* getInstance() {
static SingletonThemeButton instance(DEFAULT_THEME);
return &instance;
}
// Use reference data type instead. This is also known as "Meyers Singleton"
static SingletonThemeButton& getInstance() {
static SingletonThemeButton instance(DEFAULT_THEME);
return instance;
}
};Warning
Lưu ý: hai cách xây dựng hàm
getInstancetrên chỉ thread safe với C++11 trở lên.
Applicability
Sử dụng Singleton khi ta muốn:
- Đảm bảo rằng chỉ có một instance của lớp.
- Quản lý số lượng thể hiện của một lớp trong giới hạn nhất định.
Pros and Cons
| Pros | Cons |
|---|---|
| Có thể đảm bảo rằng lớp chỉ có một thể hiện | Có thể vi phạm tính single responsibility khi giải quyết hai vấn đề cùng một lúc (do singleton object là một global object nên lập trình viên thường có xu hướng thêm nhiều trách nhiệm cho nó). |
| Có được global access point đến thể hiện đó | Yêu cầu phải xử lý thêm cả trường hợp đa luồng |
| Đối tượng singleton chỉ được khởi tạo khi nó được sử dụng | Khó viết unit test vì đa số các test framework đều phụ thuộc vào việc kế thừa để tạo ra các mock object. Mà constructor của single class là private và việc ghi đè các phương thức static đối với một số ngôn ngữ là bất khả thi nên chúng ta phải nghĩ cách khác để tạo ra mock object. |