網頁

2018/4/9

Java 設計模式 觀察者模式 Observer Pattern

Observer Pattern(觀察者模式)是屬於設計模式中Behavioral Pattern(行為模式)。當物件(object)間的有一對多(one-to-many)關係時,可利用Observer Pattern來設計。

例如當一個物件的狀態(state)發生變化時,希望所相依的所有物件都可以自動收到通知的狀況。

在GoF的Design Patterns的書中對於Observer Pattern的定義:

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中有兩個角色,一為觀察者(Observer,也可稱為Listener),一為被觀察對象(Observable,也可稱為Subject)。

通常會以現實生活中讀者(Reader)訂閱新聞(News)的例子來解釋Observer Pattern。訂閱新聞時,讀者不用隨時主動地去檢查是否有新的新聞,因為這樣太麻煩了,比較聰明的方法是當有新的新聞時,新聞會自動通知所有訂閱的讀者。在訂閱新聞的例子中,讀者就是Observer,新聞就是Observable,有很多的讀者訂閱某一新聞,所以是一對多關係。


另一個例子就是通訊軟體(例如LINE),你不用隨時檢查群組中是否有新的對話訊息,而是當群組中有新的訊息,你會自動收到通知。加入(訂閱)群組的人很多,而群組只有一個,這就是典型的一對多關係。

在Observer Pattern中,Observer及Observable分別需要實作以下方法。(若您是第一次學習什麼是Observer Pattern,對於下面各方法的解釋看過就好,因為了解設計模式最好還是看範例才容易理解)

Observer物件要有以下三種方法:

  • 可被Observable呼叫的方法(method)。當Observable的狀態改變時,Observable使用此方法來通知Observer。
  • 訂閱(subscribe)Observable的方法。
  • 取消訂閱Observable的方法。

而Observable物件有下面三種方法:

  • 註冊(register)Observer的方法。當Observer訂閱Observable時,Observable使用此方法來註冊Observer。
  • 取消註冊Observer的方法。當Observer取消訂閱Observable時,Observable使用此方法來取消註冊Observer。
  • 通知Observer的方法。當Observable狀態改變時,用來通知所有的Observer。

以下為讀者訂閱新聞的範例程式,強烈建議您打開IDE親手打一打下面的範例。

程式目錄結構


首先要建立兩個Interface,Observable/Subject(被觀察對象)及Observer/Listener(觀察者)。

Observable.java

package idv.matt.observable;

import idv.matt.observer.Observer;

public interface Observable {

  /**
   * 註冊Observer(Reader)
   */
  public void register(Observer reader);
  
  /**
   * 取消註冊Observer(Reader)
   */
  public void unregister(Observer reader);
  
  /**
   * 通知Observer(Reader)
   */
  public void inform();
  
}

Observable定義了以下方法:

  • 註冊Observer的方法register()
  • 取消註冊Observer的方法unregister()
  • 通知所有Observer的方法notify()

Observer.java

package idv.matt.observer;

import idv.matt.observable.Observable;

public interface Observer {
  
  /**
   * 訂閱Observable(News)
   */
  void subscribe(Observable news);
  
  /**
   * 取消訂閱Obervable(News)
   */
  void unsubscribe();
  
  /**
   * 給Observable(News)呼叫的方法
   */
  void update();
  
}

Observer定義了以下方法:

  • 訂閱Observable的方法subscribe()
  • 取消訂閱Observable的方法unsubscribe()
  • Observable用來通知Observer更新的方法update()

實作Observable的新聞類別News

News.java

package idv.matt.observable;

import java.util.ArrayList;

import idv.matt.observer.Observer;

public class News implements Observable {

  private ArrayList<Observer> readerList = new ArrayList<Observer>(); // 紀錄已註冊的Readers
  private boolean latestNews = true; // News的狀態
  
  public boolean isLatestNews() {
    return latestNews;
  }
  
  /**
   * 設定News狀態
   */
  public void setLatestNews(boolean latestNews) {
    this.latestNews = latestNews;
    inform();
  }
  
  /**
   * 註冊Reader
   */
  @Override
  public void register(Observer reader) {
    readerList.add(reader);
  }
  
