Java基於回調的觀察者模式詳解

本文由「言念小文」原創,轉載請說明文章出處java

1、前言設計模式

什麼是回調?回調如何使用?如何優雅的使用?
本文將首先詳解回調的原理,而後介紹回調的基本使用方法,最後介紹基於回調的「觀察者模式」實現,演示
如何優化回調使用方法。多線程

 

2、什麼是回調併發

案例1
現有一農場須要向氣象局訂閱天氣預報信息。農場向氣象局發出訂閱請求,氣象局接受農場的訂閱請求後,
天天都會向農場推送後一天的天氣信息。農場天天接受到天氣預報信息,將作對應的生產安排,具體安排
以下:若是氣溫在0~10℃,播種小麥,若是氣溫在11~15℃播種大豆,若是氣溫在16~20℃播種棉花,不然
維護農場設備。異步

咱們從「案例1」中能夠提取回調的概念。
首先,農場向氣象局訂閱天氣預報信息,氣象局會在當天向農場發送第二天的天氣預報。這裏有兩個異步條件:
a.農場訂閱天氣預報後,氣象局不可能當即回覆此後每一每天氣預報信息;
b.農場並不知道氣象局會在前一天具體哪個精確時間點將天氣預報發送給本身。
所以「農場-氣象局」之間信息傳遞是異步的。
其次,農場接收到天氣預報信息後,纔會進行「工做安排」,因爲農場不知道天氣預報信息返回的精確時間,所以進行
「工做安排」的時機實際是由氣象局決定的。天然地,咱們想到將「工做安排」用一個函數(func())來實現,而且該函數的
具體實現由農場(Farm類)來實施,而函數的調用位置及調用時機由氣象局(MeteorologicalBureau類)來決定。這就是一個典型的回調場景,
而func()函數被稱之爲回調函數。下面咱們給出回調的通俗描述:
程序中某一模塊A(類/庫/其餘)中經過一段代碼(類/函數)實現某一功能(模塊A定義該功能實現的具體細節),但該段代碼
執行並不取決於模塊A,而是由模塊B(類/庫/其餘)決定,這時一般預先將該段代碼的入口地址做爲參數傳遞
給模塊B,由模塊B在程序的運行期間根據具體狀況來選擇什麼時候何地調用這段代碼。這一過程便稱做回調。
經過下圖直觀理解回調函數

3、如何使用回調
咱們經過實現「案例1」來演示如何使用回調測試

第一步,建立一個關於氣象局的監聽接口MeteorologicalBureauListener,該接口中聲明氣象局相關行爲的函數,
這裏聲明瞭天氣信息發佈函數onRelease()。優化

public interface MeteorologicalBureauListener {

    /**
     * 天氣信息發佈
     * @param description 天氣預報信息描述
     * @param minTemperature 最低氣溫
     * @param maxTemperature 最高氣溫
     * @param minWindscale 最低風力
     * @param maxWindscale 最高風力
     */
    void onRelease(String description, 
            int minTemperature, 
            int maxTemperature,
            int minWindscale,
            int maxWindscale);
}

第二步,建立氣象局類MeteorologicalBureau,該類負責接受天氣預報信息的訂閱和發佈天氣預報信息spa

/**
 * 氣象局(天氣預報信息發佈者)
 * @author WenYong
 *
 */
public class MeteorologicalBureau {

    private MeteorologicalBureauListener mListener;
    
    /**
     * 註冊對"氣象局"類監聽
     * @param listener
     */
    public void register(MeteorologicalBureauListener listener){
        if(null == listener){
            return;
        }
        mListener = listener;
    }
    
    /**
     * 取消註冊對"氣象局"類監聽
     * @param listener
     */
    public void unregister(MeteorologicalBureauListener listener){
        if(null == listener){
            return;
        }
        if(mListener.equals(listener)){
            mListener = null;
        }
    }
    
