Java設計模式之(十二)——觀察者模式

YSOcean發表於2021-12-01

1、什麼是觀察者模式?

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

觀察者模式(Observer Design Pattern):在物件之間定義一個一對多的依賴,當一個物件狀態改變的時候,所有依賴的物件都會得到通知並自動更新。

說人話:也叫釋出訂閱模式,能夠很好的解耦一個物件改變,自動改變另一個物件這種情況。

2、觀察者模式定義

image-20210919214718188

①、Subject 被觀察者

定義被觀察者必須實現的職責, 它必須能夠動態地增加、 取消觀察者。 它一般是抽象類或者是實現類, 僅僅完成作為被觀察者必須實現的職責: 管理觀察者並通知觀察者。

②、Observer觀察者

觀察者接收到訊息後, 即進行update(更新方法) 操作, 對接收到的資訊進行處理。

③、ConcreteSubject具體的被觀察者

定義被觀察者自己的業務邏輯, 同時定義對哪些事件進行通知。

④、ConcreteObserver具體的觀察者

每個觀察在接收到訊息後的處理反應是不同, 各個觀察者有自己的處理邏輯。

3、觀察者模式通用程式碼

/**
 * 觀察者
 */
public interface Observer {
    // 更新方法
    void update();
}
/**
 * 具體觀察者
 */
public class ConcreteObserver implements Observer{
    @Override
    public void update() {
        System.out.println("接受到資訊,並進行處理");
    }
}
/**
 * 被觀察者
 */
public abstract class Subject {
    // 定義一個被觀察者陣列
    private List<Observer> obsList = new ArrayList<>();

    // 增加一個觀察者
    public void addObserver(Observer observer){
        obsList.add(observer);
    }

    // 刪除一個觀察者
    public void delObserver(Observer observer){
        obsList.remove(observer);
    }

    // 通知所有觀察者
    public void notifyObservers(){
        for (Observer observer : obsList){
            observer.update();
        }
    }
}
/**
 * 具體被觀察者
 */
public class ConcreteSubject extends Subject{
    // 具體的業務
    public void doSomething(){
        super.notifyObservers();
    }
}
public class ObserverClient {

    public static void main(String[] args) {
        // 建立一個被觀察者
        ConcreteSubject subject = new ConcreteSubject();
        // 定義一個觀察者
        Observer observer = new ConcreteObserver();
        // 觀察者觀察被觀察者
        subject.addObserver(observer);
        subject.doSomething();
    }
}

4、JDK 實現

在 JDK 的 java.util 包下,已經為我們提供了觀察者模式的抽象實現,感興趣的可以看看,內部邏輯其實和我們上面介紹的差不多。

觀察者 java.util.Observer

image-20210920164737899

被觀察者 java.util.Observable

image-20210920165032781

image-20210920165205123

5、例項

使用者進行註冊,註冊完成之後,會發一封歡迎郵件。

5.1 普通實現

image-20210920220940735
public class UserController {

    public void register(String userName, String passWord){
        // 1、根據使用者名稱密碼儲存在資料庫
        Long userId = saveUser(userName, passWord);
        // 2、如果上一步有結果則傳送一封歡迎郵件
        if(userId != null){
            Mail.sendEmail(userId);
        }
    }


    public Long saveUser(String userName, String passWord){
        return 1L;
    }
}

上面的註冊介面實現了兩件事,註冊和傳送郵件,很明顯違反了單一職責原則,但假設這個註冊需求是不是經常變動的,這樣寫也沒有什麼問題,但是假如需求變動,比如不僅要傳送郵件,還得傳送簡訊,那還這樣寫,那register介面會變得很複雜。

那應該如何簡化呢?沒錯,就是觀察者模式。

image-20210920221425379

5.2 觀察者模式實現

我們直接套用 JDK 的實現。

import java.util.Observable;

/**
 * 使用者登入——被觀察者
 */
public class UserControllerObservable extends Observable {

    public void register(String userName, String passWord){
        // 1、根據使用者名稱密碼儲存在資料庫
        Long userId = saveUser(userName, passWord);
        // 2、如果上一步有結果則通知所有觀察者
        if(userId != null){
            super.setChanged();
            super.notifyObservers(userName);
        }
    }

    public Long saveUser(String userName, String passWord){
        return 1L;
    }

}
import java.util.Observable;
import java.util.Observer;

/**
 * 傳送郵件——觀察者
 */
public class MailObserver implements Observer {

    @Override
    public void update(Observable o, Object arg) {
        System.out.println("傳送郵件:" + arg + "歡迎你");
    }
}
/**
 * 傳送手機簡訊——觀察者
 */
public class SMSObserver implements Observer {

    @Override
    public void update(Observable o, Object arg) {
        System.out.println("傳送簡訊:" + arg + "歡迎你");
    }
}

測試:

