Java中的設計模式(一):觀察者模式

hjavn發表於2021-08-13

一、從“紅燈停,綠燈行”開始

  在汽車界,不論你是迅捷如風的秋名山車神,還是新上崗的馬路殺手,在交通燈前都需要遵守這樣一條鐵律——“紅燈停,綠燈行”。當你坐上駕駛位的那一刻,就註定了你必須隨“燈”而行。

  在上面的場景中出現了兩個角色—— 交通燈駕駛員 ,駕駛員需要觀察交通燈的變色情況(即 變紅變綠 ),根據不同的變色情況作出對應的行駛措施(即 )。這一物件間的行為模式在軟體設計中同樣存在,也就是我們下面要學習的設計模式—— 觀察者模式

二、基本概念

1. 定義

  觀察者模式 (Observer Pattern)是用於建立一種物件和物件之間依賴關係的 物件行為型設計模式 ,其定義為:

在物件之間定義一個一對多的依賴,當一個物件狀態改變時,所有依賴的物件都會自動收到通知。

  在這一定義中明確了兩個物件:

  • 目標物件:即被依賴的物件或被觀察的物件,當狀態發生變更時會通知所有的觀察者物件。在上面的例子中,交通燈就是被觀察的物件;

  • 觀察者物件:即依賴的物件,當觀察的物件狀態發生變更時會自動收到通知,根據收到的通知作出相應的行為(或進行對應狀態的更新操作)。在上面的例子中,駕駛員就是其中的觀察者;

  其結構圖如下:

觀察者模式.jpg

  除此以外,觀察者模式 也被稱為 釋出訂閱模式(Publish-Subscribe Pattern)、 模型-檢視模式 (Model-View Pattern)、 源-監聽器模式 (Source-Listener Pattern)等等。

2. 基於觀察者模式的事件驅動模型

  在實際的程式設計過程中,我們更多的是關注某一事件的發生,比如上面所說的 交通燈變紅/變綠 這樣一個事件,而在發生了交通燈變色之後,汽車才會做出相應的舉措 (停車/啟動) ,這就是 事件驅動模型 ,也稱委派事件模型(Delegation Event Model,DEM)。在事件驅動模型中有以下三個要素:

  • 事件源:即最初發生事件的物件,也對應者觀察者模式中被觀察的目標物件;

  • 事件物件:即被觸發的事件,事件物件需要有能夠執行該事件的主體,即事件源;

  • 事件監聽者:即監聽發生事件的物件,當監聽的對應物件發生某個事件之後,事件監聽者會根據發生的事件做出預先設定好的相應舉措;

  上述所說的事件驅動模型其實是通過觀察者模式來實現的,下面是觀察者模式和事件驅動模型的對應關係:

基於觀察者的事件驅動模型.jpg

  從上圖中可以看到,在事件驅動模型中,事件監聽者就對應著觀察者模式中的觀察者物件,事件源和事件共同組成了被觀察和被處理的目標物件,其中事件源對應著被觀察的目標物件(即事件監聽者會被註冊到事件源上),而發生在事件源上的事件則是需要被事件監聽者處理的物件。

  發生在事件源上的事件實際上是對觀察者模式中的目標物件的狀態變更這一動作的擴充套件,單一的狀態變更無法更好的滿足開發的需要,而事件則具備更好的擴充套件性。

三、原始碼探究

1. JDK中的觀察者模式

  觀察者模式是如此的常用,以至於JDK從1.0版本開始就提供了對該模式的支援。在JDK中提供了 Observable 類和 Observer 介面,前者提供了被觀察物件的基類實現,後者則提供了觀察者的通用處理介面。通過 繼承/實現 這兩個類,開發可以很輕鬆的完成觀察者模式的使用。

  下面具體分析一下 Obserable 類中的 notifyObservers(Object arg) 方法:

  1. public void notifyObservers(Object arg) {
  2. // 區域性變數,用於存放觀察者集合
  3. Object[] arrLocal;
  4. // 這裡對目標物件加鎖,防止獲取目標物件狀態和觀察者集合時出現執行緒安全問題。
  5. // 但是在通知觀察者進行相應處理時則不需要保障執行緒安全。
  6. // 在當前競爭的情況下,最壞的結果如下:
  7. // 1) 一個新加入的觀察者會錯過本地通知;
  8. // 2) 一個最近被登出的觀察者會被錯誤地通知
  9. synchronized (this) {
  10. // 判斷當前目標物件狀態是否變更
  11. if (!changed)
  12. return;
  13. arrLocal = obs.toArray();
  14. // 清除狀態
  15. clearChanged();
  16. }
  17. for (int i = arrLocal.length-1; i>=0; i–)
  18. // 通知所有觀察者進行對應操作
  19. ((Observer)arrLocal[i]).update(this, arg);
  20. }

  從該方法中可以看到想要完成對所有觀察者的通知需要滿足 目標物件狀態改變 這一必要條件。為了保證獲取狀態和觀察者集合時執行緒安全,這裡使用了 synchronized 關鍵字和區域性變數。但是同步程式碼塊並沒有包含呼叫觀察者 update 方法,這就導致了可能會出現有觀察者沒有收到通知或者收到錯誤的通知。

  對於JDK提供的觀察者模式,使用的流程為: Observable.setChanged() -> Observable.notifyObservers(Object arg)

