設計模式之觀察者模式

1 定義

定義對象之間的一種一對多依賴關系,使得每當一個對象狀態(tài)發(fā)生改變時皱炉,其相關依賴對象皆得到通知并被自動更新怀估。觀察者模式的別名包括發(fā)布-訂閱(Publish/Subscribe)模式、模型-視圖(Model/View)模式合搅、源-監(jiān)聽器(Source/Listener)模式或從屬者(Dependents)模式多搀。觀察者模式是一種對象行為型模式。

要點就是灾部,觀察者模式定義了一些列對象之間的一對多的關系康铭,當一個對象改變狀態(tài),其他依賴者都會受到通知梳猪。
知曉定義之后玉吁,我們仍然需要了解他的應用場景:一個軟件系統(tǒng)常常要求在某一個對象的狀態(tài)發(fā)生變化的時候痹愚,某些其他的對象做出相應的改變。做到這一點的設計方案有很多贾惦,但是為了使系統(tǒng)能夠易于復用叠荠,應該選擇低耦合度的設計方案匿沛。減少對象之間的耦合有利于系統(tǒng)的復用,但是同時設計師需要使這些低耦合度的對象之間能夠維持行動的協(xié)調一致榛鼎,保證高度的協(xié)作逃呼。觀察者模式是滿足這一要求的各種設計方案中最重要的一種(此段摘抄,因為總結的真的好= =)者娱。

2 類圖

觀察者模式類圖.png
  • Subject:主題接口(被觀察者)抡笼,每個主題可以有多個觀察者。抽象主題提供了添加黄鳍,刪除和通知觀察者的抽象方法推姻。
  • Observer: 觀察者接口,所有潛在的觀察者必須實現它框沟,這個接口只有一個update方法藏古, 當主題改變時被調用增炭。
  • ConcreteSubject:具體的主題,實現了抽象方法拧晕,在notifyObservser隙姿,通知所有已添加的觀察者。
  • ConcreteObserver:具體的觀察者厂捞,可以是任意類输玷,必須實現update方法,當需要監(jiān)測主題的狀態(tài)變化靡馁,通過主題的add方法訂閱饲嗽。

在任何時候我們都可以增加新的觀察者,主題唯一依賴的是一個實現Observer接口的對象列表奈嘿,當有新的類出現時貌虾,我們無需修改主題的代碼,只需要將新類型實現觀察者接口裙犹,然后注冊為觀察者即可尽狠。可以看到叶圃,修改具體主題或者觀察者其中一方都不會影響另外一方袄膏,這種松耦合的設計更能應對變化,將對象之間的互相依賴降到最低掺冠,建立有彈性的OO系統(tǒng)沉馆。

3 簡單實現

正好最近室友在玩彩票,我們就用彩票例子來做一個簡單的實現德崭。假定彩票莊家是被觀察者斥黑,那么我們這些買彩票的玩家則為觀察者,我們通過買彩票跟莊家綁定訂閱關系眉厨,開獎的時候锌奴,莊家通知所有玩家中獎號碼,我們則會知道自己有沒有中獎憾股。

//抽象觀察者
interface UserObserver {
    void update(String winCode);
}
//具體觀察者(彩票玩家)
class LotteryTicketUser implements UserObserver {
    private String code;
    private String name;

    public LotteryTicketUser(String code, String name) {
        this.code = code;
        this.name = name;
    }
    @Override
    public void update(String winCode) {
       System.out.println("我是" + name + (winCode.equals(code) ? "我中獎了鹿蜀!" : "我沒中獎"));
    }
}
//抽象被觀察者(莊家)
interface Banker { 
    //提供添加,刪除服球,和通知觀察者的方法
    void add(UserObserver observer) ;
    void remove(UserObserver observer) ;
    void notify(String winCode);
}
//具體被觀察者(黑心莊家)
class BlackBanker implements Banker {
    private List<UserObserver> list = new ArrayList<>();

    @Override
    public void add(UserObserver observer) {
        list.add(observer);
    }

