天天看點

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

Java中的設計模式(一):觀察者模式
人生苦短,不如養狗

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

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

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

二、基本概念

1. 定義

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

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

  在這一定義中明确了兩個對象:

  • 目标對象

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

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

  其結構圖如下:

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

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

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

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

  • 事件源

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

    :即被觸發的事件,事件對象需要有能夠執行該事件的主體,即事件源;
  • 事件監聽者

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

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

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

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

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

三、源碼探究

1. JDK中的觀察者模式

  觀察者模式是如此的常用,以至于JDK從1.0版本開始就提供了對該模式的支援。在JDK中提供了

Observable

類和

Observer

接口,前者提供了被觀察對象的基類實作,後者則提供了觀察者的通用處理接口。通過 繼承/實作 這兩個類,開發可以很輕松的完成觀察者模式的使用。

  下面具體分析一下

Obserable

類中的

notifyObservers(Object arg)

方法:

public void notifyObservers(Object arg) {
        // 局部變量,用于存放觀察者集合
        Object[] arrLocal;
        // 這裡對目标對象加鎖,防止擷取目标對象狀态和觀察者集合時出現線程安全問題。
        // 但是在通知觀察者進行相應處理時則不需要保障線程安全。
        // 在目前競争的情況下,最壞的結果如下:
        // 1) 一個新加入的觀察者會錯過本地通知;
        // 2) 一個最近被登出的觀察者會被錯誤地通知
        synchronized (this) {
            // 判斷目前目标對象狀态是否變更
            if (!changed)
                return;
            arrLocal = obs.toArray();
            // 清除狀态
            clearChanged();
        }
        for (int i = arrLocal.length-1; i>=0; i--)
            // 通知所有觀察者進行對應操作
            ((Observer)arrLocal[i]).update(this, arg);
 }      

  從該方法中可以看到想要完成對所有觀察者的通知需要滿足 目标對象狀态改變 這一必要條件。為了保證擷取狀态和觀察者集合時線程安全,這裡使用了

synchronized

關鍵字和局部變量。但是同步代碼塊并沒有包含調用觀察者

update

方法,這就導緻了可能會出現有觀察者沒有收到通知或者收到錯誤的通知。

  對于JDK提供的觀察者模式,使用的流程為:

Observable.setChanged()

->

Observable.notifyObservers(Object arg)

2. JDK中的事件驅動模型

  除了觀察者模式,JDK還實作了對事件驅動模型的支援。為此,JDK提供了

EventObject

類 和

EventListener

接口來支援這一模型。前者代表了事件驅動模型中的 事件對象 ,後者則代表了 事件監聽者 。

  首先我們來看下

EventObject

的構造函數:

public EventObject(Object source) {
        if (source == null)
            throw new IllegalArgumentException("null source");
        this.source = source;
 }      

  可以看到,在構造函數中必須傳入一個

source

對象,該對象在官方注釋中被定義為最初發生事件的對象。這個解釋乍一看還是有點抽象,結合上面交通燈的例子可能會更好了解一點。

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

  根據上面的邏輯我們不難看到,司機這一事件監聽者實際上是注冊到交通燈這一事件源上,然後去處理交通燈所發生的事件。這裡我們可以看下JDK提供的事件監聽者接口

EventListener

,可以看到這裡隻是聲明了一個接口,裡面沒有任何的方法。從個人角度來了解,這可能是作者考慮到衆口難調的情況,與其費盡周折想一個通用的方法,不如單純定義一個接口,讓使用者自由發揮。

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

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

  在JDK的基礎上,Spring架構提供了

ApplicationEvent

ApplicationListener

ApplicationEventPublisher

三個基礎類來支援釋出/訂閱模式。其中

ApplicationEvent

ApplicationListener

分别繼承了

EventObject

EventListener

,其作用也和這兩個類相同,就不再過多贅述。這裡具體關注一下

ApplicationEventPublisher

這個新引入的類,這個新引入的類就對應着上面事件驅動模型中事件源這一角色,差別于JDK中的自由奔放,這裡将事件源定義為了事件釋出者,并提供了一下兩個方法:

@FunctionalInterface
public interface ApplicationEventPublisher {
  /**
   * 通知所有注冊到釋出者上面的監聽器進行對應的事件處理
   *
   * @param event 用于釋出的事件,這裡的事件對象必須是ApplicationEvent的基類
   */
  default void publishEvent(ApplicationEvent event) {
    publishEvent((Object) event);
  }
  /**
   * 通知所有注冊到釋出者上面的監聽器進行對應的事件處理
   * 
   * @param event 用于釋出的事件,任意類型事件都可以進行處理
   */
  void publishEvent(Object event);
}      

  可以看到為了保證擴充性和自由行,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中的事件處理如果存線上程池的話,還使用了線程池就行異步處理對應的事件,進一步将釋出者和監聽者做了解耦。

四、總結

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

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