Observer is a behavioral design pattern that lets you define a subscription mechanism to notify multiple objects about any events that happen to the object they’re observing.

Problem

Giả sử ta có hai loại đối tượng là customer và store. Customer quan tâm đến một sản phẩm cụ thể nào đó mà sẽ có hàng ở store trong vài ngày tới.

Để biết khi nào có hàng, customer có thể đi đến store mỗi ngày để kiểm tra. Tất nhiên, việc này gây ra một sự bất tiện không hề nhỏ cho customer.

Một giải pháp khác là store sẽ gửi thông báo (spam) đến cho tất cả các customer khi có hàng. Tuy nhiên, chỉ những customer quan tâm đến sản phẩm mới không thấy khó chịu bởi thông báo này. Khi đó, store chỉ làm vừa lòng một vài customer nhưng lại gây mất thiện cảm với các customer khác.

Solution

Để giải quyết vấn đề trên, ta cần có một cơ chế gửi thông báo đến những khách hàng nào mà có quan tâm đến sản phẩm.

Ta gọi object gửi thông báo là subject và các object quan tâm là observer. Bất cứ khi nào trạng thái của subject thay đổi thì nó sẽ gửi thông báo đến các observer đang lắng nghe.

Tất cả các observer đều sẽ cần phải tuân theo một interface và subject sẽ lưu một mảng các đối tượng thuộc interface này.

Structure

Observer pattern sẽ bao gồm những thành phần sau:

  1. Subject: là một interface chứa:
    • Danh sách các observer.
    • Phương thức dùng để gửi cập nhật đến các observer.
    • Các phương thức để thêm và loại bỏ observer.
  2. Observer: là một interface có phương thức update giúp observer nhận và xử lý cập nhật từ subject.
  3. Concrete subject: implementation của subject.
  4. Concrete observer: implement các đoạn code xử lý cập nhật từ subject.

Minh họa:

Mỗi khi có sự thay đổi trạng thái bên trong subject, nó sẽ duyệt qua danh sách các observer và gọi thực hiện phương thức update của chúng.

Implementation

Giả sử ta cần xây dựng một hệ thống theo dõi tài khoản của người dùng cho phép ghi chú lại những thao tác của người dùng, bao gồm cả khi email của người dùng hết hạn hoặc IP của người dùng không hợp lệ.

Sơ đồ lớp:

Phân tích:

  1. Concrete subject là AccountService, có nhiệm vụ gửi thông báo tới tất cả các observer đang lắng nghe khi người dùng thực hiện một thao tác nào đó.
  2. Observer có phương thức update nhận vào đối số là một user.
  3. Các concrete observer: Logger, MailerProtector.

User and Login Status

Trước tiên, tạo ra lớp để biểu diễn một người dùng như sau:

class User {
private:
	std::string _email;
	std::string _ip;
	std::string _status;
 
public:
	std::string getEmail() { return _email; }
	std::string getIp() { return _ip; }
	std::string getStatus() { return _status; }
 
	void setEmail(std::string email) { _email = email; }
	void setIp(std::string ip) { _ip = ip; }
	void setStatus(std::string status) { _status = status; }
 
public:
	std::string toString() {
		std::stringstream ss;
		ss << "User(email=" << _email << ", ip=" << _ip << ", status=" << _status << ")";
		return ss.str();
	}
};

Đồng thời tạo ra một class có chứa các chuỗi status:

class LoginStatus {
public:
	inline static std::string SUCCESS = "SUCCESS";
	inline static std::string FAILURE = "FAILURE";
	inline static std::string INVALID = "INVALID";
	inline static std::string EXPIRED = "EXPIRED";
};

Observer

Thiết lập interface của observer đơn giản như sau:

class Observer {
public:
	virtual void update(User* user) = 0;
};

Subject

Thiết lập interface của subject:

class Subject {
public:
	virtual void attach(Observer* observer) = 0;
	virtual void dettach(Observer* observer) = 0;
	virtual void notify() = 0;
};

Concrete Subject

Xây dựng lớp AccountService như sau:

class AccountService : public Subject {
private:
	User* _user = nullptr;
	std::vector<Observer*> _observers;
 
public:
	AccountService(std::string email, std::string ip) {
		_user = new User();
		_user->setEmail(email);
		_user->setIp(ip);
	}
 
public:
	void attach(Observer* observer) {
		for (Observer* existingObserver : _observers) {
			if (existingObserver == observer) {
				return;
			}
		}
 
		_observers.push_back(observer);
	}
 
	void dettach(Observer* observer) {
		for (Observer* existingObserver : _observers) {
			if (existingObserver == observer) {
				_observers.erase(std::remove(_observers.begin(), _observers.end(), observer), _observers.end());
			}
		}
	}
 
