Command thực tế: hàng đợi, logging và ActionListener
Lệnh bay vào hàng đợi yêu cầu, ghi nhật ký để phục hồi sau sự cố, và nút bấm giao diện với ActionListener.
KhóDesign pattern ~60 phút
Ôn lại trước khi học
Giới thiệu
Bài này học 3 ứng dụng thực tế của Command. Bạn dùng nó cho hàng đợi in và phục hồi sau sập.
Kiến thức cần nhớ
Hàng đợi yêu cầu
- Bên gửi thả lệnh vào hàng đợi rồi làm việc khác.
- Bên xử lý lấy dần từng lệnh ra chạy.
- Lợi ích là san tải giờ cao điểm, bên gửi không bị chặn.
- Ví dụ đời thường: như xếp hàng in, gửi xong đi làm việc khác.
Ghi nhật ký phục hồi
- Mọi lệnh đổi dữ liệu đều ghi vào nhật ký bền vững trước khi chạy.
- Sập thì đọc nhật ký và chạy lại từ điểm kiểm tra gần nhất.
- Lệnh phải tự chứa đủ thông tin để chạy lại độc lập.
- Bốn nhịp: ghi, chạy, đánh dấu xong, chốt điểm kiểm tra.
Nút giao diện
- Nút là bên gọi, lập trình viên gắn lệnh vào nút.
- Cùng 1 lệnh gắn được vào nút, menu và phím tắt.
- Đổi logic 1 lần thì cả 3 đường cùng đổi.
- Đây chính là Command mà dân giao diện dùng hằng ngày.
| Ứng dụng | Lệnh lưu ở đâu | Lợi ích chính |
|---|---|---|
| Hàng đợi | Hàng đợi chung | San tải, không chặn |
| Nhật ký | Nhật ký bền vững | Làm lại sau sập |
| Nút giao diện | Gắn vào nút menu | 1 lệnh nhiều đường gọi |
Ví dụ
- Input: 3 máy gửi thả 10 lệnh vào hàng đợi. Output: cả 10 chạy đủ, không mất không lặp.
- Giải thích: bên gửi không chờ, bên xử lý lấy dần theo thứ tự.
- Input: sập giữa chừng ở lệnh 100, điểm kiểm tra ở 80. Output: chạy lại từ 81 tới 100.
- Giải thích: đoạn trước 80 đã chốt nên bỏ qua.
💻 Bài tập
Ngôn ngữ: C++17 (có std::thread, std::condition_variable). Nộp vào day-082/.
Bài 1: Hàng đợi lệnh nhiều luồng
class CommandQueue {
public:
void push(std::unique_ptr<Command> c); // an toàn luồng
std::unique_ptr<Command> pop(); // chờ khi rỗng; trả nullptr khi đã shutdown và rỗng
void shutdown();
};
class WorkerPool { public: WorkerPool(CommandQueue &q, int nThreads); void join(); };
IncrementCommand(std::atomic<int>&) tăng biến đếm 1.
3 luồng gửi, mỗi luồng 10.000 lệnh; 4 worker
-> sau shutdown + join: counter = 30.000 (không mất, không lặp)
Chạy dưới -fsanitize=thread.
Bài 2: Nhật ký và khôi phục sau sập
Kho số dư tài khoản đơn giản std::map<std::string, long long>. Lệnh: Deposit(acc, amount), Withdraw(acc, amount), Transfer(from, to, amount).
| Yêu cầu | Chi tiết |
|---|---|
serialize() | Mỗi lệnh ghi thành 1 dòng text: SEQ TYPE args |
| Write-ahead log | Ghi dòng vào wal.log và fsync trước khi chạy lệnh |
| Checkpoint | Mỗi 100 lệnh, ghi toàn bộ số dư vào snapshot.txt kèm SEQ cuối |
recover() | Nạp snapshot, chạy lại các dòng WAL có SEQ lớn hơn |
Chạy 250 lệnh, giết tiến trình (hoặc gọi std::abort) ở lệnh 173
recover() -> số dư giống hệt khi chạy 173 lệnh không sập
Snapshot ở 100, chỉ chạy lại 73 lệnh
Bài 3: Một lệnh, ba nơi gọi (nâng cao)
Mô phỏng trình soạn thảo có Document. Viết SaveCommand, BoldCommand, FindCommand.
- Gắn cùng một đối tượng lệnh vào: nút toolbar, mục menu, phím tắt (bảng
std::map<std::string, Command*>như"Ctrl+S"). - Thêm
canExecute():SaveCommandchỉ bật khi tài liệu có thay đổi. Cả 3 nơi gọi đều hỏicanExecute()trước. - Đổi logic
SaveCommand(thêm ghi file backup). Chứng minh cả 3 nơi đổi theo mà không sửa chúng.
✅ Tiêu chí hoàn thành
- Bài 1 counter đúng, sanitizer luồng sạch.
- Bài 2 khôi phục đúng sau sập ở vị trí ngẫu nhiên (chạy 20 lần).
- Bài 3 cả 3 nơi dùng chung 1 đối tượng lệnh.
- Giải thích được vì sao phải ghi log trước khi chạy lệnh.
❓ Câu hỏi ôn tập
- Hàng đợi tách bên gửi và xử lý thế nào?
- Nhật ký giúp phục hồi sau sập ra sao?
- Vì sao nút giao diện chính là Command?
- Lệnh trong nhật ký cần chứa gì?
🌕 Bạn đã hoàn thành ngày 82. Làm hết bài tập phía trên rồi sang ngày tiếp theo.