Trạng thái của các đối tượng là rất quan trọng trong lập trình hướng đối tượng. Mẫu thiết kế Observer được sử dụng khi một đối tượng cần thông báo về sự thay đổi trạng thái cho các đối tượng phụ thuộc của nó.
Observer Pattern là gì?
Mẫu thiết kế Observer là một trong những mẫu hành vi trong lập trình. Nó xác định một mối quan hệ một-nhiều giữa các đối tượng, trong đó khi một đối tượng thay đổi trạng thái, tất cả các đối tượng phụ thuộc của nó sẽ được tự động thông báo và cập nhật.
Observer có thể đăng ký với hệ thống và khi hệ thống có sự thay đổi, nó sẽ nhận được thông báo. Khi không cần thiết nữa, Observer có thể bị gỡ bỏ khỏi hệ thống.
Hiện tại, hệ thống đang liên lạc với 2 observer: Observer 1 và Observer 2. Khi hệ thống phát sinh một sự kiện cụ thể nào đó, nó sẽ thông báo (notification) với cả 2 observer như hình số 1-10.
Mẫu thiết kế Observer còn có tên gọi khác là Dependents, Publish/Subscribe hoặc Source/Listener.
Cài đặt Observer Pattern như thế nào?
Các thành phần tham gia trong Observer Pattern gồm:
- Subject: Chứa danh sách các observer, cung cấp phương thức để thêm và loại bỏ observer.
- Observer: Định nghĩa một phương thức update() cho các đối tượng sẽ được subject thông báo đến khi có sự thay đổi trạng thái.
- ConcreteSubject: Cài đặt các phương thức của Subject, lưu trữ trạng thái danh sách các ConcreteObserver, gửi thông báo đến các observer của nó khi có sự thay đổi trạng thái.
- ConcreteObserver: Cài đặt các phương thức của Observer, lưu trữ trạng thái của subject, thực thi việc cập nhật để giữ cho trạng thái đồng nhất với subject gửi thông báo đến.
Sự tương tác giữa subject và các observer như sau: mỗi khi subject có sự thay đổi trạng thái, nó sẽ duyệt qua danh sách các observer của nó và gọi phương thức cập nhật trạng thái ở từng observer, có thể truyền chính nó vào phương thức để các observer có thể lấy ra trạng thái của nó và xử lý.
Ví dụ Observer Pattern với ứng dụng Tracking thao tác một Account
Giả sử hệ thống của chúng ta cần theo dõi tài khoản của người dùng. Mọi thao tác của người dùng đều cần được ghi log lại, gửi mail thông báo khi tài khoản hết hạn, chặn người dùng nếu truy cập không hợp lệ,...
Chương trình của chúng ta như sau:
// Subject.java
package com.gpcoder.patterns.behavioral.observer.account;
public interface Subject {
void attach(Observer observer);
void detach(Observer observer);
void notifyAllObserver();
}
// AccountService.java
package com.gpcoder.patterns.behavioral.observer.account;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
enum LoginStatus {
SUCCESS, FAILURE, INVALID, EXPIRED
}
@Data
class User {
private String email;
private String ip;
private LoginStatus status;
}
public class AccountService implements Subject {
private User user;
private List observers = new ArrayList>();
public AccountService(String email, String ip) {
user = new User();
user.setEmail(email);
user.setIp(ip);
}
@Override
public void attach(Observer observer) {
if (!observers.contains(observer))
observers.add(observer);
}
@Override
public void detach(Observer observer) {
if (observers.contains(observer)) {
observers.remove(observer);
}
}
@Override
public void notifyAllObserver() {
for (Observer observer : observers) {
observer.update(user);
}
}
public void changeStatus(LoginStatus status) {
user.setStatus(status);
System.out.println("Status is changed");
this.notifyAllObserver();
}
public void login() {
if (!this.isValidIP()) {
user.setStatus(LoginStatus.INVALID);
} else if (this.isValidEmail()) {
user.setStatus(LoginStatus.SUCCESS);
} else {
user.setStatus(LoginStatus.FAILURE);
}
System.out.println("Login is handled");
this.notifyAllObserver();
}
private boolean isValidIP() {
return "127.0.0.1".equals(user.getIp());
}
private boolean isValidEmail() {
return "contact@gpcoder.com".equalsIgnoreCase(user.getEmail());
}
}
// Observer.java
package com.gpcoder.patterns.behavioral.observer.account;
public interface Observer {
void update(User user);
}
// Logger.java
package com.gpcoder.patterns.behavioral.observer.account;
public class Logger implements Observer {
@Override
public void update(User user) {
System.out.println("Logger: " + user);
}
}
// Mailer.java
package com.gpcoder.patterns.behavioral.observer.account;
public class Mailer implements Observer {
@Override
public void update(User user) {
if (user.getStatus() == LoginStatus.EXPIRED) {
System.out.println("Mailer: User " + user.getEmail() + " is expired. An email was sent!");
}
}
}
// Protector.java
package com.gpcoder.patterns.behavioral.observer.account;
public class Protector implements Observer {
@Override
public void update(User user) {
if (user.getStatus() == LoginStatus.INVALID) {
System.out.println("Protector: User " + user.getEmail() + " is invalid. "
+ "IP " + user.getIp() + " is blocked");
}
}
}
// ObserverPatternExample.java
package com.gpcoder.patterns.behavioral.observer.account;
public class ObserverPatternExample {
public static void main(String[] args) {
AccountService account1 = createAccount("contact@gpcoder.com", "127.0.0.1");
account1.login();
account1.changeStatus(LoginStatus.EXPIRED);
System.out.println("-");
AccountService account2 = createAccount("contact@gpcoder.com", "116.108.77.231");
account2.login();
}
private static AccountService createAccount(String email, String ip) {
AccountService account = new AccountService(email, ip);
account.attach(new Logger());
account.attach(new Mailer());
account.attach(new Protector());
return account;
}
}
Output của chương trình:
Login is handled
Logger: User(email=contact@gpcoder.com, ip=127.0.0.1, status=SUCCESS)
Status is changed
Logger: User(email=contact@gpcoder.com, ip=127.0.0.1, status=EXPIRED)
Mailer: User contact@gpcoder.com is expired. An email was sent!
-
Login is handled
Logger: User(email=contact@gpcoder.com, ip=116.108.77.231, status=INVALID)
Protector: User contact@gpcoder.com is invalid. IP 116.108.77.231 is blocked
Lợi ích của Observer Pattern là gì?
- Dễ dàng mở rộng với ít sự thay đổi: Mẫu này cho phép thay đổi Subject và Observer một cách độc lập. Chúng ta có thể tái sử dụng các Subject mà không cần tái sử dụng các Observer và ngược lại. Nó cho phép thêm Observer mà không sửa đổi Subject hoặc Observer khác. Vì vậy, nó đảm bảo nguyên tắc Open/Closed Principle (OCP).
- Sự thay đổi trạng thái ở một đối tượng có thể được thông báo đến các đối tượng khác mà không phải giữ chúng liên kết quá chặt chẽ.
- Một đối tượng có thể thông báo đến một số lượng không giới hạn các đối tượng khác.
Bên cạnh những lợi ích, chúng ta cần xem xét đến trường hợp cập nhật không mong muốn của Subject. Bởi vì các Observer không biết về sự hiện diện của nhau, nó có thể gây tốn nhiều chi phí của việc thay đổi Subject.
Sử dụng Observer Pattern khi nào?
- Thường được sử dụng trong mối quan hệ một-nhiều giữa các object với nhau. Trong đó, một đối tượng thay đổi và muốn thông báo cho tất cả các object liên quan biết về sự thay đổi đó.
- Khi thay đổi một đối tượng, yêu cầu thay đổi đối tượng khác và chúng ta không biết có bao nhiêu đối tượng cần thay đổi, những đối tượng này là ai.
- Sử dụng trong ứng dụng broadcast-type communication.
- Sử dụng để quản lý sự kiện (Event management).
- Sử dụng trong mẫu mô hình MVC (Model View Controller Pattern): trong MVC, mẫu này được sử dụng để tách Model khỏi View. View đại diện cho Observer và Model là đối tượng Observable.
Tài liệu tham khảo:
- https://sourcemaking.com/design_patterns/observer
- https://refactoring.guru/design-patterns/observer
- https://www.baeldung.com/java-observer-pattern
- https://pawelgrzybek.com/the-observer-pattern-in-javascript-explained/
- Design Patterns: Elements of Reusable Object-Oriented Software - GOF
- Design Pattern for dummies