1. 程式人生 > >設計模式——觀察者模式(海姆達爾與仙宮人民)

設計模式——觀察者模式(海姆達爾與仙宮人民)

本文首發於cdream的個人部落格,點選獲得更好的閱讀體驗!

歡迎轉載,轉載請註明出處

本文主要對觀察者進行概述講解,並使用觀察者模式來模擬海姆達爾在發現敵人來襲後通知雷神托爾和洛基的過程。

image-20181209130344471

一、概念

定義

觀察者模式也叫作釋出-訂閱模式,也就是事件監聽機制。觀察者模式定義了物件之間的依賴關係,讓多個觀察者物件同時監聽某一個主題物件,這個主題物件在狀態上發生變化時,會通知所有觀察者物件,使他們能夠自動更新自己,並採取相應活動

主要結構

  1. 抽象主題角色

被觀察者,把所有觀察者物件引用儲存在一個集合裡,對外提供增加刪除的介面

  1. 具體主題角色

將有關狀態存入具體觀察者物件,在內部狀態發生改變時給所有註冊過的觀察者傳送資訊

  1. 抽象觀察者角色

為所有具體觀察者定義更新介面

  1. 具體觀察者角色

儲存與主題狀態自恰的狀態,實現更新介面,與主題狀態協調

UML類圖

image-20181209111356949

特點:鬆耦合

觀察者讓主題物件好觀察者之間鬆耦合,他們可以相互互動,但不清楚彼此的細節。
  • 觀察者只知道觀察者實現了某個介面,並不需要知道觀察者具體類,實現了哪些細節。
  • 任何時候可以增加觀察者。主題唯一依賴的東西是儲存觀察者實現的列表。所以在執行時新增新的觀察者也不會對主題造成影響。
  • 有新的觀察者加入時,主題程式碼不需要改變。主題在意的只是傳送通知給觀察者。
  • 可以獨立的使用主題和觀察者,因為他們之間是鬆耦合的。
  • 改變主題或觀察者其一併不需要修改另一方,只要遵守介面就可以。

二、海達姆斯與仙宮人民

海姆達爾(Heimdall)是希芙的哥哥。他是能眼視萬物和耳聽一切的仙宮守護哨兵,他站在彩虹橋比弗羅斯特上,並注意觀察任何對仙宮的襲擊。他作為仙宮的守衛站立著,保衛這個城市的大門使任何闖入者遠離,是奧丁最為信任的僕人之一。

對於仙宮的住民來說Heimdall是他們的可觀察者,當海姆達爾觀察到危機,會向所有他需要通知的人傳送通知。現在我們用觀察者模式來模擬海姆達斯發現危機通知仙宮住民這種情況。

下面是一個UML圖,實現了海姆達斯與仙宮人民的解耦,去除強耦合關係,同時哨兵介面可以給仙宮所有哨兵使用,觀察者們根據需要實現Action介面

image-20181209115326591

定義哨兵介面

/**
 * 阿斯加德所有的哨兵都有的行為
 * @author cdream
 * @date 2018/12/9
 */
public interface Sentinel {
    /**
     * 註冊需要通知的阿斯加德人
     * @param asgardManObserver
     */
    void registerObserver(AsgardManObserver asgardManObserver);

    /**
     * 移除需要通知的阿斯加德人
     * @param asgardManObserver
     */
    void removeObserver(AsgardManObserver asgardManObserver);

    /**
     * 通知所有asgard人
     */
    void notifyObservers();

    /**
     * 觀察到了資訊
     * @param message
     */
    void setMessage(String message);
}

海姆達斯實現哨兵介面

/**
 * 海姆達爾類,他向仙宮內的人民來傳遞資訊,是仙宮內人民的看觀察者
 * @author cdream
 * @date 2018/12/9
 */
public class Heimdall implements Sentinel {
    // 維繫所有需要通知的人,這是觀察者和被觀察者唯一關聯的地方
    private ArrayList<AsgardManObserver> lists=new ArrayList<>();
    private String message;
    @Override
    public void registerObserver(AsgardManObserver asgardManObserver) {
        lists.add(asgardManObserver);
    }

    @Override
    public void removeObserver(AsgardManObserver asgardManObserver) {
        lists.remove(asgardManObserver);
    }

    @Override
    public void notifyObservers() {
        lists.forEach(asgardMan -> asgardMan.update(message));
    }

    @Override
    public void setMessage(String message) {
        this.message = message;
        System.out.println("Heimdall:"+message);
        notifyObservers();
    }
}

AsgardManObserver介面,所有的想接收資訊的人都要實現

public interface AsgardManObserver {
    /**
     * 接收來自海姆達爾的資訊,並更新狀態
     * @param message
     */
    void update(String message);
}

Action介面,需要採取行動的人實現

public interface Action {
    // 採取行動
    void takeAction();
}

兩個需要用到的常量

// 滅霸
public static final String THANOS="Thanos";
// 冰霜巨人
public static final String FROST_GIANTS="Frost Giants";

觀察者1:雷神托爾

public class Thor implements AsgardManObserver,Action {
    private String message;

    @Override
    public void takeAction() {
        // 如果是滅霸
        if (message!=null && message.contains(Const.THANOS)){
            System.out.println("Thor:準備對抗滅霸");
        // 如果是冰霜巨人
        }else if(message !=null && message.contains(Const.FROST_GIANTS)){
            System.out.println("Thor:準備對抗冰霜巨人");
        }else{
            System.out.println("Thor:我沒聽懂你說什麼");
        }
    }
    // 一旦海姆達爾傳送敵人襲擊訊息,托爾立即採取行動
    @Override
    public void update(String message) {
        this.message = message;
        takeAction();
    }
}

觀察者2:洛基

public class Lokey implements AsgardManObserver,Action {
    private String message;
    @Override
    public void takeAction() {
        // 如果是滅霸
        if (message!=null && message.contains(Const.THANOS)){
            System.out.println("Lokey:準備逃走");
        // 如果是爸爸
        }else if(message !=null && message.contains(Const.FROST_GIANTS)){
            System.out.println("Lokey:準備反叛");
        }else{
            System.out.println("Lokey:我繼續做我的閒魚~");
        }
    }

    @Override
    public void update(String message) {
        this.message = message;
        takeAction();
    }
}

觀察者3:鹹魚

public class SaltedFish implements AsgardManObserver {
    private String message;
    @Override
    public void update(String message) {
        this.message = message;
        System.out.println("閒魚:繼續做鹹魚");
    }
}

開始模擬:

public class Test {
    public static void main(String[] args) {
        // 仙宮建立,阿斯加德人誕生
        Sentinel heimdall = new Heimdall();
        AsgardManObserver thor = new Thor();
        AsgardManObserver lokey = new Lokey();
        AsgardManObserver saltedFish = new SaltedFish();
        // 三個人都去海爾達姆那裡去註冊
        heimdall.registerObserver(thor);
        heimdall.registerObserver(lokey);
        heimdall.registerObserver(saltedFish);
        // 冰霜巨人來襲
        heimdall.setMessage(Const.FROST_GIANTS + "來襲");
        // 洛基叛變,海達姆斯不再通知洛基
        heimdall.removeObserver(lokey);
        System.out.println("-------------");
        //滅霸來襲
        heimdall.setMessage(Const.THANOS + "來襲");

    }
}

結果:

Heimdall:Frost Giants來襲
Thor:準備對抗冰霜巨人
Lokey:準備反叛
閒魚:繼續做鹹魚
-------------
Heimdall:Thanos來襲
Thor:準備對抗滅霸
閒魚:繼續做鹹魚

對抗冰霜巨人一戰,洛基叛變,海姆達爾不在向其傳送通知~

這是一個典型的觀察者模式的推模式,海姆達爾一旦得到敵人來襲的訊息就會通知他所維繫的觀察者,海爾達姆與仙宮住民是鬆耦合的,都可以獨立行動,又不用關注各自的細節(所以他也不知道洛基得到訊息後會做什麼:)),新來了觀察者直接維繫到list裡擴充套件性強。

拉模式:將整個被觀察者物件引用送給觀察者,由觀察者獲取需要的資訊。如下,注意notifyObservers方法的修改

/**
 * 海姆達爾類,他向仙宮內的人民來傳遞資訊,是仙宮內人民的看觀察者
 * @author cdream
 * @date 2018/12/9
 */
public class Heimdall implements Sentinel {
    // 維繫所有需要通知的人,這是觀察者和被觀察者唯一關聯的地方
    private ArrayList<AsgardManObserver> lists=new ArrayList<>();
    private String message;
    @Override
    public void registerObserver(AsgardManObserver asgardManObserver) {
        lists.add(asgardManObserver);
    }

    @Override
    public void removeObserver(AsgardManObserver asgardManObserver) {
        lists.remove(asgardManObserver);
    }

    @Override
    public void notifyObservers() {
        // 只要在遍歷這裡傳遞this就可以,觀察者的update方法需要修改一下
        lists.forEach(asgardMan -> asgardMan.update(this));
    }

