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

在軟件世界也是這樣,例如蕴潦,Excel 中的數據與折線圖像啼、餅狀圖、柱狀圖之間的關系潭苞;MVC 模式中的模型與視圖的關系忽冻;事件模型中的事件源與事件處理者。所有這些此疹,如果用觀察者模式來實現就非常方便僧诚。

模式的定義與特點

觀察者(Observer)模式的定義:指多個對象間存在一對多的依賴關系,當一個對象的狀態(tài)發(fā)生改變時蝗碎,所有依賴于它的對象都得到通知并被自動更新湖笨。這種模式有時又稱作發(fā)布-訂閱模式、模型-視圖模式蹦骑,它是對象行為型模式慈省。

觀察者模式是一種對象行為型模式,其主要優(yōu)點如下眠菇。

  1. 降低了目標與觀察者之間的耦合關系边败,兩者之間是抽象耦合關系。符合依賴倒置原則捎废。
  2. 目標與觀察者之間建立了一套觸發(fā)機制放闺。

它的主要缺點如下。

  1. 目標與觀察者之間的依賴關系并沒有完全解除缕坎,而且有可能出現循環(huán)引用怖侦。
  2. 當觀察者對象很多時,通知的發(fā)布會花費很多時間谜叹,影響程序的效率匾寝。

模式的結構與實現

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

1. 模式的結構

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

  1. 抽象主題(Subject)角色:也叫抽象目標類女仰,它提供了一個用于保存觀察者對象的聚集類和增加猜年、刪除觀察者對象的方法抡锈,以及通知所有觀察者的抽象方法。
  2. 具體主題(Concrete Subject)角色:也叫具體目標類乔外,它實現抽象目標中的通知方法床三,當具體主題的內部狀態(tài)發(fā)生改變時,通知所有注冊過的觀察者對象杨幼。
  3. 抽象觀察者(Observer)角色:它是一個抽象類或接口撇簿,它包含了一個更新自己的抽象方法,當接到具體主題的更改通知時被調用差购。
  4. 具體觀察者(Concrete Observer)角色:實現抽象觀察者中定義的抽象方法四瘫,以便在得到目標的更改通知時更新自身的狀態(tài)。

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

3-1Q1161A6221S.gif

圖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("具體目標發(fā)生改變...");
        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作出反應!");
    }
}

程序運行結果如下:

具體目標發(fā)生改變...
--------------
具體觀察者1作出反應稳析!
具體觀察者2作出反應锹杈!

模式的應用實例

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

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

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

3-1Q1161A646395.gif

圖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個基點播赁,提升了出口產品收入,提升了出口公司的銷售利潤率吼渡。

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

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

分析:在本實例中,學校的“鈴”是事件源和目標盒犹,“老師”和“學生”是事件監(jiān)聽器和具體觀察者懂更,“鈴聲”是事件類。學生和老師來到學校的教學區(qū)急膀,都會注意學校的鈴膜蛔,這叫事件綁定;當上課時間或下課時間到脖阵,會觸發(fā)鈴發(fā)聲皂股,這時會生成“鈴聲”事件;學生和老師聽到鈴聲會開始上課或下課命黔,這叫事件處理呜呐。這個實例非常適合用觀察者模式實現就斤,圖 3 給出了學校鈴聲的事件模型。

3-1Q1161AGQ46.gif

圖3 學校鈴聲的事件模型圖

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

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

再定義一個學校的鈴(BellEventSource)類绷旗,它是事件源,是觀察者目標類副砍,該類里面包含了監(jiān)聽器容器 listener衔肢,可以綁定監(jiān)聽者(學生或老師),并且有產生鈴聲事件和通知所有監(jiān)聽者的方法豁翎。

然后角骤,定義鈴聲事件監(jiān)聽者(BellEventListener)類,它是抽象觀察者心剥,它包含了鈴聲事件處理方法 heardBell(RingEvent e)邦尊。

最后,定義老師類(TeachEventListener)和學生類(StuEventListener)优烧,它們是事件監(jiān)聽器蝉揍,是具體觀察者,聽到鈴聲會去上課或下課畦娄。圖 4 給出了學校鈴聲事件處理程序的結構又沾。

3-1Q1161AP0K8.gif

圖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()); //注冊監(jiān)聽器(老師)
        bell.addPersonListener(new StuEventListener());    //注冊監(jiān)聽器(學生)
        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; //監(jiān)聽器容器
    public BellEventSource() {
        listener = new ArrayList<BellEventListener>();
    }
    //給事件源綁定監(jiān)聽器
    public void addPersonListener(BellEventListener ren) {
        listener.add(ren);
    }
    //事件觸發(fā)器:敲鐘纷责,當鈴聲sound的值發(fā)生變化時捍掺,觸發(fā)事件。
    public void ring(boolean sound) {
        String type = sound ? "上課鈴" : "下課鈴";
        System.out.println(type + "響再膳!");
        RingEvent event = new RingEvent(this, sound);
        notifies(event);    //通知注冊在該事件源上的所有監(jiān)聽器
    }
    //當事件發(fā)生時,通知綁定在該事件源上的所有監(jiān)聽器做出反應(調用事件處理方法)
    protected void notifies(RingEvent e) {
        BellEventListener ren = null;
        Iterator<BellEventListener> iterator = listener.iterator();
        while (iterator.hasNext()) {
            ren = iterator.next();
            ren.heardBell(e);
        }
    }
}
//抽象觀察者類:鈴聲事件監(jiān)聽器
interface BellEventListener extends EventListener {
    //事件處理方法挺勿,聽到鈴聲
    public void heardBell(RingEvent e);
}
//具體觀察者類:老師事件監(jiān)聽器
class TeachEventListener implements BellEventListener {
    public void heardBell(RingEvent e) {
        if (e.getSound()) {
            System.out.println("老師上課了...");
        } else {
            System.out.println("老師下課了...");
        }
    }
}
//具體觀察者類:學生事件監(jiān)聽器
class StuEventListener implements BellEventListener {
    public void heardBell(RingEvent e) {
        if (e.getSound()) {
            System.out.println("同學們,上課了...");
        } else {
            System.out.println("同學們喂柒,下課了...");
        }
    }
}

