Menu
CoddyTech

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 đề.

challenge icon

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 GameSettings cầ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", 50 và 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ượng
    • setFullscreen(bool fs): cập nhật chế độ toàn màn hình
    • displaySettings(): 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 applyGamePreset nhận tên cấu hình sẵn (chuỗi). Hàm này cần truy cập singleton GameSettings và 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ành 70, chế độ toàn màn hình thành false
    • "competitive": đặt độ khó thành "Hard", âm lượng thành 30, chế độ toàn màn hình thành true
    • 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:

    1. Tên cấu hình sẵn (chuỗi)
    2. 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 applyGamePreset rồ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: no

Với đầu vào competitive và 20:

Difficulty: Normal
Volume: 50
Fullscreen: no

Difficulty: Hard
Volume: 30
Fullscreen: yes

Difficulty: Hard
Volume: 20
Fullscreen: yes

Lư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;
}
quiz iconTự kiểm tra

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

Tự luyện tập: Trình biên dịch C++ online