設計模式(二):觀察者模式


一、『觀察者模式』定義

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

可以用【出版者+訂閱者=觀察者模式】的方式來類比。

如果你瞭解報紙的訂閱是怎麼回事,其實就知道觀察者模式是怎麼回事,只是名稱不太一樣:出版者改稱爲【主題】(Subject),訂閱者改稱爲【觀察者】(Observer)。用戶可以申請成爲訂閱者,也可以取消;而一旦出版者有新的報紙,訂閱者就可以收到。

這裏出版者只有一個,而訂閱者可以有多個,因此【主題】和【觀察者】定義了一對多的關係。【觀察者】依賴於此【主題】,只要【主題】狀態一有變化,【觀察者】就會被通知。根據通知的風格,【觀察者】可能因新值而更新。


二、場景

下面還是通過具體場景來一步步說明。

場景:某系統中有三個部分,分別是氣象站(獲取實際氣象數據的物理裝置)、WeatherData對象(追蹤來自氣象站的數據,並更新佈告板)和佈告板(顯示目前天氣狀況給用戶看)。

氣象站有很多感應裝置,比如溼度感應裝置、溫度感應裝置和氣壓感應裝置。WeatherData對象只有一個,而佈告板有多個,我們想要WeatherData對象把數據傳給所有需要數據的佈告板。

2.1 自定義觀察者模式

首先需要定義【主題】和【觀察者】接口,再讓WeatherData對象實現【主題】接口,佈告板實現【觀察者】接口。

/**
 * 主題接口
 */
interface Subject {
    // 註冊觀察者
    public void registerObserver(Observer observer);
    // 刪除觀察者
    public void removeObserver(Observer observer);
    // 主題改變,通知所有觀察者
    public void notifyObserver();
}

/**
 * 觀察者接口
 */
interface Observer {
    public void update(float temperature, float humidity, float pressure);
}

/**
 * WeatherData對象(實現主題接口),只有一個
 */
class WeatherData implements Subject {
    private List<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;

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

    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    public void removeObserver(Observer observer) {
        int i = observers.indexOf(observer);
        if (i >= 0) {
            observers.remove(i);
        }
    }

    public void notifyObserver() {
        for (Observer observer : observers) {
            observer.update(temperature, humidity, pressure);
        }
    }

    public void measurementsChanged() {
        notifyObserver();
    }

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

/**
 * 公告板1(實現觀察者接口),可以有多個不同的公告板
 */
class CurrentConditionsDisplay implements Observer {
    private float temperature;
    private float humidity;
    private float pressure;
    private Subject weatherData;

    public CurrentConditionsDisplay(Subject weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);  // 註冊
    }

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

    public void display() {
        System.out.println("temperature=" + temperature +
                ", humidity=" + humidity +
                ", pressure=" + pressure);
    }
}

/**
 * 測試
 */
public class WeatherStation {
    public static void main(String[] args) {
        // 主題
        WeatherData weatherData = new WeatherData();
        // 第一個觀察者
        CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);
        // 多次通知
        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(82, 70, 29.2f);
        weatherData.setMeasurements(78, 90, 29.2f);
    }
}

觀察者模式有兩種方式,分別是推(push)和拉(pull)方式。

  • 推(push)方式,即【主題】把數據全部推給【觀察者】。上面的代碼就是採用推方式。

  • 拉(pull)方式,即【觀察者】從【主題】拉取自己需要的數據。拉方式只需要在推方式的基礎上在【主題】里加上getter方法,讓【觀察者】可以通過調用getter方法獲取數據。

2.2 Java內置觀察者模式

Java API有內置的觀察者模式。java.util包內包含最基本的Observer接口與Observable類,這和我們的Subject接口與Observer接口很相似。內置的Observer接口與Observable類使用上更方便,因爲許多功能都已經事先準備好了。你也可以使用推(push)或拉(pull)的方式傳送數據。

2.2.1 內置觀察者模式的源代碼

Observale類(可觀察者,即【主題】)jdk1.8源代碼:

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

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

    public synchronized void addObserver(Observer o) {
        if (o == null)
            throw new NullPointerException();
        if (!obs.contains(o)) {
            obs.addElement(o);
        }
    }

    public synchronized void deleteObserver(Observer o) {
        obs.removeElement(o);
    }

    public void notifyObservers() {
        notifyObservers(null);
    }

    public void notifyObservers(Object arg) {
        // 臨時數組緩衝區,用作當前觀察者狀態的快照。
        Object[] arrLocal;
        synchronized (this) {
            if (!changed)
                return;
            arrLocal = obs.toArray();
            clearChanged();
        }
        for (int i = arrLocal.length-1; i>=0; i--)
            ((Observer)arrLocal[i]).update(this, arg);
    }

    public synchronized void deleteObservers() {
        obs.removeAllElements();
    }

    protected synchronized void setChanged() {
        changed = true;
    }

    protected synchronized void clearChanged() {
        changed = false;
    }

    public synchronized boolean hasChanged() {
        return changed;
    }

    public synchronized int countObservers() {
        return obs.size();
    }
}

