通過上篇的介紹我們知道了觀察者模式的基本特點、使用場合以及如何以C++語言實現。有過多次編寫觀察者模式代碼經驗的你也許會發現,幾乎所有的案例存在為數相當可觀的重復性代碼:定義一個觀察者接口;定義一個主題並實現其諸如注冊一/多個觀察者,移除一/多個觀察者,廣播至所注冊的觀察者等基本行為。既然如此,我們有沒有可能為所有觀察者模式抽象出共有的接口與行為,以便日後復用呢?
此篇文章便是探討如何實現一個通用或稱為萬能的觀察者模式庫。
我們為所有的觀察者/訂閱者抽象出一個共有的接口IObserver:
struct IObserver{
virtualvoidupdate()=0;
virtual~Observer(){}
};
當主題狀態發生改變時IObserver對象的update方法會被自動調用。IObserver的子類會實現update方法,以便具有其特定的行為。考慮到update方法的具體實現,大部分情況下我們需要查詢主題的狀態,從而做出反應。這有多種實現方案:一是生成全局或類似全局性(如Singleton技術)的主題對象:
Subjectg_subject;
...
structConcreteObserver:public IObjserver{
virtualvoidupdate(){
if(g_subject.getStatus()==xxx){
...
}
};
因為“盡可能的不要使用全局對象”緣故,這種方式不常用。二是為update方法增加一個參數,以便告知update某些必要的信息,為具有普遍性,我以
Event代表此類,定義如下:
struct Event{
Event(Subject &subject);
BasicSubject*getSubject();
virtual~Event(){}
};
很明顯,這應該是個基類,所以具有需析構方法,此外,Event還提供一個獲取主題的方法。BasicSubject類是我們隨後要說到的主題基類。這樣,IObserver接口的定義看起來應該是這樣:
struct IObserver{
virtualvoidupdate(Event&event)=0;
virtual~IObserver(){}
};
接下來處理我們的主題,根據前面所提到的它應該具有的行為,它的定義應該大致像這樣:
class BasicSubject{
public:
virtual ~BasicSubject() {}
voidaddObserver(IObserver&observer);
voidremoveObserver(IObserver&observer);
protected:
voidnotifyAll(Event&event);
protected:
std::list<Observer*>observers_;
};
BasicSubject基類有三個方法,分別是增加一個觀察者,移除一個觀察者以及通知已注冊觀察者。至於其實現,我留給讀者,當作練習。
現在讓我們通過以上三個基類(Event、IObserver及BasicSubject)來重新實現在上篇中所給出的例子:
structMMEvent:publicEvent{
MMEvent(MMInteligenceAgent &sub):Event(sub){}
};
structMMInteligenceAgent:public BasicSubject{
MMStatusgetStatus()const{returnstatus_;}
voidtrace(){notifyAll(MMEvent(this));} // for demonstrating how to use nofifyAll method.
private:
MMStatusstatus_;
};
structLarcener:public IObserver{
virtualvoidupdate(MMStatusstatus){
if(status==Sleeping){
...
}
}
};
現在是不是簡單了許多?
不要停止你的腳步,更不要高興的過早。
我們事先定義了三個接口讓我們的客戶遵循,約束太多了。
主題Subject與觀察者Observer之間雖然已是抽象耦合(相互認識對方的接口基類),但仍可改進,使兩者間的耦合度更低。
考慮到UI中的窗口設計,需要監視的窗口事件可能有:
windowOpened
windowClosing
windowIconified
windowDeiconified
windowActivated
windowActivated
windowDeactivated
倘若代碼全由你一人設計,你大可將以上7個事件合並為一個粗事件並通過窗口(也就是這裡的Subject了)提供一個標志表明目前發生的是這7個中的哪一個事件,這沒什麼問題。但是,我相信並不是所有代碼都由你一人包辦,設想你的同事或是客戶將WindowEventListener(也就是這裡的Observer)設計成幾個獨立的更新方法的情況吧(java便是如此)。糟糕,我們目前定義的IObserver接口只支持單一更新方法。
是時候將我們的設計改進了。
事實上,在我們定義的三個基類當中最沒有意義的便是IObserver接口,它什麼也沒幫我們實現,僅是個Tag標記,以便我們能為BasicSubject類指明addObserver及removeObserver方法的參數。通過模板技術,我們不必定義IObserver接口:
template<
classObserverT,
classContainerT=std::list<ObserverT*>
>
classBasicSubject
{
public:
inline voidaddObserver(ObserverT&observer);
inline voidremoveObserver(ObserverT&observer);
protected:
ContainerTobservers_;
};
BasicSubject不需要虛析構函數,因為客戶不需要知道BasicSubject類的存在。類模板參數ContainerT的存在是為了讓客戶可以選擇容器類型,默認容器類型是std::list,也許你的客戶更喜歡std::vector,於是他便可這樣使用:
class MyBasicSubject : public BasicSubject<MyObserver, std::vector<MyObserver*> { ...};
當BasicSubject狀態改變時需要通知觀察者,所以notifyAll方法仍不可缺少。考慮到觀察者可能具有多個更新方法,我們可以通過notifyAll方法的參數來指定要更新的方法。是的,就是函數指針了。所以nofifyAll方法可能是這樣的:
template<typenameReturnT,typenameArg1T>
voidBasicSubject::notifyAll(ReturnT(ObserverT::*pfn)(Arg1T),Arg1Targ1){
for(ContainerT::iteratorit=observers_.begin(),itEnd=observers_.end();it!=itEnd;++it){
((*it)->*pfn)(arg1);
}
}
其中pfn是指向ObserverT類的、具有ReturnT返回類型的、接收一個類型為Arg1T參數的函數的指針。
現在連Event基類都不需要了,其角色完全由模板參數類型Arg1T所取代。
問題遠沒有結束。
仔細想想Arg1T參數類型的推導,編譯器既可選擇從pfn函數所聲明的形參類型中推導也可選擇從arg1實參推導,當實參(arg1)類型可唯一推導且與pfn函數聲明的形參類型完全匹配時沒問題。當實參類型與形參類型不匹配時編譯器報錯。如:
structMyObserver{
voidincrement(int &val){++val;}
};
structMySubject:publicBasicSubject<MyObserver>{
voidtrigger(){
inti=10;
notifyAll(&MyObserver::increment,i);
}
};
我的編譯器上的報錯信息大致是:"template parameter 'Arg1T' is ambiguous" ... "could be 'int' or 'int &'"。編譯器不知道Arg1T是int(從實參i推導)還是int&(從函數increment形參val推導)。編譯器真傻。
此問題的根源是模板參數
多渠道推導的不匹配性所致。為避免多渠道推導,聰明的你可能想到這樣定義notifyAll方法:
template<typenameMemFunT,typename Arg1T>
void BasicSubject::notifyAll(constMemFunT&pfn, Arg1T &arg1);
值得表揚。
設想pfn所聲明的形參類型是const引用類型(如const int&)而用戶把常量(如10)直接用作實參的情形吧:
structMyObserver{
voidincrement(const int&val){}
};
structMySubject:publicBasicSubject<MyObserver>{
voidtrigger(){
notifyAll(&MyObserver::increment, 10);
}
};
編譯器會抱怨不能把實參(10)類型(int)轉換到形參(val)類型(const int&)。
那能否將arg1聲明為const引用類型呢,即:
template<typenameMemFunT,typenameArg1T>
void BasicSubject::notifyAll(constMemFunT&pfn, const Arg1T &arg1);
這會限制觀察者更新方法對參數進行任何修改,不可接受。
按著你的思路,我可以給你一種解決方案,不過要將notifyAll方法聲明為:
template<typenameMemFunT,typenameArg1T>
inlinevoid notifyAll(constMemFunT&pfn,Arg1Targ1) ;
是的,arg1前少個引用(&)符號。當觀察者更新方法的形參類型為非引用類型時沒任何問題,僅僅是多了一次拷貝而使效率稍微低下而已:
structMyObserver{
voidincrement(int val){}
};
structMySubject:publicBasicSubject<MyObserver>{
voidtrigger(){
notifyAll(&MyObserver::increment, 10); // OK
}
};
但是當形參類型為引用類型時直接使用的結果與預期行為不符:
structMyObserver{
voidincrement(int&val){++val;}
};
structMySubject:publicBasicSubject<MyObserver>{
voidtrigger(){
int i = 10;
notifyAll(&MyObserver::increment, i);
cout << i << endl; // 輸出10,但我們期望是11
}
};
我們可以通過一個額外的輔助類將其解決:
template<typenameT>
classref_holder
{
T&ref_;
public:
inlineref_holder(T&ref):ref_(ref){}
inlineoperatorT&()const{returnref_;}
};
template<typenameT>
inlineref_holder<T>ByRef(T&t){
returnref_holder<T>(t);
}
函數ByRef的存在僅僅是為了方便生成ref_holder對象(類似STL中的make_pair)。當需要引用傳遞時以ByRef函數作用到實參上:
structMyObserver{
voidincrement(int&val){++val;}
};
structMySubject:publicBasicSubject<MyObserver>{
voidtrigger(){
int i = 10;
notifyAll(&MyObserver::increment, ByRef(i));
cout << i << endl; // 輸出11,OK
}
};
現在沒問題了,前提是能正確使用。但是,我敢打賭,你的客戶會經常忘記ByRef函數的存在,以致最終放棄你所提供的解決方案。
我會給出另外一種更完美的方案。
實際上,此處的notfiyAll方法是個轉發函數,對其的調用會轉發給已向BasicSubject注冊了的所有觀察者對象的相應更新方法(我稱之為目的函數)。為了具有正確的轉發行為以及較高的效率,轉發函數的形參類型聲明與目的函數的形參類型聲明必須遵循一定的對應規則。篇幅所限,這裡直接給出結論(以下將“轉發函數形參”簡稱為“轉發形參 ”,將“目的調用函數形參”簡稱為“目的形參”。):
目的形參類型為const引用類型時,轉發形參類型也是const引用類型;
目的形參類型為non-const引用類型時,轉發形參類型也是non-const引用類型;
目的形參類型為其它類型時,轉發形參類型是const引用類型。
我們通過模板traits技術可實現上面所提的轉發——目的函數形參類型對應規則:
template<typenameT>
structarg_type_traits {
typedefconstT&result;
};
template<typenameT>
structarg_type_traits<T&> {
typedefT&result;
};
template<typenameT>
structarg_type_traits<constT&> {
typedefconstT&result;
};
最後一個traits的存在是必須的,因為引用引用類型(如int&&)在C++中是不合法的。現在我們可以定義我們的notifyAll方法了:
template<typenameReturnT,typenameArg1T>
inlinevoid BasicSubject::notifyAll(ReturnT(ObserverT::*pfn)(Arg1T),
typenamearg_type_traits<Arg1T>::resultarg1) {
for(ContainerT::iteratorit=observers_.begin(),itEnd=observers_.end();it!=itEnd;++it)
((*it)->*pfn)(arg1);
}
聰明的你可能會問,萬一觀察者的更新方法參數不是一個呢?說真的,我也很想確定到底具有幾個參數,令我悲傷的是我的客戶經常這樣回答:“我也不知道有幾個。”
我使用了一種比較簡單、笨拙卻行之有效的手段解決了這一問題。我通過重載notifyAll方法,使其分別對應更新方法是0、1、2、3……個參數的情況。
template<typenameReturnT>
inlinevoid BasicSubject::notifyAll(ReturnT(ObserverT::*pfn)()) {
for(ContainerT::iteratorit=observers_.begin(),itEnd=observers_.end();it!=itEnd;++it)
((*it)->*pfn)();
}
template<typenameReturnT,typenameArg1T>
inlinevoid BasicSubject::notifyAll(ReturnT(ObserverT::*pfn)(Arg1T),
typenamearg_type_traits<Arg1T>::resultarg1) {
for(ContainerT::iteratorit=observers_.begin(),itEnd=observers_.end();it!=itEnd;++it)
((*it)->*pfn)(arg1);
}
template<typenameReturnT,typenameArg1T,typenameArg2T>
inlinevoid BasicSubject::notifyAll(ReturnT(ObserverT::*pfn)(Arg1T,Arg2T),
typenamearg_type_traits<Arg1T>::resultarg1,
typenamearg_type_traits<Arg2T>::resultarg2) {
for(ContainerT::iteratorit=observers_.begin(),itEnd=observers_.end();it!=itEnd;++it)
((*it)->*pfn)(arg1,arg2);
}
...
template<typenameReturnT,typenameArg1T,typenameArg2T,typenameArg3T,typenameArg4T,typenameArg5T>
inlinevoid BasicSubject::notifyAll(ReturnT(ObserverT::*pfn)(Arg1T,Arg2T,Arg3T,Arg4T,Arg5T),
typenamearg_type_traits<Arg1T>::resultarg1,
typenamearg_type_traits<Arg2T>::resultarg2,
typenamearg_type_traits<Arg3T>::resultarg3,
typenamearg_type_traits<Arg4T>::resultarg4,
typenamearg_type_traits<Arg5T>::resultarg5){
for(ContainerT::iteratorit=observers_.begin(),itEnd=observers_.end();it!=itEnd;++it)
((*it)->*pfn)(arg1,arg2,arg3,arg4,arg5);
}
按我的經驗,超過5個參數的類方法不常見,要是你真的有幸遇到了,你大可讓實現作者與你共進晚餐,當然,賬單由他付。你也大可再為notifyAll增加幾個重載方法。
代碼看起來有點復雜,但你的客戶卻很方便:
structMyObserver{
voidcopy(intsrc,int&dest){dest=src;}
};
structMySubject:publicBasicSubject<MyObserver>{
voidtrigger(){ // demonstrate how to use notifyAll method.
int i= 0;
notifyAll(&MyObserver::copy, 100, i);
assert(i == 100);
}
};
intmain(){
MyObserverobs;
MySubjectsub;
sub.addObserver(obs);
sub.trigger();
}
以上便是我所實現的通用觀察者模式庫的骨架。之所以稱為骨架,是因為還有許多諸如多線程等現實問題沒有考慮,我將在下篇中與讀者一起探討現實世界中可能遇到的問題。
<未完,待續>