人人都會設計模式—觀察者模式–Observer

tigerchain發表於2019-03-03
觀察者模式大綱
觀察者模式大綱

PS:轉載請註明出處
作者: TigerChain
地址: www.jianshu.com/p/b972ba509…
本文出自 TigerChain 簡書 人人都會設計模式

教程簡介

  • 1、閱讀物件
    本篇教程適合新手閱讀,老手直接略過
  • 2、教程難度
    初級,本人水平有限,文章內容難免會出現問題,如果有問題歡迎指出,謝謝
  • 3、Demo 地址:github.com/githubchen0…

正文

一、什麼是觀察者模式

1、生活中的觀察者模式

1、警察抓小偷

在現實生活中,警察抓小偷是一個典型的觀察者模式「這以一個慣犯在街道逛街然後被抓為例子」,這裡小偷就是被觀察者,各個幹警就是觀察者,幹警時時觀察著小偷,當小偷正在偷東西「就給幹警傳送出一條訊號,實際上小偷不可能告訴幹警我有偷東西」,幹警收到訊號,出擊抓小偷。這就是一個觀察者模式

2、裝模作樣寫作業

小時候家裡家活比較多,爸媽讓我去幹活的時候,我偶爾會說我要學習「其實不想去幹活,當然只是偶爾,我還是常常幹家務的」,然後爸媽就去地裡了,我一個人在家裡,先擺出一張桌子「上面放好想看的書」,然後開啟電視機看起電視劇,但是又怕家裡人回到家中看到我在看電視,於是把家裡的大門鎖住「當爸媽回的時候肯定要開門」,當我聽見開門聲就會立馬關掉電視,做到作業桌上「裝模作樣寫作業」—-在這過程中:我充當的就是觀察者,爸媽就是被觀察者,他們開門就會觸發門響「相當於告訴我說他們回來了」,我聽到響聲「關電視,寫作業」,有過類似的經驗的朋友們下面點個贊

3、遠端視訊會議等

老闆和員工遠端開會:老闆是被觀察者,員工是觀察者。微信公號:微信公號作者是被觀察者,微信使用者是觀察者「當公號作者傳送一篇文章,關注了公號的觀察者都可以收到文章」等

2、程式中的觀察者模式

觀察者模式的定義

觀察者模式描述的是一種一對多的關係「一個被觀察者對應多個觀察者」,當被觀察者的狀態發生改變時,所有觀察者都會得到通知。通俗的理解:觀察者模式就是在特定的時刻「被觀察者傳送通知」幹特定的事情「觀察者收到通知處理自己相應的事件」

觀察者模式是一種一對多關係
觀察者模式是一種一對多關係

觀察者模式的特點

觀察者模式的三要素:觀察者,被觀察者,事件「訂閱」

觀察者模式的結構

角色 類別 說明
Subject 介面或抽象類 主題也叫被觀察者
RealSubject 真實的主題類 具體的被觀察者,內部維護了觀察者的列表
IObserver 觀察者介面或抽象類 抽象出觀察者的介面
RealObserver 具體的觀察者 被觀察者有更新,觀察者立馬響應更新

觀察者模式簡單的 UML

觀察者模式簡單的 UML
觀察者模式簡單的 UML

二、觀察者模式舉例

在舉例之前,我們先看看一個概念–回撥,什麼是回撥:就呼叫一圈又回到自已了「通俗的就可以這樣認為」

1、回撥

例子一:小明叫爸爸吃飯

舉個例子,小明媽媽做好了飯,讓小明去地裡叫他爸回來吃飯,小明說好的我馬上去,過了半個小時小明和他爸一起來了,小明給媽媽的說:“媽,爸回來了”,媽媽說:“好的我知道了,讓你爸洗洗手吃飯吧”,在這一過程中,小明給媽媽的說:“媽,爸回來了”就是一個回撥,不好理解?那看程式碼吧

小明叫爸爸吃飯簡單的 UML

小明叫爸爸吃飯簡單的 UML
小明叫爸爸吃飯簡單的 UML