有些時候【主題】改變了,但我們並不想通知【觀察者】,這時就可以使用changed標誌來進行判斷。需要通知時就調用setChanged()方法,把changed設爲true。不想通知時就調用clearChanged()方法,將changed狀態設置回false。另外也有一個hasChanged()方法,告訴你changed標誌的當前狀態。

Observer接口(即【觀察者】)jdk1.8源代碼:

public interface Observer {
    void update(Observable o, Object arg);
}

2.2.2 使用內置觀察者模式

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

/**
 * WeatherData對象(繼承內置被觀察者類Observale),只有一個
 */
class WeatherData extends Observable {
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData() {
    }

    public void measurementsChanged() {
        setChanged();
        notifyObservers();
    }

    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(實現觀察者接口),可以有多個不同的公告板
 */
class CurrentConditionsDisplay implements Observer {
    Observable observable;
    private float temperature;
    private float humidity;
    private float pressure;

    public CurrentConditionsDisplay(Observable observable){
        this.observable = observable;
        observable.addObserver(this);  // 註冊
    }

    public void update(Observable observable, Object arg) {
        if (observable instanceof WeatherData) {
            WeatherData weatherData = (WeatherData) observable;
            this.temperature = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            this.pressure = weatherData.getPressure();
            display();
        }
    }

    public void display() {
        System.out.println("temperature=" + temperature +
                ", humidity=" + humidity +
                ", pressure=" + pressure);
    }
}

/**
 * 測試
 */
public class WeatherStation {
    public static void main(String[] args) {
        // 主題
        WeatherData weatherData = new WeatherData();
        // 第一個觀察者
        CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);
        // 多次通知
        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(82, 70, 29.2f);
        weatherData.setMeasurements(78, 90, 29.2f);
    }
}

這裏採用的是拉方式。


三、總結

觀察者模式提供了一種對象設計,讓主題和觀察者之間松耦合。

關於觀察者的一切,主題只知道觀察者實現了某個接口(也就是Observer接口)。主題不需要知道觀察者的具體類是誰、做了些什麼或其他任何細節。

任何時候我們都可以增加新的觀察者。因爲主題唯一依賴的東西是一個實現Observer接口的對象列表,所以我們可以隨時增加觀察者。事實上,在運行時我們可以用新的觀察者取代現有的觀察者,主題不會受到任何影響。同樣的,也可以在任何時候刪除某些觀察者。

有新類型的觀察者出現時,主題的代碼不需要修改。假如我們有個新的具體類需要當觀察者,我們不需要爲了兼容新類型而修改主題的代碼,所有要做的就是在新的類裏實現此觀察者接口,然後註冊爲觀察者即可。主題不在乎別的,它只會發送通知給所有實現了觀察者接口的對象。

我們可以獨立地複用主題或觀察者。如果我們在其他地方需要使用主題或觀察者,可以輕易地複用,因爲二者並非緊耦合。

改變主題或觀察者其中一方,並不會影響另一方。因爲兩者是松耦合的,所以只要他們之間的接口仍被遵守,我們就可以自由地改變他們。

設計原則:爲了交互對象之間的松耦合設計而努力。


要點:

  1. 觀察者模式定義了對象之間一對多的關係。
  2. 主題(也就是可觀察者)用一個共同的接口來更新觀察者。
  3. 觀察者和可觀察者之間用松耦合方式結合(loosecoupl-ing),可觀察者不知道觀察者的細節,只知道觀察者實現了觀察者接口。
  4. 使用此模式時,你可從被觀察者處推(push)或拉(pull)數據(然而,推的方式被認爲更“正確”)。
  5. 有多個觀察者時,不可以依賴特定的通知次序。
  6. Java有多種觀察者模式的實現,包括了通用的java.util.Observable。
  7. 要注意java.util.Observable實現上所帶來的一些問題。比如Observable是一個類,Observable將關鍵的方法保護起來。

參考

《HeadFirst設計模式》

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