Java - 觀察者模式(Observer)範例

偶然間讀到歐萊禮的Java網路程式設計一書,在講解Thread時有提到當下的thread執行run

method時產生一編碼資料並存進陣列內,但是在run還沒有執行完的當下,另一區塊的程式

卻因為該陣列內尚沒有字串的資料而讀取使用導致NullPointerException,因此有應用到

Observer的技巧來巧妙的避開這個問題。

後來發現原來Java API的util有相關Observer、Observable可使用,因此在這邊也撰寫了一範例

來套用看看相關功能。

首先觀察者模式的建構,會定義被觀察者觀察者,為一對多的關係,即被觀察者可能被多

個觀察者給觀察,當被觀察者呼叫setChanged()及notifyObservers(Object obj)後,各觀察者即

可以透過override的update(Observable o, Object arg) method來取得被觀察者當下的成員變數

資料(get)及傳送過來的Object obj的參數實體。

由此可以知道被觀察者並不知道有哪些觀察者的存在,他只要通知所有的觀察者及把他要

pass的訊息給發送出去,觀察者即可以利用這份資料來做下一步的處理。


緊接著,在這邊小弟將撰寫一個抽獎的範例來套用看看觀察者設計模式

一、定義被觀察者(Observable)

public class LotteryObservable extends Observable{
    private List<Participant> personList;
    private StringBuilder awardMsg; 
    public LotteryObservable(File f){
       personList = LoadParticipantList(f);
    }
 
    public List<Participant> LoadParticipantList(File f){
       List<Participant> plist = new ArrayList<Participant>();
  
       try (BufferedReader br = new BufferedReader(new FileReader(f))){
   
          String data = null;
          System.out.println("參加抽獎的名單如下:");
          while((data = br.readLine()) != null){
             Participant p = new Participant(data);
             System.out.println(p);
             plist.add(p);
          }
          System.out.println("==============公布得獎名單==============");
     } catch (FileNotFoundException e) {
       // TODO Auto-generated catch block
       System.out.println("指定檔案不存在!!");
     } catch(IOException e){
       System.out.println("IOException");
     }
     return plist;
   }
 
   public void processLottery(int p_num, AwardType atype){
      Random rmd = new Random();
      List<Participant> awardMenu = new ArrayList<Participant>();
      awardMsg = new StringBuilder();
  
      if(AwardType.THIRD_AWARD == atype)awardMsg.append("抽出三獎得主:\n");
      else if(AwardType.SPECIAL_AWAED == atype)awardMsg.append("抽出二獎得主:\n");
  
      for(int i = 0 ; i < p_num ; i++){
         int n = rmd.nextInt(personList.size()-1);
         awardMsg.append(personList.get(n)+"\n");
         awardMenu.add(personList.get(n));
         personList.remove(n);
      }
  
      awardMsg.deleteCharAt(awardMsg.length()-1);
      this.setChanged();
      this.notifyObservers(awardMenu);
   } 

   public StringBuilder getAwardMsg() {
      return awardMsg;
   }
}

首先這個類別LotteryObservable需繼承Observable,並且定義LoadParticipantList方法來載入

抽獎者名單建立List<Participant> personList (Participant類別自行定義)

接著,processLottery方法為進行抽獎的過程,判斷當下的抽獎型態(定義enum)來顯示當下

要抽哪個獎項。再來透過亂數來取得index決定要抽出list下的Participant,並加入至awardMenu

內或轉換成字串assign至awardMsg內。

最後,呼叫setChanged及notifyObservers來知會觀察者!!

二、定義觀察者(多個)

觀察者需實作Observer並自行定義您的update方法!

1. AwardObserver 觀察者單純只是get被觀察者當下的得獎訊息來做發佈!

public class AwardObserver implements Observer{
   @Override
   public void update(Observable o, Object arg) {
     // TODO Auto-generated method stub
     LotteryObservable lobs = (LotteryObservable)o;
     System.out.println(this.getClass().getName()+"\n"+lobs.getAwardMsg().toString());
   }
}


2. AwardStatisticsObserver 觀察者將取得被觀察者pass過來的argument來做進一步的分析得獎

名單。區分出各獎項各有男、女各有多少人等簡單分析。

public class AwardStatisticsObserver implements Observer{
   @Override   
   public void update(Observable o, Object arg) {
     // TODO Auto-generated method stub
     @SuppressWarnings("unchecked")
     List<Participant> awardMenu = (List<Participant>) arg;
  
     Map<String, Integer> sex2num = new HashMap<String, Integer>();
     int count;
     for(Participant p : awardMenu){
        if(!sex2num.containsKey(p.getSex())){
           count = 1;
        }else{
           count = sex2num.get(p.getSex()) + 1;
        }
        sex2num.put(p.getSex(), count);
     }
     System.out.println(this.getClass().getName());
     for(String sex : sex2num.keySet()){
        System.out.println(sex + "性\t人數 => "+ sex2num.get(sex));
     }
   }
}

三、定義主要類別(開辦活動)

這個類別會定義被觀察者將被哪些觀察者觀察,並且assign抽獎名單及該活動要抽出哪些

獎項及人數。

public class Activity {
 /**
  * @param args
  */
 public static void main(String[] args) {
  // TODO Auto-generated method stub
  LotteryObservable lo = new LotteryObservable(new File("C:/menu.txt"));
  
  lo.addObserver(new AwardObserver());
  lo.addObserver(new AwardStatisticsObserver());
  
  lo.processLottery(4, AwardType.THIRD_AWARD);
  System.out.println("============================");
  lo.processLottery(3, AwardType.SECOND_AWARD);
 }
}

四、其他程式

定義獎項的enum及抽獎者

public enum AwardType {
 SPECIAL_AWAED, FIRST_AWARD, SECOND_AWARD, THIRD_AWARD
}

public class Participant {
 private String name;
 private String unit;
 private String sex;
 private int age;
 
 public Participant(String resource) {
  StringTokenizer st = new StringTokenizer(resource, ",");
  this.unit = st.nextToken();
  this.sex  = st.nextToken();
  this.age  = Integer.parseInt(st.nextToken());
  this.name = st.nextToken();
 }
 public String getName() {
  return name;
 }
 public void setName(String name) {
  this.name = name;
 }
 public String getUnit() {
  return unit;
 }
 public void setUnit(String unit) {
  this.unit = unit;
 }
 public String getSex() {
  return sex;
 }
 public void setSex(String sex) {
  this.sex = sex;
 }
 public int getAge() {
  return age;
 }
 public void setAge(int age) {
  this.age = age;
 }
 @Override
 public String toString() {
  return "["+unit+"]"+"=>"+sex+"\t"+age+"(age)\t"+name;
 }
}

Demo畫面



最後,小弟在此只是簡單講述Observer模式的概念及藉由抽獎的議題來自行發揮Observer

模式,若概念講述有誤或範例舉的不貼切請不吝指教囉!

留言