寫程式碼

  • 1、定義一個回撥介面
回撥介面
回撥介面
  • 2、定義媽媽類
媽媽類
媽媽類
  • 3、定義小明類
小明類
小明類
  • 4、測試
測試類
測試類
  • 5、執行檢視結果
測試結果
測試結果

這就是回撥,我們看看的資料的走向 Mom–>xiaoming–>Mom 轉了一圈回來了,這就是回撥

例子二,模擬Android 中 View 的點選事件

經過例子一,我敢保證多數朋友對回撥還是稀裡糊塗,不要緊,我們再來一個例子感受一下,做過 Android 的朋友一定呼叫過 View.setOnclickListener(OnClickListener onClickListener) 點選函式,沒錯 OnClickListener 就是一個回撥介面,我們來使用 java 程式碼模擬一下這個過程

先看一下 UML

自定義 View 點選事件的 UML
自定義 View 點選事件的 UML

根據 UML 寫程式碼

  • 1、 定義一個 View 類
public class View {

    private OnClickListener onClickListener ;
    // 觸發點選事件
    protected void click(){
        if(onClickListener !=null){
            onClickListener.onClick(this);
        }
    }
    // 設定回撥
    public void setOnClickListener(OnClickListener onClickListener){
        this.onClickListener = onClickListener ;
    }

    public interface OnClickListener{
        // 定義回撥方法
        void onClick(View v) ;
    }
}複製程式碼
  • 2、定義一個 Button 類
/**
 * Created by TigerChain
 * 定義一個按鈕
 */
public class Button extends View {
    public void click(){
        super.click();
    }
}複製程式碼
  • 3、測試類 Test
public class Test {

    public static void main(String args[]){
        Button button = new Button() ;
        //看到了沒,看到這裡是不是很親切,是不是發現 次哦! 這就是回撥
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                System.out.print("自定義 View 的回撥事件");
            }
        });
        // 模擬使用者點選這個運作,Android 系統的 View 是重寫手勢來呼叫這個方法的,沒有暴露給使用者
        button.click();
    }
}複製程式碼
  • 4、執行檢視結果
測試自定義 View 點選事件結果
測試自定義 View 點選事件結果

如果你看 Android 原始碼,或是三方的原始碼會發現好多這樣的回撥方法,比如網路請求成功失敗的回撥等。

使用觀察者模式實現自定義 View

  • 定義 View 「被觀察者」
public class View {
    //被觀察者的列表
    private ArrayList<OnClickListener> onClickListeners = new ArrayList<>() ;
    // 觸發通知
    protected void click(){
        for(OnClickListener onClickListener:onClickListeners){
            if(onClickListener !=null){
                onClickListener.onClick(View.this);
            }
        }
    }
    // 註冊觀察者
    public void setOnClickListener(OnClickListener onClickListener){
        onClickListeners.add(onClickListener) ;
    }

    public interface OnClickListener{
        // 定義通知的方法
        void onClick(View v) ;
    }

    public void unRegister(OnClickListener onClickListener){
        if(onClickListeners.contains(onClickListener)){
            onClickListeners.remove(onClickListener) ;
        }
    }
}複製程式碼

注意這裡的 OnClickListener 就是抽象的觀察者

  • 2、定義一個 Button
/**
 * Created by TigerChain
 */
public class Button extends View {

}複製程式碼
  • 3、測試 Test
public class Test {
    public static void main(String args[]){
        //定義一個被觀察者
        Button button = new Button() ;
        //註冊其中一個觀察者
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                System.out.println("TigerChain");
            }
        });
        // 註冊另一個觀察者
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                System.out.println("never give up");
            }
        });
        // 被觀察者觸發事件
        button.click();
    }
}複製程式碼
  • 4、執行檢視結果
觀察者實現 View 的點選事件
觀察者實現 View 的點選事件

