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

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ụngLệnh lưu ở đâuLợi ích chính
Hàng đợiHàng đợi chungSan tải, không chặn
Nhật kýNhật ký bền vữngLàm lại sau sập
Nút giao diệnGắn vào nút menu1 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ầuChi tiết
serialize()Mỗi lệnh ghi thành 1 dòng text: SEQ TYPE args
Write-ahead logGhi dòng vào wal.log và fsync trước khi chạy lệnh
CheckpointMỗ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.

  1. 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").
  2. Thêm canExecute(): SaveCommand chỉ bật khi tài liệu có thay đổi. Cả 3 nơi gọi đều hỏi canExecute() trước.
  3. Đổ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.

Bớt paste mù, hiểu sâu từng dòng mình viết.

180 Days Challenge. 180 ngày cai nghiện vibe code.