    /**
     * 天氣信息預測
     */
    public void predict(){
        new Thread(new Runnable() {
            
            public void run() {
                try {
                    // 模擬耗時操做
                    Thread.sleep(9000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                mListener.onRelease("明日渝北區氣溫8~10℃,風力5~8級", 8, 10, 5, 8);
            }
        }).start();
        
    }
    
}

第三步,建立農場類,該類訂閱天氣預報信息,並在接收到天氣預報信息後作對應工做安排線程

/**
 * 農場(天氣預報信息訂閱者)
 * @author WenYong
 *
 */
public class Farm {

    private MeteorologicalBureau mBureau;
    private MeteorologicalBureauListener mBureauListener;
    
    public Farm(MeteorologicalBureau bureau){
        mBureau = bureau;
    }
    
    /**
     * 訂閱天氣信息
     */
    public void subscribe(){
        System.out.println(TestUtils.getTimeStamp() + "," + "農場訂閱天氣預報信息");
        mBureauListener = new MeteorologicalBureauListener() {

            public void onRelease(String description, int minTemperature,
                    int maxTemperature, int minWindscale, int maxWindscale) {
                System.out.println(TestUtils.getTimeStamp() + "," 
                    + "農場接收到天氣信息:" + description);
                doAfterReceiveWheatherInfo(minTemperature, maxTemperature);
            }
        };
        mBureau.register(mBureauListener);
    }
    
    /**
     * 取消訂閱天氣信息
     */
    public void unsubscribe(){
        mBureau.unregister(mBureauListener);
    }
    
    /**
     * 接收到氣象局發佈的天氣信息後,農場作對應的工做安排
     * @param minTemperature 明日最低氣溫
     * @param maxTemperature 明日最高氣溫
     */
    private void doAfterReceiveWheatherInfo(int minTemperature, int maxTemperature){
        String timeStamp = TestUtils.getTimeStamp();
        if(minTemperature >= 0 && minTemperature <= 10){
            System.out.println(timeStamp + "," + "農場明日工做安排:播種小麥");
        }else if(minTemperature >= 11 && minTemperature <= 15){
            System.out.println(timeStamp + "," + "農場明日工做安排:播種大豆");
        }else if(minTemperature >= 16 && minTemperature <= 20){
            System.out.println(timeStamp + "," + "農場明日工做安排:播種棉花");
        }else{
            System.out.println(timeStamp + "," + "農場明日工做安排:維護設備");
        }
    }
    
}

第四步,編寫測試類,首先new一個農場對象並訂閱天氣預報信息,而後氣象局調用predict()函數預測天氣併發布預報

public class Test {

    public static void main(String[] args) {
        MeteorologicalBureau bureau = new MeteorologicalBureau();
        // 農場訂閱天氣信息
        new Farm(bureau).subscribe();
        // 氣象局進行天氣信息預測
        bureau.predict();
    }
}

第五步,運行,結果以下:
2019-02-06 21-54-59,農場訂閱天氣預報信息
2019-02-06 21-55-08,農場接收到天氣信息:明日渝北區氣溫8~10℃,風力5~8級
2019-02-06 21-55-08,農場明日工做安排:播種小麥

從結果不難看出,農場向氣象局訂閱天氣預報後,9s後氣象局向農場發佈了天氣預報信息,而後農場根據天氣預報信息
作出了對應工做安排。

原理分析:
好了,看到告終果以後,咱們來分析如何實現回調的。第一步在氣象局的監聽接口MeteorologicalBureauListener中聲明
了天氣信息發佈接口onRelease()。而後第二步在農場類Farm的天氣訂閱方法subscribe()中,之內部類對象的方式實現MeteorologicalBureauListener
接口並重寫onRelease()方法,而後經過mBureau.register(mBureauListener)將內部類對象傳遞給氣象局對象,這樣實際就將Farm對象中實現的onRelease()方法
傳遞給了氣象局對象。從而氣象局對象就能夠根據具體狀況來調用該方法了。再看測試類Test中,首先農場訂閱天氣信息的過程,就將
Farm對象中定義的接口方法onRelease()傳遞給了氣象局對象,而後氣象局對象調用predict(),該方法先模擬耗時9s,而後便執行了onRelease()方法,這樣
至關於便將天氣信息發佈給了Farm對象,因爲農場對象事先已定義好接收到天氣預報信息後的工做安排doAfterReceiveWheatherInfo(),故而當onRelease()被
氣象局回調後,緊接着便執行了農場的工做安排。

 

4、回調進階(基於回調的「觀察者模式」實現)
在學會了回調的基本使用方法後,咱們將案例1稍加修改,增長一個天氣預報訂閱者
案例2
現有一農場和一機場須要向氣象局訂閱天氣預報信息。農場和機場向氣象局發出訂閱請求,氣象局接受訂閱請求後,
天天都會向農場和機場推送後一天的天氣信息。農場天天接受到天氣預報信息,將作對應的生產安排,具體安排
以下:若是氣溫在0~10℃,播種小麥,若是氣溫在11~15℃播種大豆,若是氣溫在16~20℃播種棉花,不然
維護農場設備;機場接收到天氣預報信息,將採起對應的運營管理措施,具體以下:若是風力小於5級,不作預警正常起飛,
若是風力5~8級,預警起飛,若是風力大於8級,暫停起飛。

案例2中看一看出,氣象局發佈信息是一對多的關係,以下圖:


這即是咱們開發中常常遇到的觀察者模式(設計模式中的觀察者模式在此很少作介紹),農場和機場做爲「觀察者」向氣象局訂閱天氣預報信息,氣象局做爲信息發佈者
天天以一對多的方式,向農場和機場「廣播」信息。那麼如何經過回調實現」一對多「的信息發佈呢?

第一步,建立一個關於氣象局的監聽接口MeteorologicalBureauListener,該接口中聲明氣象局相關行爲的函數,
這裏聲明瞭天氣信息發佈函數onRelease()。

public interface MeteorologicalBureauListener {

    /**
     * 天氣信息發佈
     * @param description 天氣預報信息描述
     * @param minTemperature 最低氣溫
     * @param maxTemperature 最高氣溫
     * @param minWindscale 最低風力
     * @param maxWindscale 最高風力
     */
    void onRelease(String description, 
            int minTemperature, 
            int maxTemperature,
            int minWindscale,
            int maxWindscale);
}

第二步,建立氣象局類MeteorologicalBureau,該類負責接受天氣預報信息的訂閱和發佈天氣預報信息。須要注意
這裏使用了一個併發隊列來存儲機場和農場傳遞過來的MeteorologicalBureauListener實現對象的引用。之因此使用ConcurrentLinkedQueue
是爲了防止在後面遍歷的時候出現多線程問題:遍歷的同時被修改,從而致使軟件閃退。

/**
 * 氣象局(天氣預報信息發佈者)
 * @author WenYong
 *
 */
public class MeteorologicalBureau {

    private ConcurrentLinkedQueue<MeteorologicalBureauListener> mListenerQueue;
    
    public MeteorologicalBureau(){
        mListenerQueue = new ConcurrentLinkedQueue<>();
    }
    
    /**
     * 註冊對"氣象局"類監聽
     * @param listener
     */
    public void register(MeteorologicalBureauListener listener){
        if(null == listener){
            return;
        }
        if(mListenerQueue.contains(listener)){
            return;
        }
        mListenerQueue.add(listener);
    }
    
    /**
     * 取消註冊對"氣象局"類監聽
     * @param listener
     */
    public void unregister(MeteorologicalBureauListener listener){
        if(null == listener){
            return;
        }
        if(!mListenerQueue.contains(listener)){
            return;
        }
        mListenerQueue.remove(listener);
    }
    
    /**
     * 天氣信息預測
     */
    public void predict(){
        new Thread(new Runnable() {
            
            public void run() {
                try {
                    // 模擬耗時操做
                    Thread.sleep(9000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                release(mListenerQueue, "明日渝北區氣溫8~10℃,風力5~8級", 8, 10, 5, 8);
            }
        }).start();
        
    }
    
    /**
     * 天氣預報信息發佈
     * @param queue 監聽隊列
     * @param description 天氣預報描述
     * @param minTemperature 最低氣溫
     * @param maxTemperature 最高氣溫
     * @param minWindscale 最低風力
     * @param maxWindscale 最高風力
     */
    private void release(ConcurrentLinkedQueue<MeteorologicalBureauListener> queue,
            String description, 
            int minTemperature,
            int maxTemperature,
            int minWindscale,
            int maxWindscale){
        if(null == queue || queue.isEmpty()){
            return;
        }
        Iterator<MeteorologicalBureauListener> it = queue.iterator();
        while(it.hasNext()){
            it.next().onRelease(description, minTemperature, 
                    maxTemperature, minWindscale, maxWindscale);
        }
    }
    
}

第三步,建立農場類(農場類代碼和案例1相同這裏再也不重複貼出)和機場類

/**
 * 機場(天氣預報信息訂閱者)
 * @author WenYong
 *
 */
public class Airport {
    
    private MeteorologicalBureau mBureau;
    private MeteorologicalBureauListener mBureauListener;
    
    public Airport(MeteorologicalBureau bureau){
        mBureau = bureau;
    }
    
    /**
     * 訂閱天氣信息
     */
    public void subscribe(){
        System.out.println(TestUtils.getTimeStamp() + "," + "機場訂閱天氣預報信息");
        mBureauListener = new MeteorologicalBureauListener() {

            public void onRelease(String description, int minTemperature,
                    int maxTemperature, int minWindscale, int maxWindscale) {
                System.out.println(TestUtils.getTimeStamp() + "," 
                        + "機場接收到天氣信息:" + description);
                    doAfterReceiveWheatherInfo(minWindscale, maxWindscale);
            }
        };
        mBureau.register(mBureauListener);
    }
    
    /**
     * 取消訂閱天氣信息
     */
    public void unsubscribe(){
        mBureau.unregister(mBureauListener);
    }
    
    /**
     * 接收到氣象局發佈的天氣信息後,機場作對應的運營管理措施
     * @param minWindscale
     * @param maxWindscale
     */
    private void doAfterReceiveWheatherInfo(int minWindscale, int maxWindscale){
        String timeStamp = TestUtils.getTimeStamp();
        if(maxWindscale < 5){
            System.out.println(timeStamp + "," + "機場明日運營管理措施:不作預警正常起飛");
        }else if(minWindscale >= 5 && maxWindscale <= 8){
            System.out.println(timeStamp + "," + "機場明日運營管理措施:預警起飛");
        }else{
            System.out.println(timeStamp + "," + "機場明日運營管理措施:暫停起飛");
        }
    }

}

第四步,編寫測試類,首先分別new一個農場對象和機場對象,並訂閱天氣預報信息,而後氣象局調用predict()函數預測天氣併發布預報

public class Test {

    public static void main(String[] args) {
        MeteorologicalBureau bureau = new MeteorologicalBureau();
        // 農場訂閱天氣信息
        new Farm(bureau).subscribe();
        // 機場訂閱天氣信息
        new Airport(bureau).subscribe();
        // 氣象局進行天氣信息預測
        bureau.predict();
    }
}

第五步,運行,結果以下:
2019-02-07 10-35-54,農場訂閱天氣預報信息
2019-02-07 10-35-54,機場訂閱天氣預報信息
2019-02-07 10-36-03,農場接收到天氣信息:明日渝北區氣溫8~10℃,風力5~8級
2019-02-07 10-36-03,農場明日工做安排:播種小麥
2019-02-07 10-36-03,機場接收到天氣信息:明日渝北區氣溫8~10℃,風力5~8級
2019-02-07 10-36-03,機場明日運營管理措施:預警起飛

從結果能夠看出,農場和機場分別向氣象局訂閱了天氣預報信息,9s模擬耗時後,氣象局向它們發佈了天氣預報信息,兩者並根據對應
天氣信息做了對應工做安排和運營管理。

原理分析:
案例2中回調的實現原理與案例1中相同,在此再也不贅述。不一樣點在於案例2如何實現「一對多」的回調。氣象局類MeteorologicalBureau中使用
併發隊列mListenerQueue來存儲機場和農場傳遞過來的MeteorologicalBureauListener實現對象的引用,這樣氣象局就能夠調用兩者中實現的onRelease()方法。
MeteorologicalBureau中調用私有的release()來對mListenerQueue中對象實現遍歷,從而遍歷各訂閱對象中onRelease()方法。

5、結語回調是咱們平常開發工做中使用最爲基礎最爲頻繁的技術手段,不管是同步調用仍是異步調用場景(特別是異步調用使用尤爲多)有大量應用。若是您也是跟我當初同樣是初入行的小白,但願本文對您有用,另外在java開發中常常用到判null處理,本文代碼中常用在判null時,大量使用return處理,我的以爲這是一個好習慣,多使用return以減小邏輯判斷的嵌套,使代碼更容易閱讀。

相關文章
相關標籤/搜索