Mẫu Singleton
Thuộc phần Lập trình hướng đối tượng trong hành trình C++ của Coddy. Bài học 91 trên 104.
Mẫu Singleton đảm bảo một lớp chỉ có một đối tượng trong toàn bộ chương trình và cung cấp một điểm truy cập toàn cục đến đối tượng đó. Mẫu này hữu ích với những tài nguyên cần được chia sẻ, như trình quản lý cấu hình, trình ghi nhật ký hoặc nhóm kết nối cơ sở dữ liệu.
Ý tưởng chính là đặt hàm khởi tạo ở chế độ riêng tư để không ai có thể trực tiếp tạo instance, sau đó cung cấp một phương thức tĩnh trả về instance duy nhất:
#include <iostream>
#include <string>
class Logger {
private:
std::string logFile;
// Hàm khởi tạo (constructor) riêng tư, ngăn tạo instance trực tiếp
Logger() : logFile("app.log") {}
// Xóa hàm khởi tạo sao chép và toán tử (operator) gán
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
public:
// Phương thức (method) tĩnh để truy cập instance duy nhất
static Logger& getInstance() {
static Logger instance; // Chỉ tạo một lần, tồn tại đến khi chương trình kết thúc
return instance;
}
void log(const std::string& message) {
std::cout << "[LOG] " << message << "\n";
}
};
int main() {
Logger::getInstance().log("Application started");
Logger& logger = Logger::getInstance();
logger.log("Processing data");
}static Logger instance bên trong getInstance() là một tính năng của C++11 có tên là “magic statics”, tính năng này an toàn với đa luồng và đảm bảo chỉ khởi tạo một lần. Bằng cách xóa hàm khởi tạo bản sao và toán tử gán, chúng ta ngăn không cho bất kỳ ai tạo bản sao của singleton.
Hãy dùng Singleton một cách hạn chế, vì nó tạo ra trạng thái toàn cục, khiến việc kiểm thử khó hơn. Singleton phù hợp nhất với các tài nguyên thực sự được chia sẻ, khi việc tạo nhiều đối tượng có thể gây ra vấn đề.
Thử thách
DễHãy tạo một singleton GameSettings để quản lý cấu hình cho ứng dụng trò chơi. Đây là trường hợp rất phù hợp để dùng mẫu Singleton. Bạn cần đúng một trình quản lý cài đặt mà mọi phần trong trò chơi đều có thể truy cập, nhờ đó cấu hình luôn nhất quán.
Bạn sẽ sắp xếp code trong ba tệp:
GameSettings.h: Định nghĩa lớp singleton để quản lý cấu hình trò chơi.Lớp
GameSettingscần lưu ba cài đặt dưới dạng thành viên riêng tư:difficulty(chuỗi),volume(số nguyên từ 0-100) vàfullscreen(giá trị boolean). Khởi tạo chúng bằng các giá trị mặc định:"Normal",50vàfalse.Triển khai mẫu Singleton đúng cách:
- Đặt hàm khởi tạo ở phạm vi riêng tư
- Xóa hàm tạo bản sao và toán tử gán
- Cung cấp phương thức
getInstance()tĩnh trả về tham chiếu đến instance duy nhất
Thêm các phương thức công khai sau để thao tác với cài đặt:
setDifficulty(const std::string& diff): cập nhật độ khósetVolume(int vol): cập nhật âm lượngsetFullscreen(bool fs): cập nhật chế độ toàn màn hìnhdisplaySettings(): in ra tất cả cài đặt hiện tại
GameModule.h: Tạo một module riêng để truy cập singleton.Định nghĩa một hàm có tên
applyGamePresetnhận tên cấu hình sẵn (chuỗi). Hàm này cần truy cập singletonGameSettingsvà thiết lập cài đặt dựa trên cấu hình:"casual": đặt độ khó thành"Easy", âm lượng thành70, chế độ toàn màn hình thànhfalse"competitive": đặt độ khó thành"Hard", âm lượng thành30, chế độ toàn màn hình thànhtrue- Các cấu hình khác: giữ nguyên cài đặt mặc định
Điều này cho thấy các phần khác nhau của chương trình có thể truy cập và chỉnh sửa cùng một instance singleton.
main.cpp: Kết hợp mọi thứ để minh họa cách singleton hoạt động.Đọc hai đầu vào:
- Tên cấu hình sẵn (chuỗi)
- Mức âm lượng tùy chỉnh (số nguyên)
Trước tiên, gọi
displaySettings()trên singleton để hiển thị các cài đặt mặc định ban đầu. In một dòng trống sau đó.Tiếp theo, áp dụng cấu hình bằng hàm
applyGamePresetrồi hiển thị lại các cài đặt. In một dòng trống sau đó.Cuối cùng, chỉ thay đổi âm lượng bằng giá trị đầu vào tùy chỉnh và hiển thị các cài đặt cuối cùng.
Phương thức displaySettings() cần xuất ra theo đúng định dạng sau:
Difficulty: [value]
Volume: [value]
Fullscreen: [yes/no]Ví dụ, với đầu vào casual và 85:
Difficulty: Normal
Volume: 50
Fullscreen: no
Difficulty: Easy
Volume: 70
Fullscreen: no
Difficulty: Easy
Volume: 85
Fullscreen: noVới đầu vào competitive và 20:
Difficulty: Normal
Volume: 50
Fullscreen: no
Difficulty: Hard
Volume: 30
Fullscreen: yes
Difficulty: Hard
Volume: 20
Fullscreen: yesLưu ý rằng cả main.cpp và GameModule.h đều truy cập cùng một instance singleton: thay đổi ở một nơi sẽ hiển thị ở mọi nơi. Đó là sức mạnh của mẫu Singleton khi quản lý các tài nguyên dùng chung như cài đặt trò chơi.
Tự làm thử
#include <iostream>
#include <string>
#include "GameSettings.h"
#include "GameModule.h"
using namespace std;
int main() {
// Đọc dữ liệu đầu vào
string presetName;
int customVolume;
cin >> presetName;
cin >> customVolume;
// TODO: Lấy instance singleton của GameSettings
// TODO: Hiển thị các thiết lập mặc định ban đầu
// TODO: In ra một dòng trống
// TODO: Áp dụng cấu hình có sẵn bằng hàm (function) applyGamePreset
// TODO: Hiển thị các thiết lập sau khi áp dụng cấu hình có sẵn
// TODO: In ra một dòng trống
// TODO: Ghi đè âm lượng bằng customVolume
// TODO: Hiển thị các thiết lập cuối cùng
return 0;
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Nền tảng về OOP
Tệp bên ngoàiBiên dịch và tạo bản dựng C++Tệp header và tệp nguồnNamespace và phạm viGiới thiệu về OOP trong C++Lớp và đối tượngCon trỏ thisPhương thức (hàm thành viên)Thuộc tính (dữ liệu thành viên)Cơ bản về hàm khởi tạo và hàm hủyÔn tập: máy tính đơn giản4Thuộc tính của lớp
Thành viên đối tượng và thành viên tĩnhHàm truy xuất và hàm thiết lậpHàm thành viên hằngTừ khóa mutablePhương thức và biến tĩnhHàm và lớp bạnÔn tập: Trình quản lý tài khoản ngân hàng7Kế thừa
Kế thừa cơ bảnMức truy cập trong kế thừaThứ tự gọi hàm khởi tạo và hàm hủyGhi đè phương thứcHàm ảo và VTableĐa kế thừaKế thừa ảoÔn tập: hệ phân cấp nhân viên10Tổng quan về STL
Tổng quan và triết lý của STLCác vùng chứa của STLBộ lặpCác thuật toán của STLHàm tử và biểu thức lambdaÔn tập: tần suất từ13Mẫu thiết kế phần 1
Giới thiệu về mẫu thiết kếMẫu SingletonFactory và Abstract FactoryMẫu BuilderMẫu ObserverMẫu Strategy2Quản lý bộ nhớ
Bộ nhớ ngăn xếp và heapCon trỏ và tham chiếuBộ nhớ động (new/delete)Con trỏ thông minh trong C++RAII trong C++Ôn tập: Trình quản lý mảng động5Tính đóng gói
Phạm vi truy cập trong C++Tìm hiểu sâu về phạm vi truy cậpẨn thông tinStruct và lớpLớp lồng nhau và lớp bên trongÔn tập: hệ thống hồ sơ sinh viên8Đa hình
Đa hình lúc biên dịch và khi chạyNạp chồng hàmÔn lại hàm ảoHàm ảo thuần túyLớp trừu tượngThiết kế interface trong C++Ép kiểu động và RTTIÔn tập: Máy tính hình học3Hàm khởi tạo và hàm hủy
Hàm khởi tạo mặc địnhHàm khởi tạo có tham sốHàm khởi tạo sao chépHàm khởi tạo di chuyểnDanh sách khởi tạoHàm khởi tạo ủy quyềnTìm hiểu sâu về hàm hủyQuy tắc 3 / 5 / 0Ôn tập: lớp chuỗi6Nạp chồng toán tử
Giới thiệu về nạp chồng toán tửNạp chồng toán tử số họcNạp chồng toán tử so sánhToán tử luồngNạp chồng toán tử gánNạp chồng toán tử [] và ()Toán tử chuyển đổi kiểuÔn tập: lớp ma trận9Mẫu
Mẫu hàmMẫu lớpChuyên biệt hóa mẫuMẫu biến tham sốCơ bản về SFINAE và đặc điểm kiểuÔn tập: vùng chứa tổng quátTự luyện tập: Trình biên dịch C++ online