一文徹底搞懂觀察者模式(Observer)


文章已收錄我的倉庫:Java學習筆記與免費書籍分享

設計意圖

定義對象間的一種一對多的依賴關系,當一個對象的狀態發生改變時,所有依賴於它的對象都得到通知並被自動更新。

在實際設計開發中,我們通常會降低類與類之間的耦合度,這樣可能會產生一個副作用:由於類與類被分割,我們難以維護類之間的一致性。

舉一個常見的例子,我們對用戶顯示數學餅狀圖是需要數據支撐的,例如下面這張東京奧運會金牌榜:

在開發中,這張圖表分為兩個部分,一個是視圖部分,也就是以餅狀圖呈現出的樣子,一個是數據部分,即各國的金牌數量,由於我們將數據與視圖抽離,因此一旦數據部分更新,視圖部分得不到最新的數據,難以維持一致性,這個時候我們需要一個時刻關注數據變化的觀察者,一旦觀察者感知到數據變化則立即更新視圖,我們可以讓視圖本身作為一個觀察者,但這樣設計是不好的,視圖類應當做好設計視圖的事而無需插手其他工作,更好的辦法是單獨分離出一個觀察者類以維護兩個類之間的一致性,這就是觀察者模式的設計意圖。

在實際例子中,這種模式應用非常廣泛,例如一旦小說更新將會自動訂閱,一旦會員過期將會自動續費,MVC三層模式中的控制器就會觀察視圖並實時更新模型部分......觀察者模式是應用最廣泛的模式之一。

設計

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

觀察者模式的結構圖

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

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

設計接口(抽象)是常規的設計思想:定義一個接口由子類實現。這樣做利於后續擴展,但如果確定只有一個被觀察對象,則沒有必要設計接口(抽象)類。

常見的設計是:觀察者到被觀察目標中注冊登記,告訴它有一個觀察者正在觀察他,如有變化請通知,隨后觀察目標發生變化,則通知所有注冊登錄過的觀察者並告訴自己的身份(觀察者可能觀察多個目標,某些時候它必須知道具體是那個目標發生了變化),隨后觀察者更新相應數據。

代碼示例

我們考慮上述數據與視圖之間的例子,這里假設我們的視圖接收谷歌數據源與百度數據源:

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

//視圖類
class View {
    //通過復雜轉換將數據可視化,這里簡單的打印
    public void show(Object data) {
        System.out.println(data);
    }
}

//定義抽象類 數據源類
abstract class DataSource {
    //相關的源數據
    protected String data = "";

    //存儲已經注冊過的觀察者
    protected List<Observer> observers = new ArrayList<>();

    //獲取該數據
    public String getData() {
        return data;
    }

    //觀察者到這里注冊,被觀察者保存觀察者信息
    public void addObserver(Observer observer) {
        observers.add(observer);
    }
    //移除更改觀察者就不寫了

    //接口方法,更新數據,由目標類通知觀察者
    abstract protected void updateData(String newData);

    //接口方法,通知觀察者,由子類采用不同的方法實現
    abstract public void notifyObserver();
}

//數據源類的具體實現之一,百度數據源類
class BaiduDataSource extends DataSource {
    @Override
    protected void updateData(String newData) {
        //如果數據發生變化,則更新數據並通知觀察者
        if (!newData.equals(data)) {
            //這一步是必須的,在通知觀察者前一定要完成變化
            //這就好比你明天才出發可你卻告訴你的好朋友今天走,你的好朋友來接你沒看到你,友情破碎
            //必須要保持狀態的一致性
            data = newData;
            notifyObserver();
        }
    }

    @Override
    public void notifyObserver() {
        //廣播消息,並告知觀察者自己是誰
        for (var observer : observers) {
            observer.update(this, data);
        }
    }
}

//數據源類的具體實現之一,谷歌數據源類
class GoogleDataSource extends DataSource {
    @Override
    protected void updateData(String newData) {
        //如果數據發生變化,則更新數據並通知觀察者
        if (!newData.equals(data)) {
            //必須要保持狀態的一致性
            data = newData;
            notifyObserver();
        }
    }

