觀察者模式(發佈者-訂閱者模式)

設計模式是被發現的,而非發明。

觀察者模式(發佈者-訂閱者模式)

定義

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

設計原則

  • 找出程序中變化的方面,然後將其跟固定不變的方面相分離
  • 針對接口編程,而不是針對實現編程
  • 更多的使用組合,少用繼承
  • 爲了交互對象之間的鬆耦合設計努力,降低對象之間的互相依賴

使用場景舉例

我們要實現一個氣象站,當氣象站的數據(氣溫、氣壓等)有所變動時,就通知所有的顯示板更新顯示的狀態。
並且我們需要支持可以添加新的顯示板。

分析

爲了實現上述場景,我們需要將氣象站設定爲發佈者,將顯示板設定爲訂閱者(觀察者),
發佈者通過訂閱者實現的訂閱接口,可以隨時向所有訂閱者發佈消息。

示例代碼如下

       // 氣象站(發佈者)
    class WeatherData{
        constructor() {
            this.observerList=[]; //所有的觀察者
            this.temp; //溫度
            this.humidity;//溼度
            this.pressure;//氣壓
        }

        // 註冊觀察者
        registerObserver(observer){
            this.observerList.push(observer);
        }   
        // 刪除觀察者
        removeObserver(observer){
            var index = this.observerList.indexOf(observer);
            if(index>=0){
                this.observerList.splice(index,1);
            }
        }
        // 當狀態改變時,調用此方法通知所有觀察者   
        notifyObserver(){
            this.observerList.forEach(e=>{
                e.update(this.temp,this.humidity,this.pressure);
            })
        }
        // 修改氣象站數據時,通知觀察者
        setWeatherData(temp,humidity,pressure){
            this.temp = temp;
            this.humidity = humidity;
            this.pressure = pressure;
            this.notifyObserver();
        }
    }

    // 顯示板(訂閱者)
    class DispalyModul{
        constructor(weatherData) {
            this.temp; //溫度
            this.humidity;//溼度
            this.pressure;//氣壓
            weatherData.registerObserver(this); //將訂閱者註冊到發佈者上,當發佈者進行發佈信息時,即可實現訂閱者收到信息
        }
        // 用來接收發布者消息
        update(temp,humidity,pressure){
            this.temp = temp;
            this.humidity = humidity;
            this.pressure = pressure;
            this.dispaly();
        }
        dispaly(){
            console.log(`顯示板顯示:溫度-${this.temp},溼度-${this.humidity},氣壓-${this.pressure}`)
        }
    }
    // Temp顯示板(訂閱者)
    class DispalyTemp{
        constructor(weatherData) {
            this.temp; //溫度
            this.humidity;//溼度
            this.pressure;//氣壓
            weatherData.registerObserver(this); //將訂閱者註冊到發佈者上,當發佈者進行發佈信息時,即可實現訂閱者收到信息
        }
        // 用來接收發布者消息
        update(temp,humidity,pressure){
            this.temp = temp;
            this.humidity = humidity;
            this.pressure = pressure;
            this.dispaly();
        }
        dispaly(){
            console.log(`溫度顯示板:溫度-${this.temp}`)
        }
    }
    // Humidity顯示板(訂閱者)
    class DispalyHumidity{
        constructor(weatherData) {
            this.temp; //溫度
            this.humidity;//溼度
            this.pressure;//氣壓
            weatherData.registerObserver(this); //將訂閱者註冊到發佈者上,當發佈者進行發佈信息時,即可實現訂閱者收到信息
        }
        // 用來接收發布者消息
        update(temp,humidity,pressure){
            this.temp = temp;
            this.humidity = humidity;
            this.pressure = pressure;
            this.dispaly();
        }
        dispaly(){
            console.log(`溼度顯示板:溼度-${this.humidity}`)
        }
    }
    // Pressure顯示板(訂閱者)
    class DispalyPressure{
        constructor(weatherData) {
            this.temp; //溫度
            this.humidity;//溼度
            this.pressure;//氣壓
            weatherData.registerObserver(this); //將訂閱者註冊到發佈者上,當發佈者進行發佈信息時,即可實現訂閱者收到信息
        }
        // 用來接收發布者消息
        update(temp,humidity,pressure){
            this.temp = temp;
            this.humidity = humidity;
            this.pressure = pressure;
            this.dispaly();
        }
        dispaly(){
            console.log(`氣壓顯示板:氣壓-${this.pressure}`)
        }
    }

    let subject = new WeatherData();
    let observer = new DispalyModul(subject);
    let observerTemp = new DispalyTemp(subject);
    let observerHumidity = new DispalyHumidity(subject);
    let observerPressure = new DispalyPressure(subject);

    subject.setWeatherData(23.5,"45%",750);
    // 輸出如下:
    // 顯示板顯示:溫度-23.5,溼度-45%,氣壓-750
    // 溫度顯示板:溫度-23.5
    // 溼度顯示板:溼度-45%
    // 氣壓顯示板:氣壓-750

    subject.removeObserver(observerHumidity); //刪除溼度顯示板

    subject.setWeatherData(31,"30%",950);
    // 輸出如下:
    // 顯示板顯示:溫度-31,溼度-30%,氣壓-950
    // 溫度顯示板:溫度-31
    // 氣壓顯示板:氣壓-950

我們使用JS來演示,其他語言思路相通,因JS中沒有接口概念,所以此處違背了設計原則中 針對接口編程,而不是針對實現編程的思想
爲了不違背針對接口編程的思想,此處應當將weatherData中的registerObserver、removeObserver、notifyObserver與dispalyModul中的update抽出寫成接口實現。這樣既可以保證訂閱者必須實現update方法,又能同時保證代碼具有鬆耦合的優點。

問題

可以看出,上述實現了觀察者模式的代碼中,有個明顯的問題
發佈者發佈的消息,訂閱者必須接受,不能做到訂閱者根據自己的需求獲取數據

這其實並不是問題,而是設計思路的不同。我們可以設計成當前這種向所有訂閱者發佈消息的模式,也可以設計成訂閱者向發佈者索取想要的數據,又或者也可以兩者兼容。

例如,我們需要訂閱者單獨獲取溫度信息,我們只需要在發佈者與訂閱者中分別添加如下代碼

    // 氣象站(發佈者)
    class WeatherData{
        //```
        getTemp(){
            return this.temp
        }
        //```
    }
    // Temp顯示板(訂閱者)
    class DispalyTemp{
        constructor(weatherData) {
            //```
            this.weatherData = weatherData; // 保存發佈者
            weatherData.registerObserver(this); //將訂閱者註冊到發佈者上,當發佈者進行發佈信息時,即可實現訂閱者收到信息
        }
      // ```
      getTemp(){
        return this.weatherData.getTemp();
      }
      // ```
    }
    let observerTemp = new DispalyTemp(subject);
    observerTemp.getTemp();
    // 輸出 weatherData中的temp,未定義的時候 就是undefined

總結一下

觀察者模式:發佈者提供註冊接口(registerObserver),所有人都可以通過此接口註冊成爲訂閱者,同時需要實現統一的訂閱函數(update),而後發佈者通過調用訂閱函數來給所有註冊過的觀察者發送消息。

最後

代碼是死的,人是活的,寫代碼的過程中,我們會發現很多種設計模式的變種,依照設計思路的不同,需求的不同,我們要靈活的運用設計模式,我們最終的目的,是爲了設計出彈性的、可複用的、可維護的項目。

共勉。

發佈了139 篇原創文章 · 獲贊 203 · 訪問量 69萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章