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.
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
ConnectionPoolcầ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 raConnection acquired (X available), trong đó X là số kết nối còn lại; trả vềtruenếu thành công,falsenếu không còn kết nốirelease(): tăng số kết nối hiện có và in raConnection 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
ConnectionGuardthể hiện mẫu RAII. Lớp này cần:- Nhận tham chiếu đến một
ConnectionPooltrong 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 connectiontrước khi gọi hàm release của nhóm.- Nhận tham chiếu đến một
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:
- Sức chứa của nhóm kết nối (số nguyên)
- Số lượng kết nối cần yêu cầu (số nguyên)
Tạo một
ConnectionPoolvớ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ượngConnectionGuardtheo 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 availableVớ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 availableHã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;
}
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ên2Quả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ọc11Các khái niệm OOP nâng cao
Kết hợp so với kế thừaMixin qua CRTPMẫu PimplXóa kiểuLớp enum và định kiểu mạnhXử lý ngoại lệ trong OOPPhân cấp ngoại lệ tùy chỉnh14Mẫu thiết kế phần 2
Mẫu lệnhMẫu bộ chuyển đổiMẫu trang tríMẫu phương thức khuôn mẫuMẫu trạng tháiMẫu tổng hợpRAII như một mẫu thiết kế3Hà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