2. JDK中的事件驅動模型

  除了觀察者模式,JDK還實現了對事件驅動模型的支援。為此,JDK提供了 EventObject 類 和 EventListener 介面來支援這一模型。前者代表了事件驅動模型中的 事件物件 ,後者則代表了 事件監聽者

  首先我們來看下 EventObject 的建構函式:

  1. public EventObject(Object source) {
  2. if (source == null)
  3. throw new IllegalArgumentException(“null source”);
  4. this.source = source;
  5. }

  可以看到,在建構函式中必須傳入一個 source 物件,該物件在官方註釋中被定義為最初發生事件的物件。這個解釋乍一看還是有點抽象,結合上面交通燈的例子可能會更好理解一點。

  在交通燈的例子中,交通燈就是 事件源 ,而交通燈變色就是 事件 ,司機就是事件監聽者。司機作為事件監聽者實際觀察的物件是交通燈,當發生交通燈變色事件之後,司機會根據交通燈變色事件進行相應的處理(也就是進行事件的處理)。

  根據上面的邏輯我們不難看到,司機這一事件監聽者實際上是註冊到交通燈這一事件源上,然後去處理交通燈所發生的事件。這裡我們可以看下JDK提供的事件監聽者介面 EventListener ,可以看到這裡只是宣告瞭一個介面,裡面沒有任何的方法。從個人角度來理解,這可能是作者考慮到眾口難調的情況,與其費盡周折想一個通用的方法,不如單純定義一個介面,讓使用者自由發揮。

2. Spring中的事件驅動模型–釋出/訂閱模式

  Spring框架對於事件驅動模型做了資料模型上的進一步明確,在原有的概念上又新增了 事件釋出者 的角色,由此得到了一個新的模式——釋出/訂閱模式。

  在JDK的基礎上,Spring框架提供了 ApplicationEventApplicationListenerApplicationEventPublisher 三個基礎類來支援釋出/訂閱模式。其中 ApplicationEventApplicationListener 分別繼承了 EventObjectEventListener ,其作用也和這兩個類相同,就不再過多贅述。這裡具體關注一下 ApplicationEventPublisher 這個新引入的類,這個新引入的類就對應著上面事件驅動模型中事件源這一角色,區別於JDK中的自由奔放,這裡將事件源定義為了事件釋出者,並提供了一下兩個方法:

  1. @FunctionalInterface
  2. public interface ApplicationEventPublisher {
  3. /**
    • 通知所有註冊到釋出者上面的監聽器進行對應的事件處理
    • @param event 用於釋出的事件,這裡的事件物件必須是ApplicationEvent的基類
  4. */
  5. default void publishEvent(ApplicationEvent event) {
  6. publishEvent((Object) event);
  7. }
  8. /**
    • 通知所有註冊到釋出者上面的監聽器進行對應的事件處理
    • @param event 用於釋出的事件,任意型別事件都可以進行處理
  9. */
  10. void publishEvent(Object event);
  11. }

  可以看到為了保證擴充套件性和自由行,Spring即提供了基於 ApplicationEvent 型別的事件釋出方法,也提供了 Object 型別的事件處理。這裡我們選取 AbstractApplicationContext 這一 ApplicationEvent 的基類來一窺Spring中事件釋出的邏輯:

@Override
public void publishEvent(ApplicationEvent event) {
publishEvent(event, null);
}

protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
Assert.notNull(event, “Event must not be null”);
// 將事件包裝成ApplicationEvent
ApplicationEvent applicationEvent;
if (event instanceof ApplicationEvent) {
applicationEvent = (ApplicationEvent) event;
} else {
applicationEvent = new PayloadApplicationEvent<>(this, event);
if (eventType == null) {
eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
}
}
// 如果可能,現在立即進行多播
// 或一旦初始化多播器就懶惰地進行多播
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
} else {
// 進行事件的廣播,這裡是進行廣播的關鍵
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
}
// 通過父類的context進行事件釋出
if (this.parent != null) {
if (this.parent instanceof AbstractApplicationContext) {
((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
}
else {
this.parent.publishEvent(event);
}
}
}
/**

  • 將事件廣播給對應的監聽者
  • /
    public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
    ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
    Executor executor = getTaskExecutor();
    for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
    if (executor != null) {
    executor.execute(() -> invokeListener(listener, event));
    }
    else {
    invokeListener(listener, event);
    }
    }
    }

  除了事件準備的過程,進行事件廣播通知給對應的監聽者,然後呼叫監聽者對應的方法,這一過程和上面看到過的 Observable 通知監聽器的方法基本相同。但是區別於JDK中的同步處理,Spring中的事件處理如果存線上程池的話,還使用了執行緒池就行非同步處理對應的事件,進一步將釋出者和監聽者做了解耦。

四、總結

  觀察者模式最大的特定是建立了一個一對多且鬆散的耦合關係,觀察目標只需要維持一個抽象觀察者集合,無須感知具體的觀察者有哪些。這樣一個鬆散的耦合關係有利於觀察目標和觀察者各自進行對應的抽象處理,很好的體現了開閉原則。

  當然,觀察者模式也有其弊端,比如只定義了一對多的關係,無法處理多對多的場景;又比如只能感知觀察目標發生了變化,但是具體如何變化卻無法瞭解到,等等。這些都是觀察者模式無法處理的場景或存在的問題。

本文轉自:developer.aliyun.com/article/78690...

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章