模式設計:觀察者模式(Observer模式)詳解

在現實世界中,許多對象並不是獨立存在的,其中一個對象的行爲發生改變可能會導致一個或者多個其他對象的行爲也發生改變。例如,某種商品的物價上漲時會導致部分商家高興,而消費者傷心;還有,當我們開車到交叉路口時,遇到紅燈會停,遇到綠燈會行。這樣的例子還有很多,例如,股票價格與股民、微信公衆號與微信用戶、氣象局的天氣預報與聽衆、小偷與警察等。

在軟件世界也是這樣,例如,Excel 中的數據與折線圖、餅狀圖、柱狀圖之間的關係;MVC 模式中的模型與視圖的關係;事件模型中的事件源與事件處理者。所有這些,如果用觀察者模式來實現就非常方便。

模式的定義與特點

觀察者(Observer)模式的定義:指多個對象間存在一對多的依賴關係,當一個對象的狀態發生改變時,所有依賴於它的對象都得到通知並被自動更新。這種模式有時又稱作發佈-訂閱模式、模型-視圖模式,它是對象行爲型模式。

觀察者模式是一種對象行爲型模式,其主要優點如下。

  1. 降低了目標與觀察者之間的耦合關係,兩者之間是抽象耦合關係。符合依賴倒置原則。
  2. 目標與觀察者之間建立了一套觸發機制。


它的主要缺點如下。

  1. 目標與觀察者之間的依賴關係並沒有完全解除,而且有可能出現循環引用。
  2. 當觀察者對象很多時,通知的發佈會花費很多時間,影響程序的效率。

模式的結構與實現

實現觀察者模式時要注意具體目標對象和具體觀察者對象之間不能直接調用,否則將使兩者之間緊密耦合起來,這違反了面向對象的設計原則。

1. 模式的結構

觀察者模式的主要角色如下。

  1. 抽象主題(Subject)角色:也叫抽象目標類,它提供了一個用於保存觀察者對象的聚集類和增加、刪除觀察者對象的方法,以及通知所有觀察者的抽象方法。
  2. 具體主題(Concrete Subject)角色:也叫具體目標類,它實現抽象目標中的通知方法,當具體主題的內部狀態發生改變時,通知所有註冊過的觀察者對象。
  3. 抽象觀察者(Observer)角色:它是一個抽象類或接口,它包含了一個更新自己的抽象方法,當接到具體主題的更改通知時被調用。
  4. 具體觀察者(Concrete Observer)角色:實現抽象觀察者中定義的抽象方法,以便在得到目標的更改通知時更新自身的狀態。


觀察者模式的結構圖如圖 1 所示。

圖1 觀察者模式的結構圖

2. 模式的實現

觀察者模式的實現代碼如下:

package net.biancheng.c.observer;

import java.util.*;

public class ObserverPattern {
    public static void main(String[] args) {
        Subject subject = new ConcreteSubject();
        Observer obs1 = new ConcreteObserver1();
        Observer obs2 = new ConcreteObserver2();
        subject.add(obs1);
        subject.add(obs2);
        subject.notifyObserver();
    }
}

//抽象目標
abstract class Subject {
    protected List<Observer> observers = new ArrayList<Observer>();

    //增加觀察者方法
    public void add(Observer observer) {
        observers.add(observer);
    }

    //刪除觀察者方法
    public void remove(Observer observer) {
        observers.remove(observer);
    }

    public abstract void notifyObserver(); //通知觀察者方法
}

//具體目標
class ConcreteSubject extends Subject {
    public void notifyObserver() {
        System.out.println("具體目標發生改變...");
        System.out.println("--------------");

        for (Object obs : observers) {
            ((Observer) obs).response();
        }

    }
}

//抽象觀察者
interface Observer {
    void response(); //反應
}

//具體觀察者1
class ConcreteObserver1 implements Observer {
    public void response() {
        System.out.println("具體觀察者1作出反應!");
    }
}

//具體觀察者1
class ConcreteObserver2 implements Observer {
    public void response() {
        System.out.println("具體觀察者2作出反應!");
    }
}

運行結果:

具體目標發生改變...
--------------
具體觀察者1作出反應!
具體觀察者2作出反應!

 

