Observer: publisher-subscriber và loose coupling

Định nghĩa Observer, quan hệ một-nhiều, nguyên lý loose coupling và câu chuyện trạm thời tiết với ba bảng hiển thị.
Trung bìnhDesign pattern ~60 phút

Giới thiệu

Bài này học cách một đổi lan tới nhiều bên. Bạn tách trạm đo khỏi bảng hiển thị.

Kiến thức cần nhớ

Observer một-nhiều

  • Một chủ đề đổi, nhiều người nghe cập nhật.
  • Chủ đề còn gọi là nhà xuất bản.
  • Người nghe còn gọi là người đăng ký.
  • Ví dụ đời thường: kênh video ra clip, fan nhận tin.

Trạm thời tiết và 3 bảng

  • Trạm giữ số đo, 3 bảng đăng ký nghe.
  • Số đổi thì trạm báo, mỗi bảng tự vẽ lại.
  • Trạm không biết bảng vẽ kiểu gì.
  • Ví dụ đời thường: loa phường phát, ai nghe thì nghe.

Lỏng ràng buộc và vòng đời

  • Hai bên biết về nhau càng ít càng tốt.
  • Thêm bảng mới không sửa trạm cũ.
  • Vòng đời: đăng ký, báo đổi, hủy đăng ký.
  • Ví dụ đời thường: theo dõi rồi hủy theo dõi.
Cách làmThêm bảng mới
Gọi trực tiếpMở trạm ra sửa
Hỏi liên tụcTốn tài nguyên
ObserverChỉ cần đăng ký

Ví dụ

  • Input: 3 bảng đã đăng ký, số mới về. Output: cả 3 cùng làm mới đúng số.
  • Giải thích: trạm duyệt danh sách và báo từng người.

💻 Bài tập

Ngôn ngữ: C++17 (hoặc Java). Nộp vào day-009/.

Bài 1: Trạm thời tiết

struct Observer { virtual void update(float temp, float humidity, float pressure) = 0; virtual ~Observer() = default; };
struct Subject {
  virtual void registerObserver(Observer *o) = 0;
  virtual void removeObserver(Observer *o) = 0;
  virtual void notifyObservers() = 0;
  virtual ~Subject() = default;
};
LớpViệc phải làm
WeatherData : SubjectGiữ số đo. setMeasurements(t, h, p) lưu rồi gọi notifyObservers()
CurrentConditionsDisplayIn "Hiện tại: 26.5C, 65% ẩm"
StatisticsDisplayGiữ min, max, trung bình nhiệt độ
ForecastDisplayÁp suất tăng: "Trời đẹp", giảm: "Sắp mưa", giữ nguyên: "Như cũ"

Ràng buộc

  • WeatherData không biết tên lớp display nào.
  • Đăng ký trùng một observer thì bỏ qua.
  • Hủy đăng ký trong lúc đang notify không được làm crash.
setMeasurements(26, 65, 1012)  -> Current: 26C 65%; Stats: avg 26 min 26 max 26; Forecast: Trời đẹp
setMeasurements(28, 70, 1010)  -> Stats: avg 27 min 26 max 28; Forecast: Sắp mưa
removeObserver(forecast); setMeasurements(25, 90, 1010) -> chỉ 2 bảng in

Bài 2: Đơn hàng gọi thẳng sang Observer

Code ban đầu:

void Order::complete() {
  status = "done";
  inventory.decrease(items);
  mailer.sendReceipt(customerEmail);
  analytics.track("order_done", id);
}

Yêu cầu: Order chỉ phát sự kiện OrderCompleted{id, items, email}. Ba module tự đăng ký. Thêm LoyaltyPoints (cộng 1 điểm mỗi 100.000đ) mà không sửa Order.

Bài 3: Phòng đấu giá (nâng cao)

Auction là subject. Bidder là observer, mỗi người có ngân sách tối đa và bước giá riêng.

  1. Khi có giá mới, Auction báo mọi bidder trừ người vừa trả giá.
  2. Bidder tự quyết: nếu giá hiện tại + bước <= ngân sách thì trả giá, ngược lại rút lui (hủy đăng ký).
  3. Phiên kết thúc khi không ai trả giá thêm. In người thắng và giá.
Giá khởi điểm 100. An(ngân sách 300, bước 20), Bình(250, 50), Chi(180, 10)
-> In từng lượt trả giá, ai rút lui lúc nào, người thắng cuối cùng.

Tránh đệ quy vô hạn: bidder trả giá trong update sẽ kích hoạt notify mới. Dùng hàng đợi sự kiện hoặc vòng lặp theo lượt.

✅ Tiêu chí hoàn thành

  • Test mẫu Bài 1 in đúng từng dòng.
  • Bài 2: thêm LoyaltyPoints chỉ tạo file mới.
  • Bài 3 kết thúc đúng, không đệ quy vô hạn, không crash khi hủy đăng ký giữa chừng.
  • Giải thích được "loose coupling" bằng ví dụ trong code của mình.

❓ Câu hỏi ôn tập

  • Observer là gì, quan hệ mấy-nhiều?
  • Trạm thời tiết gồm những ai?
  • Lỏng ràng buộc nghĩa là gì?
  • Đăng ký rồi hủy hoạt động thế nào?

🌕 Bạn đã hoàn thành ngày 9. 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.