PS:總結看到了沒,觀察者模式和回撥是如此的相似,如果我們把上面的註冊觀察者改成一個「和 View 回撥一模一樣」,可以說回撥是一種特殊的觀察者模式,回撥和觀察者聯絡和區別

  • 1、回撥可以說只有一個觀察者,是一對一,是一種特殊的觀察者模式「我是這樣的認為的,個人觀點,如果有誤歡迎指出」
  • 2、觀察者被觀察者持有觀察的列表,是一種一對多的關係
  • 3、回撥是一種監聽方式,觀察者模式是一種解決方案「設計模式」

有了回撥的基礎,下面我們來看看觀察者模式的幾個精典例子

2、觀察者舉例

1、微信公號推送文章

最近看了我文章的人都知道我最近在寫關於設計模式這一系列,在這裡我「TigerChain」就是一個被觀察者,普通的微信使用者就是觀察者,如果微信使用者關注了 TigerChain ,那麼我推送的每一篇的文章,微信使用者就會第一時間收到我的文章「訂閱 TigerChain的使用者」,這就是一個典型的觀察者模式

微信公號推送文章簡單的 UML

微信公號推送文章簡單的 UML
微信公號推送文章簡單的 UML

根據 UML 擼碼

  • 1、定義抽象的被觀察者 IWxServerSubject.java
/**
 * Created by TigerChain
 * 定義主題「被觀察者介面」,所有的公號作者共同屬性「其實這裡功能上微信系統的功能,直接抽象成被觀察者」
 */
public interface IWxServerSubject {
    // 新增觀察者
    void attchObserver(IObserver iObserver) ;
    // 移除觀察者
    void detachObserver(IObserver iObserver) ;
    // 通知觀察者
    void notifyObserver() ;
}複製程式碼
  • 2、定義抽象的觀察者介面 Observer.Java
/**
 * Created by TigerChain
 * 定義觀察者介面,即關注公號的微信使用者共同屬性
 */
public interface Observer {
    // 觀察者收到資訊,內容為 info
    void reciveContent(String info) ;
}複製程式碼
  • 3、定義具體的被觀察者「公號作者 TigerChain」 TigerChainSubject.java
/**
 * Created by TigerChain
 * 定義一個真實的被觀察者 TigerChain「公號的作者」
 * 裡面存了訂閱 TigerChain 微信公眾賬號的讀者
 */
public class TigerChainSubject implements IWxServerSubject {

    // 訂閱者列表「觀察者列表」,即關注 TigerChain 公號的讀者
    private List<IObserver> observers = new ArrayList<>() ;
    //作者更新公號的內容 
    private String updateContent ;

    @Override
    public void attchObserver(IObserver iObserver) {
        observers.add(iObserver) ;
    }

    @Override
    public void detachObserver(IObserver iObserver) {
        if(observers.contains(iObserver)) {
            observers.remove(iObserver);
        }
    }

    @Override
    public void notifyObserver() {
        for (IObserver iObserver:observers) {
            iObserver.reciveContent(updateContent);
        }
    }

    /**
     * 是否關注我的公號
     * @param iObserver
     * @return
     */
    public boolean isAttchObserver(IObserver iObserver){
        return observers.contains(iObserver) ;
    }

    /**
     * TigerChain 在公號中釋出文章
     * @param updateContent
     */
    public void submitContent(String updateContent){
        this.updateContent = updateContent ;
        this.notifyObserver();
    }
}複製程式碼
  • 4、定義一個具體的觀察者「普通的微信使用者」 ReaderObserver.java
/**
 * Created by TigerChain
 * 微信使用者
 */
public class ReaderObserver implements Observer {

    // 微信使用者的姓名
    private String name ;

    public ReaderObserver(String name){
        this.uname = name ;
    }

    @Override
    public void reciveContent(String info) {
        System.out.println(uname+"注意,TigerChain 傳送了文章---"+info);
    }

    public String getUname(){
        return this.name ;
    }
}複製程式碼

可以看到微信使用者有接收推送文章的能力「前提是要關注公號作者」

  • 5、來個 Test 類測試一下吧
/**
 * Created by TigerChain
 * 測試類
 */
