Hướng dẫn Java Design Pattern – Observer

Chúng ta không thể nói về Lập trình hướng đối tượng mà không xem xét trạng thái của các đối tượng. Tất cả các chương trình hướng đối tượng là về các đối tượng và sự tương tác của chúng. Trong trường hợp khi một số đối tượng nhất định cần được thông báo thường xuyên về những thay đổi xảy ra trong các đối tượng khác. Để có một thiết kế tốt có nghĩa là tách rời càng nhiều càng tốt và giảm sự phụ thuộc. Mẫu thiết kế Observer (quan sát) có thể được sử dụng bất cứ khi nào mà một đối tượng có sự thay đổi trạng thái, tất các thành phần phụ thuộc của nó sẽ được thông báo và cập nhật một cách tự động.

1. Observer Pattern là gì?

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Observer Pattern là một trong những Pattern thuộc nhóm hành vi (Behavior Pattern). Nó định nghĩa mối phụ thuộc một – nhiều giữa các đối tượng để khi mà một đối tượng có sự thay đổi trạng thái, tất các thành phần phụ thuộc của nó sẽ được thông báo và cập nhật một cách tự động.

Observer có thể đăng ký với hệ thống. Khi hệ thống có sự thay đổi, hệ thống sẽ thông báo cho Observer biết. Khi không cần nữa, mẫu Observer sẽ được gỡ khỏi hệ thống.

  • Hình 1-8, cho phép observer thứ 1 đăng ký với hệ thống.
  • Hình 1-9, cho phép observer thứ 2 đăng ký vớ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.

Observer Pattern còn gọi là DependentsPublish/Subcribe hoặc Source/Listener.

2. Cài đặt Observer Pattern như thế nào?

Các thành phần tham gia Observer Pattern:

  • Subject : chứa danh sách các observer,  cung cấp phương thức để có thể 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 ConcreateObserver, 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ý.

2.1. 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 về 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, sẽ thực hiện gửi mail thông báo khi tài khoản hết hạn, thực hiệ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 : cung cấp các phương thức để thêm, loại bỏ, thông báo observer.
  • AccountService : đóng vai trò là ConcreteSubject, sẽ thông báo tới tất cả các observers bất cứ khi nào có thao tác của người dùng liên quan đến đăng nhập, tài khoản hết hạn.
  • 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. Phương thức này chấp nhận đối số là SubjectState, cho phép các ConcreteObserver sử dụng dữ liệu của nó.
  • LoggerMailer và Protector là các ConcreteObserver. Sau khi nhận được thông báo rằng có thao tác với user và gọi tới phương thức update(), các ConcreteObserver sẽ sử dụng dữ liệu SubjectState để xử lý.

Subject.java

package com.maixuanviet.patterns.behavioral.observer.account;
 
public interface Subject {
 
    void attach(Observer observer);
 
    void detach(Observer observer);
 
    void notifyAllObserver();
}

AccountService.java

package com.maixuanviet.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<Observer> 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@maixuanviet.com".equalsIgnoreCase(user.getEmail());
    }
}

Observer.java

package com.maixuanviet.patterns.behavioral.observer.account;
 
public interface Observer {
    void update(User user);
}

Logger.java

package com.maixuanviet.patterns.behavioral.observer.account;
 
public class Logger implements Observer {
 
    @Override
    public void update(User user) {
        System.out.println("Logger: " + user);
    }
}

Mailer.java

package com.maixuanviet.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.maixuanviet.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.maixuanviet.patterns.behavioral.observer.account;
 
public class ObserverPatternExample {
 
    public static void main(String[] args) {
        AccountService account1 = createAccount("contact@maixuanviet.com", "127.0.0.1");
        account1.login();
        account1.changeStatus(LoginStatus.EXPIRED);
 
        System.out.println("---");
        AccountService account2 = createAccount("contact@maixuanviet.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@maixuanviet.com, ip=127.0.0.1, status=SUCCESS)
Status is changed
Logger: User(email=contact@maixuanviet.com, ip=127.0.0.1, status=EXPIRED)
Mailer: User contact@maixuanviet.com is expired. An email was sent!
---
Login is handled
Logger: User(email=contact@maixuanviet.com, ip=116.108.77.231, status=INVALID)
Protector: User contact@maixuanviet.com is invalid. IP 116.108.77.231 is blocked

3. Lợi ích của Observer Pattern là gì?

Lợi ích:

  • 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 ở 1 đố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 (Unexpected update) 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.

4. Sử dụng Observer Pattern khi nào?

  • Thường được sử dụng trong mối quan hệ 1-n 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.