Menu
CoddyTech

RAII như một mẫu thiết kế

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 102 trên 104.

RAII (Resource Acquisition Is Initialization) không chỉ là một thành ngữ trong C++, mà còn là một mẫu thiết kế mạnh mẽ gắn việc quản lý tài nguyên với vòng đời của đối tượng. Bạn đã tìm hiểu về RAII qua con trỏ thông minh, nhưng mẫu thiết kế này áp dụng cho mọi loại tài nguyên: handle tệp, kết nối mạng, mutex hoặc giao dịch cơ sở dữ liệu.

Ý tưởng cốt lõi rất đơn giản: lấy tài nguyên trong hàm khởi tạo (constructor), giải phóng tài nguyên trong hàm hủy (destructor). Vì C++ bảo đảm hàm hủy chạy khi đối tượng rời khỏi phạm vi, việc dọn dẹp diễn ra tự động, ngay cả khi có ngoại lệ:

#include <iostream>
#include <fstream>

class FileGuard {
    std::ofstream file;
public:
    FileGuard(const std::string& filename) : file(filename) {
        if (!file.is_open()) {
            std::cout << "Failed to open file\n";
        }
    }
    
    void write(const std::string& text) {
        if (file.is_open()) file << text;
    }
    
    ~FileGuard() {
        if (file.is_open()) {
            file.close();
            std::cout << "File closed automatically\n";
        }
    }
};

int main() {
    {
        FileGuard guard("output.txt");
        guard.write("Hello RAII");
    }  // Hàm hủy được gọi tại đây, tệp đã đóng
    
    std::cout << "After scope\n";
}

RAII phát huy hiệu quả khi quản lý khóa trong code đa luồng. std::lock_guard của thư viện chuẩn tuân theo mô hình này: nó lấy mutex khi khởi tạo và nhả mutex khi bị hủy, giúp tránh deadlock do quên mở khóa.

Khi triển khai các lớp RAII, hãy nhớ xóa hoặc triển khai đúng các thao tác sao chép/di chuyển (Quy tắc năm) để tránh nhân đôi tài nguyên hoặc giải phóng tài nguyên hai lần. RAII chuyển việc quản lý tài nguyên thủ công dễ phát sinh lỗi thành quy trình dọn dẹp an toàn, tự động.

challenge icon

Thử thách

Dễ

Hãy xây dựng một trình quản lý nhóm kết nối bằng RAII để quản lý an toàn các kết nối cơ sở dữ liệu. Trong các ứng dụng thực tế, kết nối cơ sở dữ liệu là tài nguyên tốn kém, cần được lấy và giải phóng đúng cách. Bạn sẽ tạo một lớp bọc RAII bảo đảm các kết nối luôn được trả về nhóm, ngay cả khi xảy ra ngoại lệ hoặc luồng thực thi trở nên phức tạp.