程序運行結果如下:

上課鈴響不瓶!
老師上課了...
同學們,上課了...
------------
下課鈴響灾杰!
老師下課了...
同學們蚊丐,下課了...

模式的應用場景

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

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

  1. 對象間存在一對多關系黍匾,一個對象的狀態(tài)發(fā)生改變會影響其他對象。
  2. 當一個抽象模型有兩個方面呛梆,其中一個方面依賴于另一方面時锐涯,可將這二者封裝在獨立的對象中以使它們可以各自獨立地改變和復用填物。
  3. 實現類似廣播機制的功能,不需要知道具體收聽者滞磺,只需分發(fā)廣播,系統(tǒng)中感興趣的對象會自動接收該廣播雁刷。
  4. 多層級嵌套使用保礼,形成一種鏈式觸發(fā)機制沛励,使得事件具備跨域(跨越兩種觀察者類型)通知炮障。

模式的擴展

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

1. Observable類

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

  1. void addObserver(Observer o) 方法:用于將新的觀察者對象添加到向量中送漠。
  2. void notifyObservers(Object arg) 方法:調用向量中的所有觀察者對象的 update() 方法,通知它們數據發(fā)生改變由蘑。通常越晚加入向量的觀察者越先得到通知闽寡。
  3. void setChange() 方法:用來設置一個 boolean 類型的內部標志位,注明目標對象發(fā)生了變化尼酿。當它為真時爷狈,notifyObservers() 才會通知觀察者。

2. Observer 接口

Observer 接口是抽象觀察者裳擎,它監(jiān)視目標對象的變化涎永,當目標對象發(fā)生變化時,觀察者得到通知,并調用 void update(Observable o,Object arg) 方法羡微,進行相應的工作支救。

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

分析:當原油價格上漲時拷淘,空方傷心各墨,多方局興;當油價下跌時启涯,空方局興贬堵,多方傷心。本實例中的抽象目標(Observable)類在 Java 中已經定義结洼,可以直接定義其子類黎做,即原油期貨(OilFutures)類,它是具體目標類松忍,該類中定義一個 SetPriCe(float price) 方法蒸殿,當原油數據發(fā)生變化時調用其父類的 notifyObservers(Object arg) 方法來通知所有觀察者;另外鸣峭,本實例中的抽象觀察者接口(Observer)在 Java 中已經定義宏所,只要定義其子類,即具體觀察者類(包括多方類 Bull 和空方類 Bear)摊溶,并實現 update(Observable o,Object arg) 方法即可爬骤。圖 5 所示是其結構圖。

3-1Q1161ARKO.gif

圖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();  //設置內部標志位莫换,注明數據發(fā)生變化
        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元恩尾,多方傷心了!
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末木人,一起剝皮案震驚了整個濱河市醒第,隨后出現的幾起案子蔫磨,更是在濱河造成了極大的恐慌圃伶,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搀罢,死亡現場離奇詭異榔至,居然都是意外死亡欺劳,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進店門枫弟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來淡诗,“玉大人,你說我怎么就攤上這事韩容。” “怎么了插爹?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵颠悬,是天一觀的道長溢陪。 經常有香客問我萍虽,道長杉编,這世上最難降的妖魔是什么咆霜? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任蛾坯,我火速辦了婚禮,結果婚禮上救军,老公的妹妹穿的比我還像新娘倘零。我一直安慰自己,他們只是感情好拷泽,可當我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布袖瞻。 她就那樣靜靜地躺著,像睡著了一般脂矫。 火紅的嫁衣襯著肌膚如雪砌庄。 梳的紋絲不亂的頭發(fā)上奕枢,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天缝彬,我揣著相機與錄音谷浅,去河邊找鬼。 笑死一疯,一個胖子當著我的面吹牛夺姑,可吹牛的內容都是我干的。 我是一名探鬼主播眉睹,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼竹海,長吁一口氣:“原來是場噩夢啊……” “哼丐黄!你這毒婦竟也來了?” 一聲冷哼從身側響起艰争,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤菩鲜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體狮崩,經...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡睦柴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年坦敌,在試婚紗的時候發(fā)現自己被綠了痢法。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杜顺。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡躬络,死狀恐怖,靈堂內的尸體忽然破棺而出穷当,到底是詐尸還是另有隱情,我是刑警寧澤茴扁,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布丹弱,位于F島的核電站,受9級特大地震影響躲胳,放射性物質發(fā)生泄漏纤勒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一粹湃、第九天 我趴在偏房一處隱蔽的房頂上張望为鳄。 院中可真熱鬧,春花似錦孤钦、人聲如沸纯丸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽坠陈。三九已至捐康,卻和暖如春解总,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背倾鲫。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工乌昔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留壤追,地道東北人。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓溺蕉,卻偏偏與公主長得像悼做,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子肛走,可洞房花燭夜當晚...
    茶點故事閱讀 44,901評論 2 355

推薦閱讀更多精彩內容