  /**
   * 取消註冊Reader
   */
  @Override
  public void unregister(Observer reader) {
    readerList.remove(reader);
  }
  
  /**
   * 通知全部的Reader
   */
  @Override
  public void inform() {
    for(Observer reader : readerList){
      reader.update();
    }
  }

}

News實作Observable介面,為被觀察對象:

  • 成員變數readerList中記錄了所有的訂閱讀者,任一讀者只要訂閱了新聞,即會被加入readerList
  • 成員變數latestNews代表News的狀態,若為true表示最新,反之為舊的新聞。setLatestNews()被呼叫時會更改News的狀態,進而執行inform()方法去通知讀者名單readerList中的所有讀者。
  • 註冊方法regsiter(),當讀者訂閱新聞時,該讀者會被加入讀者名單readerList
  • 取消註冊方法unregister(),當讀者取消訂閱時,該讀者會從讀者名單readerList中移除。
  • 通知方法inform(),在News的狀態更動時,會被呼叫來通知並更新所有的讀者。

實作Observer的讀者類別Reader

Reader.java

package idv.matt.observer;

import idv.matt.observable.News;
import idv.matt.observable.Observable;

public class Reader implements Observer {

  private String name = null;     // 讀者的名稱
  private Observable news = null; // 訂閱的News
  
  public Reader(String name){
    this.name = name;
  }
  
  /**
   * 訂閱News
   */
  @Override
  public void subscribe(Observable news) {
    this.news = news;
    news.register(this); // News註冊此Reader
  }
  
  /**
   * 取消訂閱News
   */
  @Override
  public void unsubscribe() {
    news.unregister(this); // News取消註冊此Reader
  }
  
  /**
   * Observer(News)的更新通知
   */
  @Override
  public void update() {
    read(); // 當Reader被News通知時會執行read()
  }

  private void read(){
    if(((News) this.news).isLatestNews()){ // 檢查新聞的狀態
      System.out.println(name + " read latest news");
    } else {
      System.out.println(name + " read old news");
    }
  }
  
}

Reader實作Observer介面,為觀察者

  • 更新方法update(),當News的狀態更新時,News會藉由此方法通知Reader
  • 訂閱方法subscribe(),用來訂閱News。訂閱後則此讀者會被新聞註冊。
  • 取消訂閱方法unsubscribe(),用來取消訂閱News,取消訂閱後則此讀者會被新聞取消註冊。
  • 閱讀方法read(),依照News的狀態來閱讀新聞。

範例的類別圖


最後是測試用的Main

Main.java

package idv.matt.main;

import idv.matt.observable.News;
import idv.matt.observer.Reader;

public class Main {
  
  public static void main(String[] args) {
    
    News news = new News(); // 建立一個新聞(Observable)

    Reader john = new Reader("Matt"); // 建立一些讀者
    Reader mary = new Reader("John");
    Reader bill = new Reader("Lisa");

    // 每個讀者訂閱新聞
    // Observer(Reader)訂閱Observable(News)
    john.subscribe(news); //訂閱新聞
    mary.subscribe(news);
    bill.subscribe(news);

    news.setLatestNews(true); // 更新Observble(News)的狀態
  }

}

執行結果

Matt read latest news
John read latest news
Lisa read latest news

Main中當news的狀態因呼叫setLatestNews()而改變時,所有的讀者都被通知。 若讀者John呼叫unsubscribe()方法取消訂閱新聞,則之後news狀態更動時便不會被通知。

除了Reader類別外,如有其他類別(例如記者(Journalist)類別或是作家(Writer)類別)也需要在News狀態改變時也被通知,只要實作Observer介面並訂閱News即可。

2 則留言:

  1. 謝謝你提供的範例,講解的非常清楚。

    問個小問題,Reader在unsubscribe的時候,有沒有需要把news設成null呢?

    回覆刪除
  2. Hi 樓上,這邊的設計一個Reader只能訂閱一種News,在subscribe()時會更新this.news,所以unsubscribe()時不用特地把this.news設為null。但如果你想要設為null也可,記得放在new.unregister(this)後面。

    回覆刪除