Bạn sẽ tổ chức code trong ba tệp:

  • ConnectionPool.h: Tạo một nhóm kết nối đơn giản để quản lý số lượng kết nối có giới hạn.

    Lớp ConnectionPool cần theo dõi số kết nối hiện có (bắt đầu bằng sức chứa được truyền vào hàm khởi tạo). Hãy triển khai:

    • acquire(): nếu còn kết nối, giảm số lượng và in ra Connection acquired (X available), trong đó X là số kết nối còn lại; trả về true nếu thành công, false nếu không còn kết nối
    • release(): tăng số kết nối hiện có và in ra Connection released (X available)
    • available(): trả về số kết nối hiện có
  • ConnectionGuard.h: Tạo lớp bọc RAII để quản lý an toàn một kết nối.

    Lớp ConnectionGuard thể hiện mẫu RAII. Lớp này cần:

    • Nhận tham chiếu đến một ConnectionPool trong hàm khởi tạo và thử lấy một kết nối
    • Lưu lại thông tin việc lấy kết nối có thành công hay không
    • Cung cấp phương thức isConnected() để kiểm tra trình quản lý có giữ một kết nối hợp lệ hay không
    • Tự động trả kết nối về nhóm trong hàm hủy, nhưng chỉ khi đã lấy được kết nối
    • Xóa hàm khởi tạo sao chép và toán tử gán sao chép để tránh nhân đôi tài nguyên (cần cân nhắc Quy tắc năm)

    Khi hàm hủy chạy, nếu đang giữ một kết nối, hãy in ra Guard releasing connection trước khi gọi hàm release của nhóm.

  • main.cpp: Minh họa cách RAII tự động dọn dẹp tài nguyên thông qua phạm vi.

    Đọc hai đầu vào:

    1. Sức chứa của nhóm kết nối (số nguyên)
    2. Số lượng kết nối cần yêu cầu (số nguyên)

    Tạo một ConnectionPool với sức chứa đã cho. Sau đó, trong một phạm vi lồng nhau (dùng dấu ngoặc nhọn), tạo số lượng đối tượng ConnectionGuard theo yêu cầu và lưu chúng trong một vector. Với mỗi trình quản lý, hãy in ra việc kết nối có thành công hay không:

    • Nếu đã kết nối: Guard N: Connected
    • Nếu chưa kết nối: Guard N: Failed to connect

    (trong đó N bắt đầu từ 1)

    Sau khi kết thúc phạm vi (các trình quản lý bị hủy), hãy in ra After scope: X connections available để hiển thị trạng thái cuối cùng của nhóm.

Ví dụ, với đầu vào 2 và 3:

Connection acquired (1 available)
Guard 1: Connected
Connection acquired (0 available)
Guard 2: Connected
Guard 3: Failed to connect
Guard releasing connection
Connection released (1 available)
Guard releasing connection
Connection released (2 available)
After scope: 2 connections available

Với đầu vào 3 và 2:

Connection acquired (2 available)
Guard 1: Connected
Connection acquired (1 available)
Guard 2: Connected
Guard releasing connection
Connection released (2 available)
Guard releasing connection
Connection released (3 available)
After scope: 3 connections available

Hãy để ý cách các kết nối tự động được giải phóng khi các trình quản lý ra khỏi phạm vi. Bạn không cần gọi release một cách tường minh trong code chính. Các hàm hủy chạy theo thứ tự ngược với thứ tự khởi tạo, trình quản lý được tạo cuối cùng sẽ bị hủy trước, và mọi kết nối đã lấy đều được bảo đảm sẽ trả về nhóm. Đó là sức mạnh của RAII: việc dọn dẹp tài nguyên diễn ra tự động và đáng tin cậy, bất kể phạm vi kết thúc như thế nào.

Tự làm thử

#include <iostream>
#include <vector>
#include "ConnectionPool.h"
#include "ConnectionGuard.h"

using namespace std;

int main() {
    // Đọc dữ liệu đầu vào
    int capacity;
    int numConnections;
    cin >> capacity;
    cin >> numConnections;
    
    // TODO: Tạo một ConnectionPool với dung lượng đã cho
    
    // TODO: Tạo một phạm vi (scope) lồng nhau bằng dấu ngoặc nhọn
    {
        // TODO: Tạo một vector để lưu các đối tượng (object) ConnectionGuard
        // Gợi ý: Bạn cần dùng con trỏ hoặc con trỏ thông minh vì ConnectionGuard
        // đã xóa hàm khởi tạo sao chép
        
        // TODO: Dùng vòng lặp (loop) để tạo numConnections guard
        // Với mỗi guard, in ra một trong hai chuỗi sau:
        // "Guard N: Connected" hoặc "Guard N: Failed to connect"
        // trong đó N bắt đầu từ 1
        
    }
    // Các guard bị hủy tại đây khi phạm vi kết thúc
    
    // TODO: In ra "After scope: X connections available"
    
    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