觀察者模式可以說是非常貼近我們生活的一個設計模式,為什么這么說呢?哲學上有這么一種說法,叫做“萬事萬物皆有聯系”,原意是說世上沒有孤立存在的事物,但其實也可以理解為任何一個事件的發生必然由某個前置事件引起,也必然會導致另一個后置事件。我們的生活中,充斥着各種各樣的相互聯系的事件,而觀察者模式,主要就是用於處理這種事件的一套解決方案。
示例
觀察者模式在不同需求下,實現方式也不盡相同,我們還是舉一個例子,然后通過逐步的改進來深刻感受一下它是如何工作的。
在中學階段有一篇課文《口技》,其中有一句“遙聞深巷中犬吠,便有婦人驚覺欠伸,其夫囈語。既而兒醒,大啼。”應該不用翻譯吧?我們接下來就是要通過程序模擬一下這個場景。
先看看他們之間的關系,如下圖所示:
初版實現
一聲狗叫引發了一系列的事件,需求很清晰,也很簡單。於是,我們可以很容易的得到如下實現:
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
的基類,如果同時作為被觀察者,除了用上述演進一的實現,還能如何實現?因為這種場景太常見了。
想清楚這兩個問題,觀察者模式才可能真正的展現出它的威力。