深入理解Java設計模式之觀察者模式

一、什麼是觀察者模式

在許多設計中,經常涉及多個對象都對一個特殊對象中的數據變化感興趣,而且這多個對象都希望跟蹤那個特殊對象中的數據變化,也就是說當對象間存在一對多關系時,在這樣的情況下就可以使用觀察者模式。當一個對象被修改時,則會自動通知它的依賴對象。

觀察者模式是關於多個對象想知道一個對象中數據變化情況的一種成熟的模式。觀察者模式中有一個稱作“主題”的對象和若幹個稱作“觀察者”的對象,“主題”和“觀察者”間是一種一對多的依賴關系,當“主題”的狀態發生變化時,所有“觀察者”都得到通知。

主要解決:一個對象狀態改變給其他對象通知的問題,而且要考慮到易用和低耦合,保證高度的協作。

二、觀察者模式的結構

觀察者模式的結構中包含四種角色:

(1)主題(Subject):主題是一個接口,該接口規定瞭具體主題需要實現的方法,比如,添加、刪除觀察者以及通知觀察者更新數據的方法。

(2)觀察者(Observer):觀察者是一個接口,該接口規定瞭具體觀察者用來更新數據的方法。

(3)具體主題(ConcreteSubject):具體主題是實現主題接口類的一個實例,該實例包含有可以經常發生變化的數據。具體主題需使用一個集合,比如ArrayList,存放觀察者的引用,以便數據變化時通知具體觀察者。

(4)具體觀察者(ConcreteObserver):具體觀察者是實現觀察者接口類的一個實例。具體觀察者包含有可以存放具體主題引用的主題接口變量,以便具體觀察者讓具體主題將自己的引用添加到具體主題的集合中,使自己成為它的觀察者,或讓這個具體主題將自己從具體主題的集合中刪除,使自己不再是它的觀察者。

三、觀察者模式的使用場景

(1)當一個對象的數據更新時需要通知其他對象,但這個對象又不希望和被通知的那些對象形成緊耦合。

(2)當一個對象的數據更新時,這個對象需要讓其他對象也各自更新自己的數據,但這個對象不知道具體有多少對象需要更新數據。

觀察者模式在實際項目的應用中非常常見,比如你到 ATM 機器上取錢,多次輸錯密碼,卡就會被 ATM吞掉,吞卡動作發生的時候,會觸發哪些事件呢?第一攝像頭連續快拍,第二,通知監控系統,吞卡發生;第三,初始化 ATM 機屏幕,返回最初狀態,你不能因為就吞瞭一張卡,整個 ATM 都不能用瞭吧,一般前兩個動作都是通過觀察者模式來完成的。觀察者可以實現消息的廣播,一個消息可以觸發多個事件,這是觀察者模式非常重要的功能。

使用觀察者模式也有兩個重點問題要解決:

廣播鏈的問題。

如果你做過數據庫的觸發器,你就應該知道有一個觸發器鏈的問題,比如表 A 上寫瞭一個觸發器,內容是一個字段更新後更新表 B 的一條數據,而表 B 上也有個觸發器,要更新表 C,表 C 也有觸發器…,完蛋瞭,這個數據庫基本上就毀掉瞭!我們的觀察者模式也是一樣的問題,一個觀察者可以有雙重身份,即使觀察者,也是被觀察者,這沒什麼問題呀,但是鏈一旦建立,這個邏輯就比較復雜,可維護性非常差,根據經驗建議,在一個觀察者模式中最多出現一個對象既是觀察者也是被觀察者,也就是說消息最多轉發一次(傳遞兩次),這還是比較好控制的;

異步處理問題。

被觀察者發生動作瞭,觀察者要做出回應,如果觀察者比較多,而且處理時間比較長怎麼辦?那就用異步唄,異步處理就要考慮線程安全和隊列的問題,這個大傢有時間看看 Message Queue,就會有更深的瞭解。

四、觀察者模式的優缺點

優點:

1、具體主題和具體觀察者是松耦合關系。由於主題接口僅僅依賴於觀察者接口,因此具體主題隻是知道它的觀察者是實現觀察者接口的某個類的實例,但不需要知道具體是哪個類。同樣,由於觀察者僅僅依賴於主題接口,因此具體觀察者隻是知道它依賴的主題是實現主題接口的某個類的實例,但不需要知道具體是哪個類。

2、觀察者模式滿足“開-閉原則”。主題接口僅僅依賴於觀察者接口,這樣,就可以讓創建具體主題的類也僅僅是依賴於觀察者接口,因此,如果增加新的實現觀察者接口的類,不必修改創建具體主題的類的代碼。。同樣,創建具體觀察者的類僅僅依賴於主題接口,如果增加新的實現主題接口的類,也不必修改創建具體觀察者類的代碼。

缺點:

1、如果一個被觀察者對象有很多的直接和間接的觀察者的話,將所有的觀察者都通知到會花費很多時間。

2、如果在觀察者和觀察目標之間有循環依賴的話,觀察目標會觸發它們之間進行循環調用,可能導致系統崩潰。

3、觀察者模式沒有相應的機制讓觀察者知道所觀察的目標對象是怎麼發生變化的,而僅僅隻是知道觀察目標發生瞭變化。

五、觀察者模式的實現

Observer類—抽象觀察者,為所有具體觀察者定義一個接口,在得到主題通知時更新自己。