模式的應用實例

【例1】利用觀察者模式設計一個程序,分析“人民幣匯率”的升值或貶值對進口公司進口產品成本或出口公司的出口產品收入以及公司利潤率的影響。

分析:當“人民幣匯率”升值時,進口公司的進口產品成本降低且利潤率提升,出口公司的出口產品收入降低且利潤率降低;當“人民幣匯率”貶值時,進口公司的進口產品成本提升且利潤率降低,出口公司的出口產品收入提升且利潤率提升。

這裏的匯率(Rate)類是抽象目標類,它包含了保存觀察者(Company)的 List 和增加/刪除觀察者的方法,以及有關匯率改變的抽象方法 change(int number);而人民幣匯率(RMBrate)類是具體目標, 它實現了父類的 change(int number) 方法,即當人民幣匯率發生改變時通過相關公司;公司(Company)類是抽象觀察者,它定義了一個有關匯率反應的抽象方法 response(int number);進口公司(ImportCompany)類和出口公司(ExportCompany)類是具體觀察者類,它們實現了父類的 response(int number) 方法,即當它們接收到匯率發生改變的通知時作爲相應的反應。圖 2 所示是其結構圖。

                                  圖2 人民幣匯率分析程序的結構圖


程序代碼如下:

package net.biancheng.c.observer;

import java.util.*;

public class RMBrateTest {
    public static void main(String[] args) {
        Rate rate = new RMBrate();
        Company watcher1 = new ImportCompany();
        Company watcher2 = new ExportCompany();
        rate.add(watcher1);
        rate.add(watcher2);
        rate.change(10);
        rate.change(-9);
    }
}

//抽象目標:匯率
abstract class Rate {
    protected List<Company> companys = new ArrayList<Company>();

    //增加觀察者方法
    public void add(Company company) {
        companys.add(company);
    }

    //刪除觀察者方法
    public void remove(Company company) {
        companys.remove(company);
    }

    public abstract void change(int number);
}

//具體目標:人民幣匯率
class RMBrate extends Rate {
    public void change(int number) {
        for (Company obs : companys) {
            ((Company) obs).response(number);
        }
    }
}

//抽象觀察者:公司
interface Company {
    void response(int number);
}

//具體觀察者1:進口公司
class ImportCompany implements Company {
    public void response(int number) {
        if (number > 0) {
            System.out.println("人民幣匯率升值" + number + "個基點,降低了進口產品成本,提升了進口公司利潤率。");
        } else if (number < 0) {
            System.out.println("人民幣匯率貶值" + (-number) + "個基點,提升了進口產品成本,降低了進口公司利潤率。");
        }
    }
}

//具體觀察者2:出口公司
class ExportCompany implements Company {
    public void response(int number) {
        if (number > 0) {
            System.out.println("人民幣匯率升值" + number + "個基點,降低了出口產品收入,降低了出口公司的銷售利潤率。");
        } else if (number < 0) {
            System.out.println("人民幣匯率貶值" + (-number) + "個基點,提升了出口產品收入,提升了出口公司的銷售利潤率。");
        }
    }
}

運行結果:

人民幣匯率升值10個基點,降低了進口產品成本,提升了進口公司利潤率。
人民幣匯率升值10個基點,降低了出口產品收入,降低了出口公司的銷售利潤率。
人民幣匯率貶值9個基點,提升了進口產品成本,降低了進口公司利潤率。
人民幣匯率貶值9個基點,提升了出口產品收入,提升了出口公司的銷售利潤率。

觀察者模式在軟件幵發中用得最多的是窗體程序設計中的事件處理,窗體中的所有組件都是“事件源”,也就是目標對象,而事件處理程序類的對象是具體觀察者對象。下面以一個學校鈴聲的事件處理程序爲例,介紹 Windows 中的“事件處理模型”的工作原理。

【例2】利用觀察者模式設計一個學校鈴聲的事件處理程序。

分析:在本實例中,學校的“鈴”是事件源和目標,“老師”和“學生”是事件監聽器和具體觀察者,“鈴聲”是事件類。學生和老師來到學校的教學區,都會注意學校的鈴,這叫事件綁定;當上課時間或下課時間到,會觸發鈴發聲,這時會生成“鈴聲”事件;學生和老師聽到鈴聲會開始上課或下課,這叫事件處理。這個實例非常適合用觀察者模式實現,圖 3 給出了學校鈴聲的事件模型。