    @Override
    public void notifyObserver() {
        //廣播消息,並告知觀察者自己是誰
        for (var observer : observers) {
            observer.update(this, data);
        }
    }
}

//觀察者接口
interface Observer {
    /**
     * 更新操作
     * @param ds    觀察的具體數據源
     * @param data  更新的數據
     */
    void update(DataSource ds, String data);
}

//觀察者A
class ObserverA implements Observer {
    //由view示例委托觀察數據源
    private View view;

    public ObserverA(View view) {
        this.view = view;
    }

    @Override
    public void update(DataSource ds, String data) {
        System.out.println("觀察到" + ds.getClass().getSimpleName() + "發生變化,更新視圖");
        //更新視圖View
        view.show(data);
    }
}


//測試類
public class Test {
    public static void main(String[] args) {
        //定義視圖類
        View view = new View();
        view.show("初始狀態");

        System.out.println();

        //定義與view相關數據源
        DataSource bds = new BaiduDataSource();//百度數據源
        DataSource gds = new GoogleDataSource();//谷歌數據源

        //為view添加觀察數據源的觀察者
        Observer observer = new ObserverA(view);

        //觀察者需要到到數據源類中注冊
        bds.addObserver(observer);
        gds.addObserver(observer);

        //手動更新數據
        bds.updateData("這是百度新數據--" + new Date());
        System.out.println();
        gds.updateData("這是谷歌新數據--" + new Date());
    }
}
//輸出
/*
初始狀態

觀察到BaiduDataSource發生變化,更新視圖
這是百度新數據--Fri Jul 30 10:43:55 CST 2021

觀察到GoogleDataSource發生變化,更新視圖
這是谷歌新數據--Fri Jul 30 10:43:55 CST 2021
*/

討論與優化

我們圍繞上面的代碼示例來討論。

  • 在發送給通知給觀察者前,維護自身狀態一致性是很重要的,在上面的代碼中我們必須要先更新數據在發送通知,就像例子說的,你明明要等到明天才出發,可你卻通知你的好朋友馬上就走走,這樣總會引起一些不好的結果。

  • 上述代碼只設置了一個觀察者,實際中可能有多個觀察者,可是觀察者之間卻又互相不知道彼此的存在,這就可能會造成重復更新的甚至更嚴重的問題,我們必須要好好設置觀察者,以保證它們在功能上不具有重復性。事實上,當觀察者越來越多時,代碼會變得更加難以擴展維護。

  • 上述代碼中我們讓觀察者保存了View的實例,實際的更新還是由該實例自己來完成,這是符合觀察者模式的定義的。但實際上,常常會由觀察者自身來更新相關數據。

  • 觀察者可能觀察多個目標,因此當目標通知觀察者時應該告知觀察者它自己是誰,以便觀察者做出相應操作,實現的辦法就是目標將自身傳入觀察者方法的參數中。這樣是符合常理的——觀察者正在觀察5歲、6歲、7歲的人比賽跑步,一旦出現達到終點則觀察者頒發獎狀,不同年齡的人評獎原則也是不同的,所以觀察者必須知道到底是誰完成比賽。

  • 上述代碼中一旦有變化則通知所有的觀察者——盡管有些觀察者對這些消息並不感興趣,當觀察者較多時,效率是很低的,我們應該只通知那些對該變化感興趣的觀察者們,我們可以定義一個Aspect類表示該變化的特點,可以采用哈希表保存觀察者:

    Map<Aspect, List<Observer>> map = new HashMap<>();
    

    觀察者注冊時,必須表面自己對那些方面的變化感興趣:

    public void addObserver(Aspect aspect, Observer observer) {
    	map.put(aspect, observer);
    }
    

其他

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) 方法,進行相應的工作。

事實上這一套類已經太老了,效率比較低,不建議使用。

總結

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

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

它的主要缺點如下。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM