設計模式(二) - 觀察者模式(Observer Pattern)

一、什麼是觀察者模式?

觀察者模式定義了對象之間的一對多依賴,這樣一來,當一個對象改變狀態時,它的所有依賴者都會收到通知並自動更新。

觀察者模式又叫做發佈-訂閱(Publish/Subscribe)模式、模型-視圖(Model/View)模式、源-監聽器(Source/Listener)模式或從屬者(Dependents)模式。

[觀察者模式-類圖]


觀察者模式(Observer Pattern) = 主題(Subject) + 觀察者(Observer)

● Subject(主題):主題又稱爲目標,它是指被觀察的對象。在主題中定義了一個觀察者集合,一個觀察目標可以接受任意數量的觀察者來觀察,它提供一系列方法來增加和刪除觀察者對象,同時它定義了通知方法notifyObservers()。目標類可以是接口,也可以是抽象類或具體類(推薦使用接口,這樣符合我們上一篇策略模式中提到的設計原則:多用組合少用繼承;同時可以達到鬆耦合的目的)。

+

● ConcreteSubject(具體主題):具體主題是主題類的實現類,通常它包含有經常發生改變的數據,當它的狀態發生改變時,向它的各個觀察者發出通知;同時它還實現了在目標類中定義的抽象業務邏輯方法(如果有的話)。如果無須擴展目標類,則具體目標類可以省略。

● Observer(觀察者):觀察者將對觀察目標的改變做出反應,觀察者一般定義爲接口,該接口聲明瞭更新數據的方法update(),因此又稱爲抽象觀察者。

● ConcreteObserver(具體觀察者):在具體觀察者中維護一個指向具體主題對象的引用,它存儲具體觀察者的有關狀態,這些狀態需要和具體目標的狀態保持一致;它實現了在抽象觀察者Observer中定義的update()方法。通常在實現時,可以調用具體目標類的registerObserver()方法將自己添加到主題類的集合中,或者通過removeObserver()方法將自己從主題類的集合中刪除。


二、代碼示例

我們舉一個氣象站的例子:創建一個WeatherData對象(追蹤來自氣象站的數據,並更新佈告板),和三個佈告板(顯示目前天氣狀況:溫度、溼度、氣壓給用戶看),一旦WeatherData對象有新的測量,這些佈告必須馬上更新。我們採用觀察者模式來實現這個程序,並用WeatherData對象來實現Subject接口,當他來當“主題”,讓佈告板來實現Observer接口,來當我們的“觀察者”。

接口包:

/**
 * 主題接口
 */
public interface ISubject {
    /**
     * 註冊觀察者
     * @param o 觀察者
     */
    void registerObserver(IObserver o);

    /**
     * 移除觀察者
     * @param o 觀察者
     */
    void removeObserver(IObserver o);

    /**
     * 通知觀察者
     */
    void notifyObservers();
}
/**
 * 觀察者接口
 */
public interface IObserver {
    /**
     * 公共的更新方法
     * @param temperature 溫度
     * @param humidity 溼度
     * @param pressure 氣壓
     */
    void update(float temperature, float humidity, float pressure);
}
/**
 * 用來展示的一個公共接口
 */
public interface IDisplayElement {
    void display();
}

具體的主題:

/**
 * 具體的主題
 */
public class WeatherData implements ISubject {
    private List<IObserver> observers;  // 用一個List來記錄觀察者
    private float temperature;  // 溫度
    private float humidity;     // 溼度
    private float pressure;     // 氣壓

    public WeatherData() {
        observers = new ArrayList<>();
    }

    /**
     * 註冊觀察者
     * @param o 觀察者
     */
    @Override
    public void registerObserver(IObserver o) {
        if (o == null)
            throw new NullPointerException();
        observers.add(o);
    }

    /**
     * 註銷觀察者
     * @param o 觀察者
     */
    @Override
    public void removeObserver(IObserver o) {
        int i = observers.indexOf(o);
        if (i >= 0)
            observers.remove(i);
    }

    /**
     * 通知觀察者
     */
    @Override
    public void notifyObservers() {
        // 這裏我們把狀態告訴每一個觀察者。因爲觀察者都實現了update()方法,所以我們知道如何通知他們。
        for (IObserver o : observers) {
            o.update(temperature, humidity, pressure);
        }
    }

    /**
     * 當WeatherData從氣象站得到更新觀測值時,我們通知觀察者
     */
    private void measurementsChanged() {
        notifyObservers();
    }

    // 用這個set值得方法來測試佈告板
    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    public float getTemperature() {
        return temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }
}
具體的觀察者們:

/**
 * 此佈告板根據WeatherData對象顯示當前觀測值
 */
public class CurrentConditionsDisplay implements IObserver, IDisplayElement {
    private float temperature;  // 溫度
    private float humidity;     // 溼度
    private ISubject weatherData;

    public CurrentConditionsDisplay(ISubject weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }

    @Override
    public void display() {
        System.out.println("Current conditions: " + temperature
                + "F degrees and " + humidity + "% humidity");
    }
}
/**
 * 此佈告板根據氣壓計顯示天氣預報
 */
public class ForecastDisplay implements IObserver, IDisplayElement {
    private float currentPressure = 29.92f;
    private float lastPressure;
    private WeatherData weatherData;

    public ForecastDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        lastPressure = currentPressure;
        currentPressure = pressure;

        display();
    }

    @Override
    public void display() {
        System.out.print("Forecast: ");
        if (currentPressure > lastPressure) {
            System.out.println("Improving weather on the way!");
        } else if (currentPressure == lastPressure) {
            System.out.println("More of the same");
        } else {
            System.out.println("Watch out for cooler, rainy weather");
        }
    }
}
/**
 * 此佈告板跟蹤最小、平均、最大的觀測值,並顯示
 */
public class StatisticsDisplay implements IObserver, IDisplayElement {
    private float maxTemp = 0.0f;
    private float minTemp = 200;
    private float tempSum = 0.0f;
    private int numReadings;
    private WeatherData weatherData;

    public StatisticsDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        tempSum += temperature;
        numReadings++;

        if (temperature > maxTemp) {
            maxTemp = temperature;
        }

        if (temperature < minTemp) {
            minTemp = temperature;
        }

        display();
    }

    @Override
    public void display() {
        System.out.println("Avg/Max/Min temperature = " + (tempSum / numReadings)
                + "/" + maxTemp + "/" + minTemp);
    }
}
氣象站測試類:

public class WeatherStation {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();

        new CurrentConditionsDisplay(weatherData);
        new StatisticsDisplay(weatherData);
        new ForecastDisplay(weatherData);

        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(82, 70, 29.2f);
        weatherData.setMeasurements(78, 90, 29.2f);
    }
}
讓我們看一下輸出結果:

Current conditions: 80.0F degrees and 65.0% humidity
Avg/Max/Min temperature = 80.0/80.0/80.0
Forecast: Improving weather on the way!
Current conditions: 82.0F degrees and 70.0% humidity
Avg/Max/Min temperature = 81.0/82.0/80.0
Forecast: Watch out for cooler, rainy weather
Current conditions: 78.0F degrees and 90.0% humidity
Avg/Max/Min temperature = 80.0/82.0/78.0
Forecast: More of the same
正如我們所見,我們在setMeasurements()方法裏更新了WeatherData的數據(相當於我們更改了主題的狀態),觀察者們就自動收到了來自主題的通知(我們將新的狀態都用display()給打印了出來),每一次主題更新,觀察者們就會收到新的推送。這與我們最初的設計意圖相符,並且體現出了觀察者模式的神奇功效。

二、觀察者的優缺點

到目前爲止,我們基本實現了一個較爲完整的觀察者模式並且見證了它的神奇功效,那麼觀察者模式有什麼優缺點呢?接下來讓我們對觀察者模式進一步的思考。

優點:

  • 觀察者模式可以實現表示層和數據邏輯層的分離,並定義了穩定的消息更新傳遞機制,抽象了更新接口,使得可以有各種各樣不同的表示層作爲具體觀察者角色。
  • 觀察者模式在觀察目標和觀察者之間建立一個抽象的耦合。
  • 觀察者模式支持廣播通信。
  • 觀察者模式符合“開閉原則”的要求。
缺點:

  • 如果一個觀察目標對象有很多直接和間接的觀察者的話,將所有的觀察者都通知到會花費很多時間。
  • 如果在觀察者和觀察目標之間有循環依賴的話,觀察目標會觸發它們之間進行循環調用,可能導致系統崩潰。
  • 觀察者模式沒有相應的機制讓觀察者知道所觀察的目標對象是怎麼發生變化的,而僅僅只是知道觀察目標發生了變化。
三、Java中內置的觀察者模式

在觀察者模式中,又有推(push)或拉(pull)兩種方式來傳送數據。

  • 推模式

 主題對象向觀察者推送主題的詳細信息,不管觀察者是否需要,推送的信息通常是主題對象的全部或部分數據。

  •  拉模式

    主題對象在通知觀察者的時候,只傳遞少量信息。如果觀察者需要更具體的信息,由觀察者主動到主題對象中獲取,相當於是觀察者從主題對象中拉數據。一般這種模型的實現中,會把主題對象自身通過update()方法傳遞給觀察者,這樣在觀察者需要獲取數據的時候,就可以通過這個引用來獲取了。

其實在JDK中,Java已經內置了對觀察者模式的支持,在java.util包內包含最基本的Observer接口和Observable類(也就是我們的Subject主題),使用內置的API更爲方便,讓我們來分析一下JDK中的觀察者API:

Observer觀察者:

public interface Observer {
    /**
     * 
     * @param   o     主題本身,好讓觀察者知道是哪個主題通知它的
     * @param   arg   傳入的數據對象,通過這個參數可以實現拉(pull)或者推(push)數據
     */
    void update(Observable o, Object arg);
}

Observable可觀察者:

public class Observable {
    private boolean changed = false;
    private Vector<Observer> obs;

    public Observable() {
        obs = new Vector<>();
    }

    /**
     * 用於註冊新的觀察者對象
     * @param   o   an observer to be added.
     * @throws NullPointerException   if the parameter o is null.
     */
    public synchronized void addObserver(Observer o) {
        if (o == null)
            throw new NullPointerException();
        if (!obs.contains(o)) {
            obs.addElement(o);
        }
    }

    /**
     * 用於刪除向量中的一個觀察者
     * @param   o   the observer to be deleted.
     */
    public synchronized void deleteObserver(Observer o) {
        obs.removeElement(o);
    }

    /**
     * 通知觀察者
     */
    public void notifyObservers() {
        notifyObservers(null);
    }

    /**
     * 多了一個對象參數,此方法可以傳送任何數據對象給每一個觀察者
     */
    public void notifyObservers(Object arg) {
        Object[] arrLocal;

        synchronized (this) {
            // 只有在changed標識爲true的時候纔會通知觀察者
            if (!changed)
                return;
            arrLocal = obs.toArray();
            // 通知完之後會把changed標識設回爲false
            clearChanged();
        }
        // 調用update()方法,通知每一個觀察者
        for (int i = arrLocal.length - 1; i >= 0; i--)
            ((Observer)arrLocal[i]).update(this, arg);
    }


    /**
     * 清空所有的觀察者
     */
    public synchronized void deleteObservers() {
        obs.removeAllElements();
    }

    /**
     * 將changed標識設爲true
     */
    protected synchronized void setChanged() {
        changed = true;
    }

    /**
     * 將changed標識設爲false
     */
    protected synchronized void clearChanged() {
        changed = false;
    }

    /**
     * 返回changed標識的狀態
     */
    public synchronized boolean hasChanged() {
        return changed;
    }

    /**
     * 返回觀察者的shu'liang
     */
    public synchronized int countObservers() {
        return obs.size();
    }
}
我們可以看到,Observer(觀察者)中的update方法多加了個一個可以傳遞數據的參數args,並且將主題也傳入了進來,這樣可以讓觀察者知道是哪個主題通知它的。而Observable(可觀察者,也就是前面我們說的主題)基本上與我們前面的實現類似,但是多了一個setChanged()方法,以及一個changed標識,該方法用來標記狀態已經改變的事實,好讓notifyObservers()知道它被調用時應該更新觀察者。如果調用notifyObservers()之前沒有先調用setChanged(),觀察者就不會被通知。

這樣做是有其必要性的,這樣可以讓我們在更新觀察者時,有更多的彈性,我們可以適當地通知觀察者。比如,如果沒有這個setChanged(),那麼我們的氣象站測量時,變化是十分敏銳的,溫度計讀書每十分之一度就會更新,這會造成WeatherData對象持續不斷地通知觀察者,這可不是我們希望看到的。

下面我們來看一下用JDK內置的API實現的氣象站:

可觀察者(WeatherData):

/**
 * 利用JDK裏提供的類來實現觀察者模式
 */
public class WeatherData extends Observable {
    private float temperature;  // 溫度
    private float humidity;     // 溼度
    private float pressure;     // 氣壓

    public WeatherData() { }

    public void measurementsChanged() {
        setChanged();   // 在調用notifyObservers()前先調用setChanged()來指示狀態已經改變
        notifyObservers();  // 沒有寫入參數,表明我們沒有傳數據,在此採用的是 拉(pull)
    }

    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    public float getTemperature() {
        return temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }
}

觀察者們:

/**
 * 觀察者1:實時顯示數據的告示板
 */
public class CurrentConditionsDisplay implements Observer, IDisplayElement {
    private Observable observable;
    private float temperature;
    private float humidity;

    /**
     * 傳入了一個可觀察者當參數,並將當前對象註冊成觀察者
     * @param observable 主題(可觀察者)
     */
    public CurrentConditionsDisplay(Observable observable) {
        this.observable = observable;
        observable.addObserver(this);
    }

    @Override
    public void update(Observable o, Object arg) {
        // 先確定觀察者是否屬於WeatherData類型,然後利用getter方法獲取溫度和溼度,之後調用display()
        if (o instanceof WeatherData) {
            WeatherData weatherData = (WeatherData) o;
            this.temperature = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            display();
        }
    }

    @Override
    public void display() {
        System.out.println("Current conditions: " + temperature
            + "F degrees and " + humidity + "% humidity");
    }
}
/**
 * 觀察者2:此佈告板根據氣壓計顯示天氣預報
 */
public class ForecastDisplay implements Observer, IDisplayElement {
    private float currentPressure = 29.92f;
    private float lastPressure;

    public ForecastDisplay(Observable observable) {
        observable.addObserver(this);
    }

    @Override
    public void update(Observable o, Object arg) {
        if (o instanceof WeatherData) {
            WeatherData weatherData = (WeatherData) o;
            lastPressure = currentPressure;
            currentPressure = weatherData.getPressure();
            display();
        }
    }

    @Override
    public void display() {
        System.out.print("Forecast: ");
        if (currentPressure > lastPressure) {
            System.out.println("Improving weather on the way!");
        } else if (currentPressure == lastPressure) {
            System.out.println("More of the same");
        } else if (currentPressure < lastPressure) {
            System.out.println("Watch out for cooler, rainy weather");
        }
    }
}
/**
 * 此佈告板跟蹤最小、平均、最大的觀測值,並顯示
 */