public class Test {
    public static void main(String args[]){

        IWxServerSubject iWxServerSubject = new TigerChainSubject() ;
        // 微信使用者
        ReaderObserver zhangsai = new ReaderObserver("張三") ;
        ReaderObserver lisi = new ReaderObserver("李四") ;
        ReaderObserver wangwu = new ReaderObserver("王五") ;
        ReaderObserver zhaoLiu = new ReaderObserver("趙六") ;

        // 微信使用者張三關注我的公號「即訂閱」
        iWxServerSubject.attchObserver(zhangsai);
        // 微信使用者李四關注我的公號「即訂閱」
        iWxServerSubject.attchObserver(lisi);
        // 微信使用者王五關注我的公號「即訂閱」
        iWxServerSubject.attchObserver(wangle);

        // 我「被觀察者」釋出了一篇文章--觀察者模式
        ((TigerChainSubject)iWxServerSubject).submitContent("人人都會設計模式:觀察者模式") ;

        boolean isAttch = ((TigerChainSubject)iWxServerSubject).isAttchObserver(zhaoLiu) ;
        if(!isAttch){
            System.out.println(zhaoLiu.getUname()+"你好!你還沒有關注 TigerChain ,請關注先,謝謝");
        }
    }
}複製程式碼

我們看到和現實情況一樣,普通微信使用者關注公號作者,然後作者傳送文章,使用者就可以收到文章了

  • 6、執行檢視結果
gierChain 傳送一篇文章結果
gierChain 傳送一篇文章結果

2、狼王開會

話說冬天來了,狼得找過冬的食物,狼王組織如開了緊急會議,下面的群狼都看著狼王傳遞會議精神和安排任務,此時狼王就是被觀察者,群狼就是觀察者,我們來看看 UML

狼王開會簡單的 UML

狼王開會簡單的 UML
狼王開會簡單的 UML

根據 UML 擼碼

  • 1、抽象被觀察者功能 Wolf.Java
/**
 * Created by TigerChain
 * 抽象的被觀察者
 */
public interface Wolf {
    // 新增觀察者
    void attchObserver(NormalWolf observer) ;
    // 移除觀察者
    void detchObserver(NormalWolf observer) ;
    // 通知觀察者
    void notifyObserver(String str) ;
}複製程式碼
  • 2、抽象觀察者普通的狼 NormalWolf.java
/**
 * Created by 抽象的觀察者,普通的狼
 */
public abstract class NormalWolf {

    // 拿到被觀察者的引用
    protected IWolf iWolf ;

    /**
     * 收到狼王下達的命令
     * @param str
     */
    public abstract void reciveCommand(String str) ;
}複製程式碼
  • 3、定義具體的被觀察者狼王 LangWang.java

由於一個狼群中只有一個狼王,所以狼王是一個單例

/**
 * Created by TigerChain
 * 狼王「被觀察者,下面的狼都看狼王的眼色行事」,是一個單例模式
 */
public class LangWang implements Wolf{

    private static LangWang instance ;
    private LangWang(){}
    public static LangWang getInstance(){
        if(instance == null){
            synchronized (LangWang.class){
                if(instance == null){
                    instance = new LangWang() ;
                }
            }
        }
        return instance ;
    }

    // 除過狼王外的狼「觀察者」
    private List<NormalWolf> observers = new ArrayList<>() ;
    // 狼王下達的命令
    private String mingLing  ;

    @Override
    public void attchObserver(NormalWolf observer) {
        observers.add(observer);
    }

    @Override
    public void detchObserver(NormalWolf observer) {
        if(observers.contains(observer)){
            observers.remove(observer) ;
        }
    }

    @Override
    public void notifyObserver(String str) {
        for(NormalWolf observer:observers){
            observer.reciveCommand(str);
        }
    }

    /**
     * 下達命令
     * @param mingLing
     */
    public void xiaDaMingling(String mingLing){
        this.mingLing = mingLing ;
        this.notifyObserver(mingLing);
    }
}複製程式碼
  • 4、定義一個觀察者偵查狼 ZhenChaLang.java