現在用“觀察者模式”來實現該事件處理模型。

首先,定義一個鈴聲事件(RingEvent)類,它記錄了鈴聲的類型(上課鈴聲/下課鈴聲)。

再定義一個學校的鈴(BellEventSource)類,它是事件源,是觀察者目標類,該類裏面包含了監聽器容器 listener,可以綁定監聽者(學生或老師),並且有產生鈴聲事件和通知所有監聽者的方法。

然後,定義鈴聲事件監聽者(BellEventListener)類,它是抽象觀察者,它包含了鈴聲事件處理方法 heardBell(RingEvent e)。

最後,定義老師類(TeachEventListener)和學生類(StuEventListener),它們是事件監聽器,是具體觀察者,聽到鈴聲會去上課或下課。圖 4 給出了學校鈴聲事件處理程序的結構。

代碼如下:

package net.biancheng.c.observer;

import java.util.*;

public class BellEventTest {
    public static void main(String[] args) {
        BellEventSource bell = new BellEventSource();    //鈴(事件源)
        bell.addPersonListener(new TeachEventListener()); //註冊監聽器(老師)
        bell.addPersonListener(new StuEventListener());    //註冊監聽器(學生)
        bell.ring(true);   //打上課鈴聲
        System.out.println("------------");
        bell.ring(false);  //打下課鈴聲
    }
}

//鈴聲事件類:用於封裝事件源及一些與事件相關的參數
class RingEvent extends EventObject {
    private static final long serialVersionUID = 1L;
    private boolean sound;    //true表示上課鈴聲,false表示下課鈴聲

    public RingEvent(Object source, boolean sound) {
        super(source);
        this.sound = sound;
    }

    public void setSound(boolean sound) {
        this.sound = sound;
    }

    public boolean getSound() {
        return this.sound;
    }
}

//目標類:事件源,鈴
class BellEventSource {
    private List<BellEventListener> listener; //監聽器容器

    public BellEventSource() {
        listener = new ArrayList<BellEventListener>();
    }

    //給事件源綁定監聽器
    public void addPersonListener(BellEventListener ren) {
        listener.add(ren);
    }

    //事件觸發器:敲鐘,當鈴聲sound的值發生變化時,觸發事件。
    public void ring(boolean sound) {
        String type = sound ? "上課鈴" : "下課鈴";
        System.out.println(type + "響!");
        RingEvent event = new RingEvent(this, sound);
        notifies(event);    //通知註冊在該事件源上的所有監聽器
    }

    //當事件發生時,通知綁定在該事件源上的所有監聽器做出反應(調用事件處理方法)
    protected void notifies(RingEvent e) {
        BellEventListener ren = null;
        Iterator<BellEventListener> iterator = listener.iterator();
        while (iterator.hasNext()) {
            ren = iterator.next();
            ren.heardBell(e);
        }
    }
}

//抽象觀察者類:鈴聲事件監聽器
interface BellEventListener extends EventListener {
    //事件處理方法,聽到鈴聲
    public void heardBell(RingEvent e);
}

//具體觀察者類:老師事件監聽器
class TeachEventListener implements BellEventListener {
    public void heardBell(RingEvent e) {
        if (e.getSound()) {
            System.out.println("老師上課了...");
        } else {
            System.out.println("老師下課了...");
        }
    }
}

//具體觀察者類:學生事件監聽器
class StuEventListener implements BellEventListener {
    public void heardBell(RingEvent e) {
        if (e.getSound()) {
            System.out.println("同學們,上課了...");
        } else {
            System.out.println("同學們,下課了...");
        }
    }
}

運行結果:

上課鈴響!
老師上課了...
同學們,上課了...
------------
下課鈴響!
老師下課了...
同學們,下課了...

 

模式的應用場景

在軟件系統中,當系統一方行爲依賴另一方行爲的變動時,可使用觀察者模式松耦合聯動雙方,使得一方的變動可以通知到感興趣的另一方對象,從而讓另一方對象對此做出響應。