public class StatisticsDisplay implements Observer, IDisplayElement {
    private float maxTemp = 0.0f;
    private float minTemp = 200;
    private float tempSum = 0.0f;
    private int numReadings;

    public StatisticsDisplay(Observable observable) {
        observable.addObserver(this);
    }

    @Override
    public void update(Observable observable, Object arg) {
        if (observable instanceof WeatherData) {
            WeatherData weatherData = (WeatherData) observable;
            float temp = weatherData.getTemperature();
            tempSum += temp;
            numReadings++;

            if (temp > maxTemp) {
                maxTemp = temp;
            }

            if (temp < minTemp) {
                minTemp = temp;
            }

            display();
        }
    }

    @Override
    public void display() {
        System.out.println("Avg/Max/Min temperature = " + (tempSum / numReadings)
                + "/" + maxTemp + "/" + minTemp);
    }
}
氣象站測試類:

public class WeatherStation {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();

        new CurrentConditionsDisplay(weatherData);
        new ForecastDisplay(weatherData);
        new StatisticsDisplay(weatherData);

        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(82, 70, 29.2f);
        weatherData.setMeasurements(78, 90, 29.2f);
    }
}
測試結果:

Avg/Max/Min temperature = 80.0/80.0/80.0
Forecast: Improving weather on the way!
Current conditions: 80.0F degrees and 65.0% humidity
Avg/Max/Min temperature = 81.0/82.0/80.0
Forecast: Watch out for cooler, rainy weather
Current conditions: 82.0F degrees and 70.0% humidity
Avg/Max/Min temperature = 80.0/82.0/78.0
Forecast: More of the same
Current conditions: 78.0F degrees and 90.0% humidity
我們可以看到,結果與之前除了順序之外,沒有什麼不同,說明實現是成功了的。但是爲什麼會這樣呢?其實原因就在java.util.Observable實現的notifyObservers()方法,中間是採取的倒序update(),這不同於我們之前的次序。誰也沒有錯,只是雙方選擇的方式不同罷了。

但是可以肯定,我們的代碼依賴這樣的次序,就是有問題的。爲什麼呢?因爲一旦觀察者/可觀察者的實現有所改變,通知次序就會改變,很可能就會產生錯誤的結果,這絕不是我們所認爲的鬆耦合。

讓我們來看看API中的Observable的黑暗面:

1.Observable是一個 “類”,而不是一個“接口”,更糟的是,他甚至沒有實現一個接口。這樣的實現限制了它的使用和複用,雖然它的功能性並沒有問題,但是不符合我們上一篇中所提到的另一個設計原則:針對接口編程,而不是針對實現編程

2.Observable將關鍵的方法保護起來了:我們可以看到API中,setChanged()方法被標爲了protected,這意味着:除非你繼承自Observable,否則你無法創建實例並組合到自己的對象中來,這又違反了之前提到的設計原則:多用組合,少用繼承


總結:

1.觀察者模式定義了對象之間的一對多關係;

2.觀察者和可觀察者之間用鬆耦合方式結合(loosecoupling),可觀察者不知道觀察者的細節,只知道觀察者實現了觀察者的接口;
3.使用此模式時,你可以從被觀察者處推(push)或拉(pull)數據(然而,推的方式被認爲你更“正確”);

4.有多個觀察者時,不可以依賴特定的通知次序;

5.Java中有多重觀察者模式,包括了通用的java.util.Observable;

6.我們要注意java.util.Observable實現上帶來的一些問題;

7.如果有必要的話,可以實現一個自己的Observable(正如我們之前所做的,創建一個Subject接口),這並不難,也不復雜;

8.補充本章的一條設計原則:爲交互對象之間的鬆耦合設計而努力(鬆耦合的設計之所以能讓我們建立有彈性的OO系統,能夠應對變化,是因爲對象之間的互相依賴降到了最低)。


ps: 如有不同的見解,可以留言,如要轉載,請標明出處謝謝。

下一篇將對“裝飾者模式”做介紹。

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