/**
 * Created by TigerChain
 * 偵查狼,另一個觀察者
 */
public class ZhenChaLang extends NormalWolf {

    public ZhenChaLang(IWolf iWolf){
        this.iWolf = iWolf ;
        this.iWolf.attchObserver(this);
    }

    @Override
    public void reciveCommand(String string) {
        System.out.println("偵查狼:狼王開會傳遞的資訊是 
"+string);
    }
}複製程式碼

在這裡我們例項化一個偵查狼的時候就會把它註冊到被觀察者中,也就是狼王開會的時候,群狼肯定狼群中的一員「外來狼可不行」,只有內部狼「內部會員」才有資格開會「這種關係就相當於註冊這個過程」

  • 5、定義另一個觀察者捕獵狼 BuLieLang.java
/**
 * Created by TigerChain
 * 捕獵狼---觀察者
 */
public class BuLieLang extends NormalWolf {

    public BuLieLang(IWolf iWolf){
        this.iWolf = iWolf ;
        // 新增觀察者,即捕獵狼放在狼王組織中
        this.iWolf.attchObserver(this);
    }

    @Override
    public void reciveCommand(String string) {
        System.out.println("捕獵狼:狼王開會傳遞的資訊是 
"+string+"
");
    }
}複製程式碼
  • 測試類 Test
/**
 * Created by TigerChain
 * 測試類
 */
public class Test {
    public static void main(String args[]){
        // 使用單例模式
        LangWang langWang = LangWang.getInstance() ;

        BuLieLang buLieLang = new BuLieLang(langWang) ;
        ZhenChaLang zhenChaLang = new ZhenChaLang(langWang) ;

        // 狼王下達命令就是傳送通知
        langWang.xiaDaMingling("1、分工合作,捕獵狼根據偵查狼反饋看機行事 
" +
                                     "2、偵查狼永遠把危險放在第一位,遇到危險第一時間提醒大家撤退");

    }
}複製程式碼
  • 6、執行檢視結果
狼王觀察者 demo 結果
狼王觀察者 demo 結果

狼王下達命令就是傳送通知,那麼現場中的狼都會收到通知,典型的觀察者模式

3、自定義 EventBus

在 Android 中我們常常使用 EventBus,它相當於是一個單例廣播,我們來自定義一個簡單的 EventBus 「不考慮執行緒切換」,其實它也是一種觀察者模式「俗稱釋出、訂閱模式」

自定義 EventBus 簡單的 UML

CustomEventBus_UML.jpg
CustomEventBus_UML.jpg

程式碼這裡不貼了,我已經上傳到 github 上了,大家可以自行看看:github.com/githubchen0…

三、Android 原始碼中的觀察者模式

1、RecyclerView 中使用觀察者模式

RecyclerView 中觀察者模式簡單的 UML

RecyclerView 中觀察者模式簡單的 UML
RecyclerView 中觀察者模式簡單的 UML

原始碼就不分析了「貼出程式碼估計又得一篇來說」,給出下面流程,大家自行看一下就明白了,動動手印象更深

從 setAdapter 開始看一下觀察者流程

setAdapter 觀察者的流程
setAdapter 觀察者的流程

2、ViewTreeObserver

ViewTreeObserver 是用來監聽檢視樹的觀察者,如果檢視樹發生全域性改變的時候就會收到通知

其中,被觀察者是 ViewTree ,觀察者是 ViewTreeObserver

ViewTreeObserver 的監聽器
ViewTreeObserver 的監聽器

抽取 ViewTreeObserver 部分程式碼講解

這裡說說 view.getViewTreeObserver().addOnGlobalLayoutListener(xxx) 場景,其它的雷同

public final class ViewTreeObserver {
    ...
 public interface OnGlobalLayoutListener {
        public void onGlobalLayout();
 }

    ...
// 新增監聽器
public void addOnGlobalLayoutListener(OnGlobalLayoutListener listener) {
        checkIsAlive();

        if (mOnGlobalLayoutListeners == null) {
            mOnGlobalLayoutListeners = new CopyOnWriteArray<OnGlobalLayoutListener>();
        }

        mOnGlobalLayoutListeners.add(listener);
    }
}