    @Override
    public void remove(UserObserver observer) {
        list.remove(observer);
    } 

    @Override
    public void notify(String winCode) {
        for (UserObserver observer : list) {
            observer.update(winCode);
        }
    }
}
//客戶端
public class Main {
    public static void main(String[] args) {
        BlackBanker blackBanker = new BlackBanker();
        LotteryTicketUser user1 = new LotteryTicketUser("111","黑子");
        LotteryTicketUser user2 = new LotteryTicketUser("222","軍哥");
        LotteryTicketUser user3 = new LotteryTicketUser("333","大將");
        blackBanker.add(user1);
        blackBanker.add(user2);
        blackBanker.add(user3);
        blackBanker.notify("444");
    }
}
//打印
我是黑子我沒中獎
我是軍哥我沒中獎
我是大將我沒中獎

中獎茴恰?不存在的。

4 兩種實現方式

簡單實現的只是一種實現方式斩熊,其實根據側重的功能往枣,觀察者分為兩種實現方式:

1.推模型

顧名思義,即被觀察者通知觀察者時,無論觀察者是否需要婉商,都會將全部或者部分信息推送至觀察者似忧,這些信息一般通過通知觀察者的方法(例如上面的update)傳遞。

2.拉模型

觀察者在接收通知的時候丈秩,可能不需要太多信息盯捌,而是在需要的時候自己去拉取,于是拉模型應運而生蘑秽。這種實現方式饺著,被觀察者只向觀察推送少量信息,一般來說是將自己的引用傳遞過去肠牲,以便觀察者在需要的時候幼衰,獲取自己的全部信息。

這里可以看到我們的簡單實現只是一種推的方式缀雳,如果是拉模型的場景渡嚣,可能就是比如有玩家需要知道莊家各種中獎信息,足彩肥印,籃彩各種等等识椰,那么這里我們可以通過update方法將莊家自身引用傳遞給玩家,玩家通過莊家提供一個獲取所有中獎的方法來查看深碱,關于具體代碼就不做演示了腹鹉。

5 案例分析

觀察者模式可謂是應用極廣,例如火了有一段時間的RxJava敷硅,EventBus這些消息隊列功咒、事件總線,又比如android中天天寫的OnclickListener(一對一)绞蹦,Adapter的notify力奋,甚至是MVC,都跟觀察者模式息息相關坦辟。太多的實例大家有興趣都可以研究一下刊侯,接下來我選其中一個結合源碼來探討一番。

1.Adapter的notifyDataSetChanged()

日常的開發(fā)離不開setAdapter锉走,同時使用Adapter的notifyDataSetChanged方法通知ListView數據刷新,先看BaseAdapter源碼:

public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {

    private final DataSetObservable mDataSetObservable = new DataSetObservable();

    public void registerDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.registerObserver(observer);
    }

    public void unregisterDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.unregisterObserver(observer);
    }
    
    public void notifyDataSetChanged() {
        mDataSetObservable.notifyChanged();
    }

可以看到BaseAdapter中包含了一個觀察者集合mDataSetObservable藕届,同時提供了增加挪蹭,刪除,通知訂閱者的方法休偶,我們來看mDataSetObservable.notifyChanged()

public class DataSetObservable extends Observable<DataSetObserver> {
 
    public void notifyChanged() {
        synchronized(mObservers) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    }

}

也就是遍歷觀察者們并調用他們的onChange方法梁厉。我們知道ListView和Adapter是通過setAdapter產生訂閱關系的,接下來來看這個方法:

@Override
    public void setAdapter(ListAdapter adapter) {
        if (mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }
        ...
        super.setAdapter(adapter);

        if (mAdapter != null) {
            mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
            mOldItemCount = mItemCount;
            mItemCount = mAdapter.getCount();
            checkFocus();
            //創(chuàng)建觀察者并為adapter添加觀察者
            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);
       ...

        }

很明顯在setAdapter中,新建了觀察者并綁定了訂閱關系词顾。從前面的分析得知八秃,notify的時候會調用觀察者的onChange方法,我們來看看觀察者的onChange:

class AdapterDataSetObserver extends AdapterView<ListAdapter>.AdapterDataSetObserver {
        @Override
        public void onChanged() {
            super.onChanged();
            if (mFastScroller != null) {
                mFastScroller.onSectionsChanged();
            }
        }
    }

啥也沒有肉盹,我們點到supet.onChange()里面:

class AdapterDataSetObserver extends DataSetObserver {
        @Override
        public void onChanged() {
            mDataChanged = true;
            mOldItemCount = mItemCount;
            mItemCount = getAdapter().getCount();
            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
                    && mOldItemCount == 0 && mItemCount > 0) {
                AdapterView.this.onRestoreInstanceState(mInstanceState);
                mInstanceState = null;
            } else {
                rememberSyncState();
            }
            checkFocus();
            //注意這里
            requestLayout();
        }
    }

一大串昔驱,但是我們只需要關注這里的requestLayout(),他通知了ListView的重新布局上忍。

小總結:
Adapter為被觀察者骤肛,ListView為觀察者(準確來說是ListView包含了一個觀察者),兩者通過setAdapter綁定發(fā)布-訂閱的關系(Adapter添加了這個觀察者)窍蓝。當Adapter調用notifyDataSetChanged()時腋颠,他就會遍歷他所添加的觀察者們(此處即為ListView中的觀察者),并調用他們的onChange()吓笙。而在onChange()中淑玫,會調用requestLayout(),從而使得ListView重新布局面睛,達到數據刷新的目的絮蒿。

要點總結

  • 觀察者模式定義了對象之間一對多的關系(事件監(jiān)聽等屬于特殊的一對一的觀察者模式)。
  • 松耦合設計侮穿,被觀察者不知道觀察者的實現細節(jié)歌径,只知道觀察者實現了自己通知他們的方法。
  • 觀察者模式的兩種實現方式亲茅,推模型和拉模型回铛。
  • 有多個觀察者時,不可以依賴特定的通知次序克锣。
  • 當一個主題有很多觀察者時茵肃,那么通知所有觀察者所花費的時間也是很多的。
  • 如果在被觀察者之間有循環(huán)依賴的話袭祟,被觀察者會觸發(fā)它們之間進行循環(huán)調用验残,導致系統(tǒng)崩潰,使用觀察者模式需要尤其注意巾乳。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末您没,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子胆绊,更是在濱河造成了極大的恐慌氨鹏,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件压状,死亡現場離奇詭異仆抵,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門镣丑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來舔糖,“玉大人,你說我怎么就攤上這事莺匠〗鹇穑” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵慨蛙,是天一觀的道長辽聊。 經常有香客問我,道長期贫,這世上最難降的妖魔是什么跟匆? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮通砍,結果婚禮上玛臂,老公的妹妹穿的比我還像新娘。我一直安慰自己封孙,他們只是感情好迹冤,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著虎忌,像睡著了一般泡徙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上膜蠢,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天堪藐,我揣著相機與錄音,去河邊找鬼挑围。 笑死礁竞,一個胖子當著我的面吹牛,可吹牛的內容都是我干的杉辙。 我是一名探鬼主播模捂,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蜘矢!你這毒婦竟也來了狂男?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤品腹,失蹤者是張志新(化名)和其女友劉穎并淋,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體珍昨,經...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了镣典。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兔毙。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖兄春,靈堂內的尸體忽然破棺而出澎剥,到底是詐尸還是另有隱情,我是刑警寧澤赶舆,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布哑姚,位于F島的核電站,受9級特大地震影響芜茵,放射性物質發(fā)生泄漏叙量。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一九串、第九天 我趴在偏房一處隱蔽的房頂上張望绞佩。 院中可真熱鬧,春花似錦猪钮、人聲如沸品山。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肘交。三九已至,卻和暖如春扑馁,著一層夾襖步出監(jiān)牢的瞬間涯呻,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工檐蚜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留魄懂,地道東北人。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓闯第,卻偏偏與公主長得像市栗,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子咳短,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內容