public class UserClient {
    public static void main(String[] args) {
        UserControllerObservable observable = new UserControllerObservable();
        observable.addObserver(new MailObserver());
        observable.addObserver(new SMSObserver());
        observable.register("張三","123");
    }
}

image-20210920224858256

通過觀察者模式改寫後,後面使用者註冊,就算在增加別的操作,我們也只需要增加一個觀察者即可,而註冊介面 register 不會有任何改動。

5.3 非同步模式優化

在回到前面那張圖:

image-20210920221425379

註冊之後進行的兩步操作:傳送郵件和傳送簡訊,上面我們通過觀察者模式改寫之後,雖然流程很清晰,但是我們發現是順序執行的,但其實這兩步操作沒有先後順序,於是,我們可以改成非同步模式,增加執行效率。

/**
 * 傳送郵件——觀察者
 */
public class MailObserver implements Observer {
    
    private Executor executor = Executors.newFixedThreadPool(2);

    @Override
    public void update(Observable o, Object arg) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("傳送郵件:" + arg + "歡迎你");
            }
        });

    }
}

5、EventBus

翻譯為“事件匯流排”,它提供了實現觀察者模式的骨架程式碼。我們可以基於此框架,非常容易地在自己的業務場景中實現觀察者模式,不需要從零開始開發。其中,Google Guava EventBus 就是一個比較著名的 EventBus 框架,它不僅僅支援非同步非阻塞模式,同時也支援同步阻塞模式。

PS:Google Guava 是一個特別好用的工具包,裡面的程式碼也都實現的比較優雅,大家感興趣的可以研究研究原始碼。

https://github.com/google/guava

下面我們以上面的例子來說明如何使用 EventBus:

①、導如 Guava 包

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1.1-jre</version>
</dependency>

②、具體程式碼如下:

import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.EventBus;

import java.util.List;
import java.util.concurrent.Executors;

public class UserController {
    private EventBus eventBus;

    public UserController(){
        eventBus = new AsyncEventBus(Executors.newFixedThreadPool(2));
    }

    /**
     * 注意:泛型引數是 Object,而不是介面 Observer
     * @param observerList
     */
    public void setObserverList(List<Object> observerList){
        for(Object observer : observerList){
            eventBus.register(observer);
        }
    }

    public void register(String userName, String passWord){
        // 1、根據使用者名稱密碼儲存在資料庫
        Long userId = saveUser(userName, passWord);
        // 2、如果上一步有結果則通知所有觀察者
        if(userId != null){
            eventBus.post(userName);
        }
    }


    public Long saveUser(String userName, String passWord){
        return 1L;
    }
}
import com.google.common.eventbus.Subscribe;

/**
 * 傳送郵件——觀察者
 */
public class MailObserver{

    @Subscribe
    public void sendMail(String userName) {
        System.out.println("傳送郵件:" + userName + "歡迎你");
    }
}
import com.google.common.eventbus.Subscribe;

/**
 * 傳送手機簡訊——觀察者
 */
public class SMSObserver{

    @Subscribe
    public void sendSMS(String userName) {
        System.out.println("傳送簡訊:" + userName + "歡迎你");
    }
}

測試:

public class EventBusClient {
    public static void main(String[] args) {
        UserController userController = new UserController();
        List<Object> observerList = new ArrayList<>();
        observerList.add(new MailObserver());
        observerList.add(new SMSObserver());
        userController.setObserverList(observerList);
        userController.register("張三","123");
    }
}

利用 EventBus 框架實現的觀察者模式,跟從零開始編寫的觀察者模式相比,從大的流程上來說,實現思路大致一樣,都需要定義 Observer,並且通過 register() 函式註冊 Observer,也都需要通過呼叫某個函式(比如,EventBus 中的 post() 函式)來給 Observer 傳送訊息(在 EventBus 中訊息被稱作事件 event)。但在實現細節方面,它們又有些區別。基於 EventBus,我們不需要定義 Observer 介面,任意型別的物件都可以註冊到 EventBus 中,通過 @Subscribe 註解來標明類中哪個函式可以接收被觀察者傳送的訊息。

6、觀察者模式優點

①、觀察者和被觀察者之間是抽象耦合

不管是增加觀察者還是被觀察者都非常容易擴充套件,在系統擴充套件方面會得心應手。

②、建立一套觸發機制

被觀察者變化引起觀察者自動變化。但是需要注意的是,一個被觀察者,多個觀察者,Java的訊息通知預設是順序執行的,如果一個觀察者卡住,會導致整個流程卡住,這就是同步阻塞。

所以實際開發中沒有先後順序的考慮使用非同步,非同步非阻塞除了能夠實現程式碼解耦,還能充分利用硬體資源,提高程式碼的執行效率。

另外還有程式間的觀察者模式,通常基於訊息佇列來實現,用於實現不同程式間的觀察者和被觀察者之間的互動。

7、觀察者模式應用場景

①、關聯行為場景。

②、事件多級觸發場景。

③、跨系統的訊息交換場景, 如訊息佇列的處理機制。

相關文章