這個接口叫做更新接口,抽象觀察者一般用一個抽象類或者一個接口實現。更新接口通常包括一個Update方法,這個方法叫做更新方法。

abstract class Observer
{
    public abstract void Update();
}

Subject類—主題或者抽象通知者,一般用一個抽象類或者一個接口實現。

它把所有對觀察者對象的引用保存到一個聚集裡,每個主題都可以有任何數量的觀察者。抽象主題提供一個接口,可以增加和刪除觀察者。

abstract class Subject
{
    private List<Observer> observers = new List<Observer>();
    //增加觀察者
    public void Attach(Observer observer)
    {
        observers.Add(observer);
    }
    //移除觀察者
    public void Detach(Observer observer)
    {
        observers.Remove(observer);
    }
    //通知
    public void Notify()
    {
        foreach (var item in observers)
        {
            item.Update();
        }
    }
}

ConcreteSubject類—具體主題或者具體通知者,將有關狀態存入具體觀察者對象;在具體主題的內部狀態改變時,給所有登記過的觀察者發送通知。

具體主題角色通常用一個具體類實現。

class ConcreteSubject : Subject
{
    private string subjectState;
    //具體被觀察者狀態
    public string SubjectState
    {
        get { return subjectState; }
        set { subjectState = value; }
    }
}

ConcreteObserver類—具體觀察者,實現抽象觀察者角色所要求的更新接口,以便使本身的狀態與主題的狀態相協調。

具體觀察者角色可以保存一個指向具體主題對象的引用。具體觀察者角色通常用一個具體類實現。

class ConcreteObserver : Observer
{
    private string name;
    private string observerState;
    private ConcreteSubject subject;
     public ConcreteObserver(ConcreteSubject subject, string name)
    {
        this.subject = subject;
        this.name = name;
    }
    public override void Update()
    {
        observerState = subject.SubjectState;
        Console.WriteLine("觀察者{0}的新狀態是{1}", name, observerState);
    }
    public ConcreteSubject Subject
    {
        get { return subject; }
        set { subject = value; }
    }
}

客戶端代碼

static void Main(string[] args)
{
    ConcreteSubject cs = new ConcreteSubject();
     cs.Attach(new ConcreteObserver(cs, "X"));
    cs.Attach(new ConcreteObserver(cs, "Y"));
    cs.Attach(new ConcreteObserver(cs, "Z"));
     cs.SubjectState = "ABC";
    cs.Notify();
     Console.Read();
}

結果

觀察者X的新狀態是ABC
觀察者Y的新狀態是ABC
觀察者Z的新狀態是ABC

六、觀察者模式和委托的結合

上述代碼盡管已經用瞭依賴倒轉原則,但是“抽象通知者”還是依賴“抽象觀察者”,也就是說,萬一沒有瞭抽象觀察者這樣的接口,這個通知功能就發送不瞭。

另外就是每個具體觀察者,它不一定是Update的方法調用。

目的:通知者和觀察者之間根本就互相不知道,由客戶端來決定通知誰

//通知者接口
interface Subject
{
    void Notify();
    string SubjectState { get; set; }
}

具體觀察者類

//看股票的同事
class StockObserver
{
    private string name;
    private Subject sub;
     public StockObserver(string name, Subject sub)
    {
        this.sub = sub;
        this.name = name;
    }
    //關閉股票
    public void CloseStock()
    {
        Console.WriteLine("{0}{1}關閉股票,繼續工作", sub.SubjectState, sub);
    }
}
//看NBA的同事
class NBAObserver
{
    private string name;
    private Subject sub;
     public NBAObserver(string name, Subject sub)
    {
        this.sub = sub;
        this.name = name;
    }
    //關閉NBA
    public void CloseNBA()
    {
        Console.WriteLine("{0}{1}關閉NBA,繼續工作", sub.SubjectState, sub);
    }
}

聲明一個委托,無參數,無返回值

//聲明一個委托,無參數,無返回值
delegate void EventHandler();

主題或者抽象通知者

//老板類
class Boss : Subject
{
    private string action;
    //聲明委托事件Update
    public event EventHandler Update;
     public string SubjectState
    {
        get { return action; }
         set { action = value; }
    }
     public void Notify()
    {
        //在訪問通知時,調用Update
        Update();
    }
}
//秘書類
class Secretary : Subject
{
    //與老板類類似,省略......
}

客戶端代碼

static void Main(string[] args)
{
    //老板張
    Boss Zhang = new Boss();
     StockObserver tongshi1 = new StockObserver("張三",Zhang);
    NBAObserver tongshi2 = new NBAObserver("李四",Zhang);
     Zhang.Update += new EventHandler(tongshi1.CloseStock);
    Zhang.Update += new EventHandler(tongshi2.CloseNBA);
     Zhang.SubjectState = "老板張駕到!";
    Zhang.Notify();
    Console.Read();
}

結果

老板張駕到!張三關閉股票,繼續工作
老板張駕到!李四關閉NBA,繼續工作

七、總結

實現觀察者模式的時候要註意,觀察者和被觀察對象之間的互動關系不能體現成類之間的直接調用,否則就將使觀察者和被觀察對象之間緊密的耦合起來,從根本上違反面向對象的設計的原則。無論是觀察者“觀察”觀察對象,還是被觀察者將自己的改變“通知”觀察者,都不應該直接調用。

另外redis裡的pub/sub也可以實現觀察者模式。

本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!

推薦閱讀: