觀察者模式可以說是非常貼近我們生活的一個設計模式,為什麼這麼說呢?哲學上有這麼一種說法,叫做“萬事萬物皆有聯絡”,原意是說世上沒有孤立存在的事物,但其實也可以理解為任何一個事件的發生必然由某個前置事件引起,也必然會導致另一個後置事件。我們的生活中,充斥著各種各樣的相互聯絡的事件,而觀察者模式,主要就是用於處理這種事件的一套解決方案。
示例
觀察者模式在不同需求下,實現方式也不盡相同,我們還是舉一個例子,然後通過逐步的改進來深刻感受一下它是如何工作的。
在中學階段有一篇課文《口技》,其中有一句“遙聞深巷中犬吠,便有婦人驚覺欠伸,其夫囈語。既而兒醒,大啼。”應該不用翻譯吧?我們接下來就是要通過程式模擬一下這個場景。
先看看他們之間的關係,如下圖所示:
初版實現
一聲狗叫引發了一系列的事件,需求很清晰,也很簡單。於是,我們可以很容易的得到如下實現:
public class Wife
{
public void Wakeup()
{
Console.WriteLine("便有婦人驚覺欠伸");
}
}
public class Husband
{
public void DreamTalk()
{
Console.WriteLine("其夫囈語");
}
}
public class Son
{
public void Wakeup()
{
Console.WriteLine("既而兒醒,大啼");
}
}
public class Dog
{
private readonly Wife _wife = new Wife();
private readonly Husband _husband = new Husband();
private readonly Son _son = new Son();
public void Bark()
{
Console.WriteLine("遙聞深巷中犬吠");
_wife.Wakeup();
_husband.DreamTalk();
_son.Wakeup();
}
}
功能實現了,呼叫很簡單,就不上程式碼了,從Dog
類中可以看出,確實是狗叫觸發了後續的一系列事件。但是,有一定經驗的人一定很快就會發現,這裡至少違反了開閉原則和迪米特原則,最終會導致擴充套件維護起來比較麻煩。因此,需要改進,而改進的方法也不難想到,無非就是抽象出一個基類或介面,讓面向實現程式設計的部分變成面向抽象程式設計,而真正關鍵的是抽象什麼的問題。難道是抽象一個基類,然後讓Wife
,Husband
,Son
繼承自該基類嗎?他們都是家庭成員,看似好像可行,但它們並沒有公共的實現,而且如果後續再加入貓,老鼠或者其它什麼的呢?就會變得更加風馬牛不相及。面對這種未知的變化,顯然很難抽象出一個公共的基類,而針對“觀察事件發生”這個行為抽象出介面或許更合適。
演進一-簡易觀察者模式
根據這個思路,下面看看改進後的實現,先定義一個公共的介面:
public interface IObserver
{
void Update();
}
這裡定義了一個跟任何子類都無關的void Update()
方法,這也是沒辦法的辦法,因為我們不可能直接對Wakeup()
或者DreamTalk()
方法進行抽象,只能通過這種方式規範一個公共的行為介面,意思是當被觀察的事件發生時,更新具體例項的某些狀態。而具體實現類就簡單了:
public class Wife : IObserver
{
public void Update()
{
Wakeup();
}
public void Wakeup()
{
Console.WriteLine("便有婦人驚覺欠伸");
}
}
public class Husband: IObserver
{
public void DreamTalk()
{
Console.WriteLine("其夫囈語");
}
public void Update()
{
DreamTalk();
}
}
public class Son : IObserver
{
public void Update()
{
Wakeup();
}
public void Wakeup()
{
Console.WriteLine("既而兒醒,大啼");
}
}
這裡Update()
僅僅相當於做了一次轉發,當然,也可以加入自己的邏輯。改變較大的是Dog
類,不過也都是前面組合模式,享元模式等中用過的常用手法,如下所示:
public class Dog
{
private readonly IList<IObserver> _observers = new List<IObserver>();
public void AddObserver(IObserver observer)
{
_observers.Add(observer);
}
public void RemoveObserver(IObserver observer)
{
_observers.Remove(observer);
}
public void Bark()
{
Console.WriteLine("遙聞深巷中犬吠");
foreach (var observer in _observers)
{
observer.Update();
}
}
}
不難理解,由於Wife
,Husband
,Son
都實現了IObserver
介面,因此可以通過IList<IObserver>
集合進行儲存,同時通過AddObserver(IObserver observer)
和RemoveObserver(IObserver observer)
對具體例項進行新增和刪除管理。
再看看呼叫的程式碼:
static void Main(string[] args)
{
Dog dog = new Dog();
Wife wife = new Wife();
Husband husband = new Husband();
Son son = new Son();
dog.AddObserver(wife);
dog.AddObserver(husband);
dog.AddObserver(son);
dog.Bark();
Console.WriteLine("----------------------");
dog.RemoveObserver(son);
dog.Bark();
}
其實,這就是需求最簡單的觀察者模式了,其中Dog
是被觀察者,也就是被觀察的主題,而Wife
,Husband
,Son
都是觀察者,下面看看它的類圖:
從這個類圖上,我們可能會發現一個問題,既然觀察者實現了一個抽象的介面,那麼被觀察者理所應當也應該實現一個抽象的介面啊,畢竟面向介面程式設計嘛!是的,但是該實現介面還是繼承抽象類呢?我們暫且擱置,先疊加一個需求看看。
演進二
翻翻課本可以看到,“遙聞深巷中犬吠,便有婦人驚覺欠伸,其夫囈語。既而兒醒,大啼。”,後面還有三個字“夫亦醒。”(後面還有很多,為防止過於複雜,我們就不考慮了),我們再來看看他們之間的關係:
結合上下文可以知道,丈夫是被兒子哭聲吵醒的,而不是狗叫。依據這些,我們可以分析出以下三點:
- 被觀察者有兩個,一個是狗,一個是兒子;
- 丈夫觀察了兩件事,一個是狗叫,一個是兒子哭;
- 兒子既是觀察者,又是被觀察者。
感覺一下子複雜了好多,不過好在有了前面的鋪墊,實現起來,好像也並不是特別困難,Wife
和Dog
沒有任何變化,主要需要修改的是Husband
和Son
,程式碼如下:
public class Husband : IObserver
{
public void DreamTalk()
{
Console.WriteLine("其夫囈語");
}
public void Update()
{
DreamTalk();
}
public void Wakeup()
{
Console.WriteLine("夫亦醒");
}
}
public class Son : IObserver
{
private readonly IList<IObserver> _observers = new List<IObserver>();
public void AddObserver(IObserver observer)
{
_observers.Add(observer);
}
public void RemoveObserver(IObserver observer)
{
_observers.Remove(observer);
}
public void Update()
{
Wakeup();
}
public void Wakeup()
{
Console.WriteLine("既而兒醒,大啼");
foreach (var observer in _observers)
{
observer.Update();
}
}
}
可以看到,Husband
多了一個Wakeup()
方法,Son
同時實現了觀察者和被觀察者的邏輯。
當然,呼叫的地方也有了一些變化,畢竟Son
的地位不同了,程式碼如下:
static void Main(string[] args)
{
Dog dog = new Dog();
Wife wife = new Wife();
Husband husband = new Husband();
Son son = new Son();
dog.AddObserver(wife);
dog.AddObserver(husband);
dog.AddObserver(son);
son.AddObserver(husband);
dog.Bark();
}
看到這裡,細心的人會發現這段程式碼存在著很多問題,至少有以下兩點:
Dog
和Son
中存在著大量重複的程式碼;- 執行一下會發現
Husband
的功能沒有實現,因為Husband
中沒有標識事件的型別或來源,因此也就不知道是該說夢話還是該醒過來。
演進三-標準觀察者模式
為了解決上述兩個問題,我們需要再做一次改進,首先第一個程式碼重複的問題,很明顯提取一個共同的基類就可以解決,而第二個問題必須通過傳參來加以區分了,我們可以先定義一個攜帶事件引數的類,事件引數通常至少包含事件來源以及事件型別(當然也可以包含其它的屬性),程式碼如下:
public class EventData
{
public object Source { get; set; }
public string EventType { get; set; }
}
改造的觀察者介面和提取的被觀察者基類如下:
public interface IObserver
{
void Update(EventData eventData);
}
public class Subject
{
private readonly IList<IObserver> _observers = new List<IObserver>();
public void AddObserver(IObserver observer)
{
_observers.Add(observer);
}
public void RemoveObserver(IObserver observer)
{
_observers.Remove(observer);
}
public void Publish(EventData eventData)
{
foreach (var observer in _observers)
{
observer.Update(eventData);
}
}
}
可以看到,觀察者IObserver
中加入了事件引數,被觀察者Subject
既沒有使用介面,也沒有使用抽象類,原則上,這樣是不合適的,但是,這個類中實在是沒有抽象方法,也不適合用抽象類,所有隻能勉強使用普通類了。
其它程式碼如下:
public class Wife : IObserver
{
public void Update(EventData eventData)
{
if (eventData.EventType == "DogBark")
{
Wakeup();
}
}
public void Wakeup()
{
Console.WriteLine("便有婦人驚覺欠伸");
}
}
public class Husband : IObserver
{
public void DreamTalk()
{
Console.WriteLine("其夫囈語");
}
public void Update(EventData eventData)
{
if (eventData.EventType == "DogBark")
{
DreamTalk();
}
else if (eventData.EventType == "SonCry")
{
Wakeup();
}
}
public void Wakeup()
{
Console.WriteLine("夫亦醒");
}
}
public class Son : Subject, IObserver
{
public void Update(EventData eventData)
{
if (eventData.EventType == "DogBark")
{
Wakeup();
}
}
public void Wakeup()
{
Console.WriteLine("既而兒醒,大啼");
Publish(new EventData { Source = this, EventType = "SonCry" });
}
}
public class Dog : Subject
{
public void Bark()
{
Console.WriteLine("遙聞深巷中犬吠");
Publish(new EventData { Source = this, EventType = "DogBark" });
}
}
可以看到,被觀察者通過Publish(EventData eventData)
方法將事件發出,而觀察者通過引數中的事件型別來決定接下來該執行什麼動作,下面是它的類圖:
這其實就是GOF定義的觀察者模式了。
定義
多個物件間存在一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都得到通知並被自動更新。
UML類圖
將上述例項的類圖簡化一下,就可以得到如下觀察者模式的類圖了:
- Subject:抽象主題角色,它是一個抽象類(而實際上我用的是普通類),提供了一個用於儲存觀察者物件的集合和增加、刪除以及通知所有觀察者的方法。
- ConcreteSubject:具體主題角色。
- IObserver:抽象觀察者角色,它是一個介面,提供了一個更新自己的方法,當接到具體主題的更改通知時被呼叫。
- Concrete Observer:具體觀察者角色,實現抽象觀察者中定義的介面,以便在得到主題的更改通知時更新自身的狀態。
優缺點
優點
- 降低了主題與觀察者之間的耦合關係;
- 主題與觀察者之間建立了一套觸發機制。
缺點
- 主題與觀察者之間的依賴關係並沒有完全解除,而且有可能出現迴圈引用;
- 當觀察者物件很多時,事件通知會花費很多時間,影響程式的效率。
當然,這裡的缺點指的是觀察者模式的缺點,上述例項的缺點其實會更多,我們後續再想辦法解決。
通知模式
其實觀察者模式中,事件的通知無外乎兩種模式-推模式和拉模式,這裡簡單的解釋一下。我們上述的實現使用的都是推模式,也就是由主題主動將事件訊息推送給觀察者,好處就是實時高效,這也是較為推薦的一種方式。
但是並非所有場景都適合使用推模式,例如,某主題有非常多的觀察者,但是每個觀察者都只關注主題的某個或某些狀態,這時使用推模式就不太合適了,因為推模式會將主題的所有狀態不加區分的推送給所有觀察者,對觀察者而言,得到的訊息就過於臃腫駁雜了。這時就可以採用拉模式了,主題公開所有可以被觀察的狀態,由觀察者主動拉取自己關注的部分。
而拉模式根據不同情況又可以有兩種實現。一種方式是由觀察者定時檢查,並拉取資料,這種操作簡單粗暴,但是,會給主題造成較大的效能負擔,同時,也會因為檢查頻率的不同而帶來不同程度的延時。而另一種方式還是由主題主動發出通知,不過通知不帶任何引數,僅僅是告訴觀察者主題有變化了,然後由觀察者去拉取自己關注的部分,這正是拉模式中最常採用的一種手段。
總結
好了,GOF定義的觀察者模式分析完了,但實際上,觀察者模式還遠遠沒有結束,限於篇幅,我們在下一篇中接著分析。不過在這之前,可以提前思考一下下面兩個問題:
dog.AddObserver(...)
真的合適嗎?實際生活中,狗真的有這種能力嗎?- 我們知道
C#
中不支援多繼承,如果Dog
本身繼承自Animal
的基類,如果同時作為被觀察者,除了用上述演進一的實現,還能如何實現?因為這種場景太常見了。
想清楚這兩個問題,觀察者模式才可能真正的展現出它的威力。