Mẫu Observer
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 94 trên 104.
Mẫu thiết kế Observer pattern xác định mối quan hệ phụ thuộc một nhiều giữa các đối tượng: khi một đối tượng (đối tượng subject) thay đổi trạng thái, tất cả đối tượng phụ thuộc vào nó (các observers) sẽ tự động nhận thông báo. Mẫu này rất phù hợp với các hệ thống sự kiện, cập nhật giao diện người dùng hoặc mọi tình huống cần nhiều đối tượng phản ứng với các thay đổi.
Mẫu này gồm hai vai trò chính: Subject duy trì danh sách các observer và thông báo cho họ, còn Observers triển khai một giao diện cập nhật:
#include <iostream>
#include <vector>
#include <algorithm>
class Observer {
public:
virtual void update(int value) = 0;
virtual ~Observer() = default;
};
class Subject {
std::vector<Observer*> observers;
int state = 0;
public:
void attach(Observer* obs) {
observers.push_back(obs);
}
void detach(Observer* obs) {
observers.erase(
std::remove(observers.begin(), observers.end(), obs),
observers.end());
}
void setState(int value) {
state = value;
notify();
}
void notify() {
for (Observer* obs : observers) {
obs->update(state);
}
}
};
class Display : public Observer {
std::string name;
public:
Display(const std::string& n) : name(n) {}
void update(int value) override {
std::cout << name << " received: " << value << "\n";
}
};
int main() {
Subject sensor;
Display screen1("Screen1"), screen2("Screen2");
sensor.attach(&screen1);
sensor.attach(&screen2);
sensor.setState(42); // Cả hai màn hình đều nhận được thông báo
}Khi gọi setState(), đối tượng chủ thể lần lượt gọi phương thức update() của tất cả đối tượng quan sát đã đăng ký. Bạn có thể thêm hoặc gỡ đối tượng quan sát bất cứ lúc nào, giúp hệ thống linh hoạt và ít phụ thuộc lẫn nhau.
Dùng Observer khi thay đổi ở một đối tượng cần cập nhật các đối tượng khác và bạn không muốn các đối tượng này phụ thuộc chặt chẽ vào nhau.
Thử thách
DễHãy xây dựng một hệ thống giám sát trạm thời tiết bằng mẫu Observer. Bạn sẽ tạo một trạm thời tiết theo dõi dữ liệu nhiệt độ và tự động thông báo cho nhiều thiết bị hiển thị mỗi khi nhiệt độ thay đổi, một ứng dụng thực tế kinh điển của mẫu này.
Bạn sẽ sắp xếp code trong ba tệp:
Observer.h: Định nghĩa giao diện observer và một observer hiển thị cụ thể.Tạo lớp
Observertrừu tượng với phương thức ảo thuần túyupdate(double temperature)để các observer triển khai và nhận thông báo, cùng với hàm hủy ảo.Sau đó, tạo lớp
TemperatureDisplaykế thừa từObserver. Mỗi thiết bị hiển thị có một tên (chuỗi) được thiết lập qua hàm khởi tạo. Khi gọiupdate(), phương thức này sẽ in ra:[DisplayName]: Temperature is [temperature] degreesWeatherStation.h: Tạo đối tượng chủ thể lưu giữ nhiệt độ và thông báo cho các observer.Lớp
WeatherStationcần:- Lưu danh sách con trỏ đến observer và nhiệt độ hiện tại (khởi tạo bằng
0.0) - Có phương thức
attach(Observer* obs)để đăng ký observer - Có phương thức
detach(Observer* obs)để xóa observer - Có phương thức
setTemperature(double temp)để cập nhật nhiệt độ và thông báo cho tất cả observer đã đăng ký - Có phương thức riêng tư
notify()gọiupdate()trên từng observer với nhiệt độ hiện tại
- Lưu danh sách con trỏ đến observer và nhiệt độ hiện tại (khởi tạo bằng
main.cpp: Minh họa cách mẫu Observer hoạt động.Đọc ba đầu vào:
- Tên thiết bị hiển thị thứ nhất (chuỗi)
- Tên thiết bị hiển thị thứ hai (chuỗi)
- Một giá trị nhiệt độ (số thực)
Tạo một
WeatherStationvà hai đối tượngTemperatureDisplayvới tên đã nhập. Gắn cả hai thiết bị hiển thị vào trạm, rồi đặt nhiệt độ bằng giá trị đầu vào. Cả hai thiết bị hiển thị sẽ tự động nhận thông báo cập nhật và in thông báo của mình.Sau đó, gỡ thiết bị hiển thị thứ nhất khỏi trạm và đặt nhiệt độ mới cao hơn giá trị đầu vào 5 độ. Chỉ thiết bị hiển thị thứ hai nhận được thông báo cập nhật này.
Ví dụ, với các đầu vào Kitchen, Bedroom và 22.5:
Kitchen: Temperature is 22.5 degrees
Bedroom: Temperature is 22.5 degrees
Bedroom: Temperature is 27.5 degreesVới các đầu vào Office, Lobby và 18.0:
Office: Temperature is 18 degrees
Lobby: Temperature is 18 degrees
Lobby: Temperature is 23 degreesHãy chú ý cách mẫu Observer tạo ra một hệ thống ít phụ thuộc chặt chẽ: trạm thời tiết không cần biết gì về từng thiết bị hiển thị cụ thể, mà chỉ cần thông báo cho những thiết bị đang lắng nghe. Khi bạn gỡ một observer, observer đó sẽ tự động ngừng nhận thông báo cập nhật.
Tự làm thử
#include <iostream>
#include <string>
#include "WeatherStation.h"
using namespace std;
int main() {
// Đọc dữ liệu đầu vào
string display1Name, display2Name;
double temperature;
cin >> display1Name;
cin >> display2Name;
cin >> temperature;
// TODO: Tạo một đối tượng WeatherStation
// TODO: Tạo hai đối tượng TemperatureDisplay với các tên đầu vào
// TODO: Gắn cả hai màn hình hiển thị vào trạm thời tiết
// TODO: Đặt nhiệt độ bằng giá trị đầu vào
// (Cả hai màn hình hiển thị đều phải in ra thông báo)
// TODO: Tách màn hình hiển thị thứ nhất
// TODO: Đặt nhiệt độ cao hơn đầu vào 5 độ
// (Chỉ màn hình hiển thị thứ hai in ra thông báo)
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ên10Tổng quan về STL
Tổng quan và triết lý của STLCác vùng chứa của STLBộ lặpCác thuật toán của STLHàm tử và biểu thức lambdaÔn tập: tần suất từ13Mẫu thiết kế phần 1
Giới thiệu về mẫu thiết kếMẫu SingletonFactory và Abstract FactoryMẫu BuilderMẫu ObserverMẫu Strategy2Quả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ọc3Hà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