    @Override
    public void setMessage(String message) {
        this.message = message;
        System.out.println("Heimdall:"+message);
        notifyObservers();
    }
}
推模式與拉模式
1.推模型是假定主題物件知道觀察者需要的資料;而拉模型是主題物件不知道觀察者具體需要什麼資料,沒有辦法的情況下,乾脆把自身傳遞給觀察者,讓觀察者自己去按需要取值。
2.推模型可能會使得觀察者物件難以複用,因為觀察者的update()方法是按需要定義的引數,可能無法兼顧沒有考慮到的使用情況。這就意味著出現新情況的時候,就可能提供新的update()方法,或者是乾脆重新實現觀察者;而拉模型就不會造成這樣的情況,因為拉模型下,update()方法的引數是主題物件本身,這基本上是主題物件能傳遞的最大資料集合了,基本上可以適應各種情況的需要。
3.拉模式會使觀察者獲取所有被觀察者資訊,同時可能會多次獲取才能會得到需要的所有資訊
4.但是想象其實,拉模式也是推模式的一種,只不過是推送個引用過去,裡面包含了更多的資訊

三、jdk對觀察者模式的支援

jdk本身提供了對觀察者模式的支援,並且支援推、拉兩種方案。主要類或介面是java.util.Observable(抽象類)和java.util.Observer(介面)

這裡就舉個Head First 設計模式的一個例子吧,天氣資料和天氣顯示器。不同的天氣顯示器會顯示不同的資訊。

用來儲存天氣資訊的實體類

public class WeatherPojo {
    // 溫度
    private float temperature;
    // 溼度
    private float humidity;
    // 氣壓
    private float pressure;

    public WeatherPojo() {
    }

    public WeatherPojo(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
    }

    public float getTemperature() {
        return temperature;
    }

    public void setTemperature(float temperature) {
        this.temperature = temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public void setHumidity(float humidity) {
        this.humidity = humidity;
    }

    public float getPressure() {
        return pressure;
    }

    public void setPressure(float pressure) {
        this.pressure = pressure;
    }
}

被觀察者:天氣資料

public class WeatherData extends Observable {
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData() {}

    public void measurementsChanged(){
        // 這個是observable中的標誌,使類更加靈活
        // 想象天氣變個0.01度你都提醒是吧是煩死了
        // 這裡進行限制,達到一定條件再發送通知
        setChanged();
        notifyObservers();
    }
    public void updateData(WeatherPojo pojo){
        this.temperature = pojo.getTemperature();
        this.humidity = pojo.getHumidity();
        this.pressure = pojo.getPressure();
        // 被觀察者資料發生了改變,提醒觀察者
        measurementsChanged();
    }

    public float getTemperature() {
        return temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }
}

觀察者:天氣顯示板

public class CurrentDisplay implements Observer {
    Observable observable;
    private float temperature;
    private float humidity;

    public CurrentDisplay(Observable observable) {
        this.observable = observable;
        observable.addObserver(this);
    }
    public void display(){
        System.out.println("溫度是:"+temperature+"; 溼度是:"+humidity);
    }
    // 注意這裡有兩個引數,前面是傳遞觀察者物件
    // 後面可以傳遞需要的引數,是notifyObservers()方法中的引數
    // 這種就是引數和被觀察者飲用都傳過去
    @Override
    public void update(Observable o, Object arg) {
        if (o instanceof WeatherData){
            WeatherData weatherData = (WeatherData) o;
            this.temperature = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            display();
        }
    }
}

jdk對觀察者模式的實現需要被觀察者繼承Observable,對程式碼有一定的侵入,例如如果海姆達爾還要繼承阿斯加德人這個類,那就需要我們手動來實現觀察者模式了。

四、總結

觀察者模式是比較常見的設計模式,我們常見的MVC就是標準的觀察者模式,如果感興趣看以google"使用觀察者模式實現mvc"。此外向訊息佇列的釋出訂閱模式也是使用的觀察者模式,而且是非同步的效能更好,像我們上面實現的這種簡單遍歷,如果觀察者實現複雜那效能看就會受到影響,畢竟要等待一個觀察者執行完才能通知下一個觀察者。

本文首發於cdream個人部落格

歡迎轉載,轉載請註明出處!


參考資料:

  1. Head First 設計模式,Eric Freeman &Elisabeth Freeman with Kathy Sierra & Bert Bates
  2. java設計模式精講 Debug 方式+記憶體分析
  3. 《JAVA與模式》之觀察者模式