	void notify() {
		for (Observer* observer : _observers) {
			observer->update(_user);
		}
	}
 
public:
	void changeStatus(std::string newStatus) {
		_user->setStatus(newStatus);
 
		std::cout << "Status is changed\n";
		notify();
	}
 
	void login() {
		if (!isValidIP()) {
			_user->setStatus(LoginStatus::INVALID);
		}
		else if (isValidEmail()) {
			_user->setStatus(LoginStatus::SUCCESS);
		}
		else {
			_user->setStatus(LoginStatus::FAILURE);
		}
 
		std::cout << "Login is handled\n";
		notify();
	}
 
private:
	bool isValidIP() {
		return "127.0.0.1" == _user->getIp();
	}
 
	bool isValidEmail() {
		return "contact@email.com" == _user->getEmail();
	}
};

Xây dựng lớp AccountService như sau:

Đầu tiên, tạo ra hai thuộc tính giúp lưu tham chiếu đến người dùng hiện tại và danh sách các observer:

class AccountService : public Subject {
private:
	User* _user = nullptr;
	std::vector<Observer*> _observers;
 
//...
}

Implement ba phương thức của interface Subject:

class AccountService : public Subject {
//...
 
public:
	void attach(Observer* observer) {
		for (Observer* existingObserver : _observers) {
			if (existingObserver == observer) {
				return;
			}
		}
 
		_observers.push_back(observer);
	}
 
	void dettach(Observer* observer) {
		for (Observer* existingObserver : _observers) {
			if (existingObserver == observer) {
				_observers.erase(std::remove(_observers.begin(), _observers.end(), observer), _observers.end());
			}
		}
	}
 
	void notify() {
		for (Observer* observer : _observers) {
			observer->update(_user);
		}
	}
 
//...
}

Tạo ra các phương thức giúp thay kiểm tra và thay đổi trạng thái của account:

class AccountService : public Subject {
//...
 
public:
	void changeStatus(std::string newStatus) {
		_user->setStatus(newStatus);
 
		std::cout << "Status is changed\n";
		notify();
	}
 
	void login() {
		if (!isValidIP()) {
			_user->setStatus(LoginStatus::INVALID);
		}
		else if (isValidEmail()) {
			_user->setStatus(LoginStatus::SUCCESS);
		}
		else {
			_user->setStatus(LoginStatus::FAILURE);
		}
 
		std::cout << "Login is handled\n";
		notify();
	}
 
private:
	bool isValidIP() {
		return "127.0.0.1" == _user->getIp();
	}
 
	bool isValidEmail() {
		return "contact@email.com" == _user->getEmail();
	}
}

Concrete Observers

Xây dựng các concrete observer giúp triển khai interface Observer:

class Logger : public Observer {
public:
	void update(User* user) {
		std::cout << "Logger: " << user->toString() << "\n";
	}
};
 
class Mailer : public Observer {
public:
	void update(User* user) {
		if (user->getStatus() == LoginStatus::EXPIRED) {
			std::cout << "Mailer: User " << user->getEmail() << " is expired. An email was sent!\n";
		}
	}
};
 
class Protector : public Observer {
public:
	void update(User* user) {
		if (user->getStatus() == LoginStatus::INVALID) {
			std::cout << "Protector: User " << user->getEmail() << " is invalid. "
				<< "IP " << user->getIp() << " is blocked\n";
		}
	}
};

Usage

Sử dụng như sau:

void TestObserver() {
	AccountService* account1 = new AccountService("contact@email.com", "127.0.0.1");
	account1->attach(new Logger());
	account1->attach(new Mailer());
	account1->attach(new Protector());
 
	account1->login();
	account1->changeStatus(LoginStatus::EXPIRED);
}

Output:

Login is handled
Logger: User(email=contact@email.com, ip=127.0.0.1, status=SUCCESS)
Status is changed
Logger: User(email=contact@email.com, ip=127.0.0.1, status=EXPIRED)
Mailer: User contact@email.com is expired. An email was sent!

Applicability

Sử dụng observer pattern khi:

  • Việc thay đổi trạng thái của một đối tượng yêu cầu sự thay đổi của những đối tượng khác và tập các đối tượng không được biết trước.
  • Một số đối tượng theo dõi các đối tượng khác trong một số trường hợp cụ thể nào đó.

Info

Observer được áp dụng trong việc thông báo sự thay đổi dữ liệu đến cho giao diện ở trong [[CSharp|C#]].

Pros and Cons

ProsCons
Open/closed: có thể thêm vào các observer class mà không làm thay đổi subject codeCác observer được thông báo random
Có thể thiết lập mối quan hệ giữa các đối tượng trong runtime

Resources