通過前面的分析與應用實例可知觀察者模式適合以下幾種情形。

  1. 對象間存在一對多關係,一個對象的狀態發生改變會影響其他對象。
  2. 當一個抽象模型有兩個方面,其中一個方面依賴於另一方面時,可將這二者封裝在獨立的對象中以使它們可以各自獨立地改變和複用。
  3. 實現類似廣播機制的功能,不需要知道具體收聽者,只需分發廣播,系統中感興趣的對象會自動接收該廣播。
  4. 多層級嵌套使用,形成一種鏈式觸發機制,使得事件具備跨域(跨越兩種觀察者類型)通知。

模式的擴展

Java 中,通過 java.util.Observable 類和 java.util.Observer 接口定義了觀察者模式,只要實現它們的子類就可以編寫觀察者模式實例。

1. Observable類

Observable 類是抽象目標類,它有一個 Vector 向量,用於保存所有要通知的觀察者對象,下面來介紹它最重要的 3 個方法。

  1. void addObserver(Observer o) 方法:用於將新的觀察者對象添加到向量中。
  2. void notifyObservers(Object arg) 方法:調用向量中的所有觀察者對象的 update() 方法,通知它們數據發生改變。通常越晚加入向量的觀察者越先得到通知。
  3. void setChange() 方法:用來設置一個 boolean 類型的內部標誌位,註明目標對象發生了變化。當它爲真時,notifyObservers() 纔會通知觀察者。

2. Observer 接口

Observer 接口是抽象觀察者,它監視目標對象的變化,當目標對象發生變化時,觀察者得到通知,並調用 void update(Observable o,Object arg) 方法,進行相應的工作。

【例3】利用 Observable 類和 Observer 接口實現原油期貨的觀察者模式實例。

分析:當原油價格上漲時,空方傷心,多方局興;當油價下跌時,空方局興,多方傷心。本實例中的抽象目標(Observable)類在 Java 中已經定義,可以直接定義其子類,即原油期貨(OilFutures)類,它是具體目標類,該類中定義一個 SetPriCe(float price) 方法,當原油數據發生變化時調用其父類的 notifyObservers(Object arg) 方法來通知所有觀察者;另外,本實例中的抽象觀察者接口(Observer)在 Java 中已經定義,只要定義其子類,即具體觀察者類(包括多方類 Bull 和空方類 Bear),並實現 update(Observable o,Object arg) 方法即可。圖 5 所示是其結構圖。

                      圖5 原油期貨的觀察者模式實例的結構圖


程序代碼如下:

package net.biancheng.c.observer;

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

public class CrudeOilFutures {
    public static void main(String[] args) {
        OilFutures oil = new OilFutures();
        Observer bull = new Bull(); //多方
        Observer bear = new Bear(); //空方
        oil.addObserver(bull);
        oil.addObserver(bear);
        oil.setPrice(10);
        oil.setPrice(-8);
    }
}

//具體目標類:原油期貨
class OilFutures extends Observable {
    private float price;

    public float getPrice() {
        return this.price;
    }

    public void setPrice(float price) {
        super.setChanged();  //設置內部標誌位,註明數據發生變化
        super.notifyObservers(price);    //通知觀察者價格改變了
        this.price = price;
    }
}

//具體觀察者類:多方
class Bull implements Observer {
    public void update(Observable o, Object arg) {
        Float price = ((Float) arg).floatValue();
        if (price > 0) {
            System.out.println("油價上漲" + price + "元,多方高興了!");
        } else {
            System.out.println("油價下跌" + (-price) + "元,多方傷心了!");
        }
    }
}

//具體觀察者類:空方
class Bear implements Observer {
    public void update(Observable o, Object arg) {
        Float price = ((Float) arg).floatValue();
        if (price > 0) {
            System.out.println("油價上漲" + price + "元,空方傷心了!");
        } else {
            System.out.println("油價下跌" + (-price) + "元,空方高興了!");
        }
    }
}

運行結果:

油價上漲10.0元,空方傷心了!
油價上漲10.0元,多方高興了!
油價下跌8.0元,空方高興了!
油價下跌8.0元,多方傷心了!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章