 ...
    // 分發事件相當於傳送通知,即被觀察者呼叫--View
  public final void dispatchOnGlobalLayout() {
        final CopyOnWriteArray<OnGlobalLayoutListener> listeners = mOnGlobalLayoutListeners;
        if (listeners != null && listeners.size() > 0) {
            CopyOnWriteArray.Access<OnGlobalLayoutListener> access = listeners.start();
            try {
                int count = access.size();
                for (int i = 0; i < count; i++) {
                    access.get(i).onGlobalLayout();
                }
            } finally {
                listeners.end();
            }
        }
    }複製程式碼

現在我們有了觀察者 ViewTreeObserver ,觀察者是 ViewTree 我們說了,主要問題的就是 dispatchOnGlobalLayout 誰呼叫了,只有觸發了這個方法那麼事件就回撥回來了「這個方法肯定是被觀察者呼叫了,系統呼叫的」,方法在 ViewRootImpl「關於 ViewRootImpl 可自行去檢視,不在本篇的範圍」 中體現出來了

看看 ViewRootImpl 的部分程式碼

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
 ...
 private void performTraversals(){
    ...
    // 執行測量
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
    // 執行佈局
    performLayout(lp, mWidth, mHeight);
    ...

    if (triggerGlobalLayoutListener) {
        mAttachInfo.mRecomputeGlobalAttributes = false;
        // 注意看這裡,這裡觸發了 dispatchOnGlobalLayout 方法,系統呼叫
        mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
    }
    ...
    // 執行繪製
    performDraw();
    ...
 }
 ...
}複製程式碼

看到了沒,mAttachInfo.mTreeObserver.dispatchOnGlobalLayout() 方法是在 ViewRootImpl 中呼叫了「即是 View 呼叫了,只要 View 樹而已發生改變,就會呼叫」,是由系統呼叫的「View 的佈局完成這後,就會呼叫」,並且還呼叫了自定義 View 的測量,佈局,繪製方法。

使用場景:比如我們想在 Activity 的 onCreate() 方法中取得某個 View 的寬高,此時是取不到的,由於佈局還沒有完成載入之前取到的是 0 ,所以使用 view.getViewTreeObserver().addOnGlobalLayoutListener(xxx) 裡面就可以獲取到 view 的寬高了,demo 程式碼如下

  view.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {// 當layout執行結束後回撥
                //使用完必須撤銷監聽(只測量一次),否則,會一直不停的不定時的測量,這比較耗效能
                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);//Added in API level 16
                //view.getViewTreeObserver().removeGlobalOnLayoutListener(this);//廢棄了
                int width = view.getMeasuredWidth();
                int width2 = view.getWidth();//和上面的值是一樣的
            }
        });複製程式碼

3、ListView

ListView 中使用觀察者模式和 RecyclerView 類似,大家可以扒扒這部分原始碼,這裡就不說了

四、觀察者模式的優缺點

優點

  • 1、解耦,被觀察者只知道觀察者列表「抽象介面」,被觀察者不知道具體的觀察者
  • 2、被觀察者傳送通知,所有註冊的觀察者都會收到資訊「可以實現廣播機制」

缺點

  • 1、如果觀察者非常多的話,那麼所有的觀察者收到被觀察者傳送的通知會耗時
  • 2、觀察者知道被觀察者傳送通知了,但是觀察者不知道所觀察的物件具體是如何發生變化的
  • 3、如果被觀察者有迴圈依賴的話,那麼被觀察者傳送通知會使觀察者迴圈呼叫,會導致系統崩潰

到此為止,我們的觀察者模式就說完了,一定要扒扒 Android 原始碼中相應的觀察者模式,你會有一種恍然大悟的感覺,點贊是一美德

以後文章會第一時間發在公號,請大家新增博文的公號,掃描新增即可關注
公眾號:TigerChain

TigerChain
TigerChain

相關文章