Game Programming Patterns -- Observer
原文地址:http://gameprogrammingpatterns.com/observer.html
原作者:Robert Nystrom
原創(chuàng)翻譯,轉(zhuǎn)載請(qǐng)注明出處
如果你朝一臺(tái)電腦丟一塊石頭的話,你肯定總是能砸中一個(gè)使用Model-View-Controller架構(gòu)搭建的應(yīng)用,而MVC的底層使用的就是觀察者模式。觀察者模式是非常普及的,Java把它放在了核心庫(java.util.Observer)中,而C#把它嵌入到了語言中(event關(guān)鍵字) 。
和軟件開發(fā)中很多東西一樣,MVC在70年代的時(shí)候被使用Smalltalk的程序員們發(fā)明出來。而使用Lisp語言的開發(fā)者可能會(huì)聲稱MVC是它們?cè)?0年代發(fā)明的。
觀察者模式是GOF提出的模式中最為廣泛使用和廣為人知的模式之一,但是我們的游戲開發(fā)世界有的是顯得有點(diǎn)與世隔絕,所以這對(duì)你來說可能是一個(gè)全新的知識(shí)。那么接下來讓我來帶你看一個(gè)激發(fā)你靈感的例子吧。
解鎖成就
比方說我們要為我們的游戲添加一個(gè)成就系統(tǒng)。它包括了一系列不同的獎(jiǎng)?wù)?,玩家可以通過在游戲中完成特定的里程碑來獲取,比如“殺死100個(gè)猴子惡魔”、“從橋上掉落”或者“只裝備一只亡靈黃鼠狼完成關(guān)卡”等。

我發(fā)誓我在畫這張圖的時(shí)候腦子里完全沒有什么別的想法。
想要干凈利落地實(shí)現(xiàn)這個(gè)功能其實(shí)是有一點(diǎn)棘手的,因?yàn)槲覀兊挠螒蚶镉蟹浅6嗟耐ㄟ^各種不同類型行為來解鎖的成就。如果我們不夠小心的話,成就系統(tǒng)的藤蔓將蔓延到我們代碼庫的每一個(gè)黑暗的角落。當(dāng)然,“從橋上掉落”是通過某種方式和物理引擎相關(guān)聯(lián)的,但是我們真的想要看到在我們的碰撞檢測(cè)算法中去調(diào)用一個(gè)tounlockFallOffBridge()方法?
這是一個(gè)反問。沒有任何一個(gè)有點(diǎn)自尊心的物理引擎程序員會(huì)讓我們用諸如游戲玩法之類的代碼去玷污他們完美的算法。
我們總是想要把游戲中關(guān)于一塊功能的代碼放到一個(gè)地方。這里有點(diǎn)挑戰(zhàn)性的是,成就事通過游戲過程中很多不同的方面來觸發(fā)的。我們要如何來實(shí)現(xiàn)成就,才能不把它的代碼耦合到所有觸發(fā)成就的功能代碼中呢?
這就是觀察者模式被設(shè)計(jì)出來的目的。它讓一段代碼去通知有些有趣的事情發(fā)生了,而并不管有哪些代碼收到了這個(gè)通知。
舉個(gè)例子,我們寫了一段物理代碼來控制對(duì)象在一個(gè)平面上或者墜落中的重力作用和運(yùn)動(dòng)軌跡。為了實(shí)現(xiàn)“從橋上掉落”這個(gè)成就,我們可以直接把成就代碼塞到物理代碼里,但是那樣的話就太糟糕了。取而代之的是,我們可以這樣做:
void Physics::updateEntity(Entity& entity)
{
bool wasOnSurface = entity.isOnSurface();
entity.accelerate(GRAVITY);
entity.update();
if (wasOnSurface && !entity.isOnSurface())
{
notify(entity, EVENT_START_FALL);
}
}
這段代碼的意思是,“恩,我不管有沒有人關(guān)心,不過這里有個(gè)東西剛剛掉下去了。想要做什么就做什么吧。”
物理引擎需要決定發(fā)送哪一個(gè)消息,所以這里不是完全解耦的。不過在軟件架構(gòu)中,我們通常只是嘗試去讓系統(tǒng)變得更好,而不是達(dá)到完美無缺。
在成就系統(tǒng)中注冊(cè)了這條消息,所以不管什么時(shí)候物理系統(tǒng)發(fā)送了這條消息,成就系統(tǒng)都可以收到它。收到消息后,成就系統(tǒng)會(huì)檢查這個(gè)墜落的物體是不是我們可憐的英雄,它的之前的落腳點(diǎn)是不是很不幸地在一座橋上。如果是的話,成就系統(tǒng)就會(huì)解鎖這個(gè)成就,伴隨著一些煙花和歡呼聲,而成就系統(tǒng)完成這些工作時(shí)是沒有與物理代碼部分有任何關(guān)聯(lián)的。
實(shí)際上,我們可以修改整個(gè)成就系統(tǒng)或者把成就系統(tǒng)從我們的游戲中移除,而不用修改物理引擎的任何一行代碼。物理引擎仍然會(huì)發(fā)送之前的那條消息,不過很明顯,現(xiàn)在沒有人去接收它了。
當(dāng)然,如果我們永遠(yuǎn)地移除了成就系統(tǒng),就不會(huì)再有人去監(jiān)聽物理引擎發(fā)出的通知了,我們同樣也可以把發(fā)送通知的代碼移除。但是從游戲的進(jìn)化史來看,我們還是在這方面保持一定的靈活性會(huì)比較好。
它是如何工作的
如果你還是不知道該如何去實(shí)現(xiàn)這個(gè)模式,你也許可以接著從我們之前的描述中推敲出來,不過為了讓你們更容易理解一些,接下來我會(huì)簡(jiǎn)單地介紹一下。
觀察者
那么就讓我們從那些想要知道別的對(duì)象發(fā)生了什么有趣的事情的類開始。這些好奇寶寶類是用下面這個(gè)接口來定義的:
class Observer
{
public:
virtual ~Observer() {}
virtual void onNotify(const Entity& entity, Event event) = 0;
};
onNotify()方法的參數(shù)你可以自己決定。因?yàn)檫@是觀察者模式,而不是可以直接粘貼到你游戲里的觀察者代碼。典型的參數(shù)是發(fā)送通知的對(duì)象和一個(gè)用來保存一些數(shù)值的data參數(shù)。
如果你使用的是支持泛型或者模板的編程語言,你在這里可以使用使用它們來表示發(fā)送通知的對(duì)象,不過把它們整理成適合你自己的用例也是一個(gè)不錯(cuò)的選擇。這里為了方便,我只硬編碼了一個(gè)entity和一個(gè)enum來描述發(fā)生的事情。
任何一個(gè)具體的類都可以通過實(shí)現(xiàn)這個(gè)接口成為一個(gè)觀察者。在我們的例子里,就是成就系統(tǒng),所以我們接下來這么做:
class Achievements : public Observer
{
public:
virtual void onNotify(const Entity& entity, Event event)
{
switch (event)
{
case EVENT_ENTITY_FELL:
if (entity.isHero() && heroIsOnBridge_)
{
unlock(ACHIEVEMENT_FELL_OFF_BRIDGE);
}
break;
// Handle other events, and update heroIsOnBridge_...
}
}
private:
void unlock(Achievement achievement)
{
// Unlock if not already unlocked...
}
bool heroIsOnBridge_;
};
被觀察對(duì)象
通知方法是在那些被監(jiān)聽的對(duì)象中調(diào)用的。在GOF的描述中,這些對(duì)象被稱為“被觀察對(duì)象(subject)”。被觀察對(duì)象有兩個(gè)功能要實(shí)現(xiàn)。首先,它保存了一個(gè)等待接收它發(fā)出通知的所有觀察者的列表:
class Subject
{
private:
Observer* observers_[MAX_OBSERVERS];
int numObservers_;
};
在實(shí)際的代碼中,你可以使用動(dòng)態(tài)的集合來代替固定長(zhǎng)度的數(shù)組。在這里我使用了比較基礎(chǔ)的數(shù)組,以方便使用其他編程語言、不知道C++標(biāo)準(zhǔn)庫的讀者。
很重要的一點(diǎn)是,被觀察者暴露了一個(gè)public接口用于修改這個(gè)列表:
class Subject
{
public:
void addObserver(Observer* observer)
{
// Add to array...
}
void removeObserver(Observer* observer)
{
// Remove from array...
}
// Other stuff...
};
這可以讓外部代碼來控制哪些代碼可以收到通知。被觀察者可以和觀察者們進(jìn)行通訊,但是和它們并沒有耦合。在我們的例子中,沒有任何一行物理引擎代碼會(huì)提到成就這個(gè)東西。然而,它依舊可以和成就系統(tǒng)進(jìn)行交流。這就是觀察者模式最機(jī)智的部分。
而同樣重要的是,被觀察者保存了一個(gè)觀察者的列表,而不是僅僅一個(gè)觀察者。這就保證了觀察者之間沒有隱藏的耦合關(guān)系。舉個(gè)例子,假如聲音引擎也關(guān)注了我們的掉落事件,這樣它可以播放一個(gè)合適的音效。如果被觀察者只支持一個(gè)觀察者的話,當(dāng)聲音引擎需要注冊(cè)監(jiān)聽時(shí),它必須把成就系統(tǒng)的監(jiān)聽注銷。
這意味著這兩個(gè)系統(tǒng)會(huì)產(chǎn)生相互干擾,并且是一種非常討厭的干擾,因?yàn)楹笠粋€(gè)注冊(cè)的會(huì)禁用前一個(gè)的監(jiān)聽。支持一個(gè)觀察者列表可以保證觀察者之間是相互獨(dú)立的。觀察者所能知道的是,這個(gè)世界上只有它自己是在關(guān)注被觀察者的。
被觀察者的另外一個(gè)工作就是發(fā)送通知:
class Subject
{
protected:
void notify(const Entity& entity, Event event)
{
for (int i = 0; i < numObservers_; i++)
{
observers_[i]->onNotify(entity, event);
}
}
// Other stuff...
};
注意這里的代碼假設(shè)了觀察者們沒有修改它們onNotify()方法的參數(shù)列表。一個(gè)更健壯的實(shí)現(xiàn)需要可以避免或者控制這種并發(fā)修改。
可以觀察的物理引擎
現(xiàn)在,我們只需要把這些和物理引擎連接起來讓它可以發(fā)送通知,而成就系統(tǒng)中可以寫一段代碼來接收這個(gè)通知。我們遵循最基本的設(shè)計(jì)模式方法,繼承Subject類:
class Physics : public Subject
{
public:
void updateEntity(Entity& entity);
}
在實(shí)際開發(fā)中,在這里我會(huì)避免使用繼承,而是在Physics類中聲明一個(gè)Subject類的實(shí)例。我們不再觀察整個(gè)物理引擎,而是去觀察“掉落事件”(falling event)這個(gè)對(duì)象。觀察者們可以像下面這樣來注冊(cè)監(jiān)聽:
對(duì)于我來說,這兩個(gè)的區(qū)別就是“觀察者”系統(tǒng)和“事件”系統(tǒng)。前者是在觀察會(huì)做一些有趣的事情的對(duì)象,而后者則是在觀察這件有趣的事情本身。
這讓我們可以把notify()寫成Subject的protected方法。如此Physics類通過繼承來調(diào)用notify()去發(fā)送通知,但是外部的代碼則沒有這個(gè)權(quán)限。同時(shí),addObserver()和removeObserver()都是public的,所以任何和物理引擎相關(guān)的代碼都可以對(duì)它進(jìn)行觀察。
現(xiàn)在,當(dāng)物理引擎做了一些值得外界關(guān)注的事情之后,它會(huì)像我們一開始的那個(gè)例子中一樣調(diào)用notify()方法。這個(gè)方法將遍歷整個(gè)觀察者列表,通知它們注意有事情發(fā)生了。

很簡(jiǎn)單,不是么?只需要一個(gè)類去維護(hù)某個(gè)接口的實(shí)例列表。讓人不敢相信的是,如此簡(jiǎn)單的方法正是無數(shù)的程序和app框架通訊系統(tǒng)的支柱。
不過觀察者模式也并不是沒有反對(duì)者的。當(dāng)我跟其他游戲開發(fā)者問及他們是如何看待觀察者模式的時(shí)候,他們提出了一些控訴。下面來看看我們是如何來解決這些問題的。
“它太慢了”
這一點(diǎn)是我聽到的被抱怨最多的,通??梢詮哪切┎⒉徽嬲私庥^察者模式的程序員口中聽到。他們先入為主地認(rèn)為任何和“設(shè)計(jì)模式”有關(guān)的東西肯定包含了一堆的類、間接性以及一些想著法子浪費(fèi)CPU資源的點(diǎn)子。
觀察者模式得到了格外糟糕的批評(píng),因?yàn)樗ǔ:汀癳vent”、“messages”以及“data binding”這些關(guān)鍵字關(guān)聯(lián)在一起。這些系統(tǒng)中的一些可能會(huì)很慢(設(shè)計(jì)如此)。它們包含了隊(duì)列或者給每條通知?jiǎng)討B(tài)分配內(nèi)存這些東西。
這就是為什么我認(rèn)為把設(shè)計(jì)模式文檔化是一件很重要的事情。當(dāng)我們隊(duì)專業(yè)術(shù)語理解不夠清楚的時(shí)候,我們就失去了清晰和簡(jiǎn)潔地進(jìn)行交流的能力。當(dāng)你說“Observer”的時(shí)候,其他人可能會(huì)認(rèn)為你在說“Events”或者“Messaging”,因?yàn)閺膩頉]有人愿意費(fèi)心去寫清楚這些之間的區(qū)別,而其他人自然也就沒有機(jī)會(huì)去了解。
這正是我嘗試通過這本書去做的事情。在接下來的章節(jié)里,會(huì)有一章關(guān)于events和messages的:事件隊(duì)列(Event Queue)
但是,既然你在之前的例子中了解到了這個(gè)模式是如何實(shí)現(xiàn)的,你就會(huì)知道速度并沒有收到太大的影響。發(fā)送通知時(shí)只是簡(jiǎn)單地遍歷了一個(gè)列表然后調(diào)用了一些虛方法。不過,它和靜態(tài)分配調(diào)用(statically dispatched call)比起來確實(shí)有一點(diǎn)慢,不過這一點(diǎn)除了在一些對(duì)性能要求極為苛刻的代碼中,幾乎是可以忽略不計(jì)的。
我認(rèn)為觀察者模式在性能消耗嚴(yán)重的代碼路徑之外使用是最為合適的,這樣你就可以承擔(dān)動(dòng)態(tài)分配的消耗。撇開這個(gè)不談的話,也找不到其他什么額外的消耗了。觀察者模式?jīng)]有給message分配新的對(duì)象,也沒有隊(duì)列。它只是在同步方法調(diào)用(synchronous method call)上存在了一點(diǎn)間接性。
“它太快了?”
實(shí)際上,你需要小心使用觀察者模式,因?yàn)樗峭綑C(jī)制(synchronous)的。被觀察者直接調(diào)用它的觀察者們,這意味著在所有的觀察者從它們的通知方法中返回之前,被觀察者是不能繼續(xù)它的工作的。一個(gè)速度慢的觀察者會(huì)導(dǎo)致被觀察者阻塞。
這聽起來很可怕,但是在實(shí)踐中,它也并不是世界末日。它只是一件你需要去注意的事情。UI程序員們--長(zhǎng)久以來一直從事著這類基于事件(event-based)的編程工作--對(duì)這個(gè)問題有一句經(jīng)過時(shí)間考驗(yàn)的座右銘:“遠(yuǎn)離UI線程”。
如果你在同步響應(yīng)一個(gè)事件的話,你需要盡可能快地完成工作并回到操作線程,這樣UI才不會(huì)鎖住。如果你有一件比較慢的工作需要處理的話,把它丟到另一個(gè)線程或者工作隊(duì)列中去處理吧。
然而,你必須小心地處理觀察者們跟線程和顯式鎖(explicit locks)的關(guān)系。如果一個(gè)觀察者嘗試去抓取被觀察者的線程鎖的話,你的游戲就死鎖了。在一個(gè)高度線程化的引擎中,使用事件隊(duì)列(Event Queue)來進(jìn)行異步通訊可能是一個(gè)更好的選擇。
“它做了太多的動(dòng)態(tài)分配”
整個(gè)程序員族群的部落--包括許多游戲開發(fā)者--已經(jīng)遷移到了垃圾回收語言上,而動(dòng)態(tài)分配也不再是曾經(jīng)那個(gè)不靠譜的角色了。但是對(duì)于像游戲這樣對(duì)性能要求很高的軟件來說,內(nèi)存分配始終存在問題,即使在托管語言(managed languages)中也是這樣。動(dòng)態(tài)分配在回收內(nèi)存時(shí)是需要消耗時(shí)間的,盡管這是一個(gè)自動(dòng)的過程。
許多游戲開發(fā)者對(duì)內(nèi)存分配的擔(dān)心要少于對(duì)碎片化的擔(dān)心。當(dāng)你的游戲需要進(jìn)行連續(xù)幾天的無崩潰運(yùn)行去通過驗(yàn)收時(shí),一個(gè)不斷增加的碎片堆會(huì)阻止你的游戲的發(fā)售。
對(duì)象池這一章里會(huì)討論到更多的細(xì)節(jié), 并介紹一個(gè)避免碎片化的通用技術(shù)。
在之前的例子代碼中,我使用了固定長(zhǎng)度的數(shù)組,因?yàn)槲蚁胱屖虑榭雌饋肀M可能簡(jiǎn)單。在實(shí)際的實(shí)現(xiàn)中,觀察者列表通常是一個(gè)動(dòng)態(tài)分配的集合,大小會(huì)隨著觀察者的增減而變化。這里內(nèi)存的變換著實(shí)困擾了一群人。
當(dāng)然,首先需要注意的是,只有在連接新的觀察者的時(shí)候內(nèi)存才會(huì)被分配。發(fā)送通知是不需要進(jìn)行內(nèi)存分配的--它只是一個(gè)方法調(diào)用。如果你在游戲開始的時(shí)候就設(shè)置好所有的觀察者,并且不會(huì)經(jīng)常變動(dòng)它們,內(nèi)存的分配就會(huì)最小化。
如果仍然覺得有問題的話,下面我會(huì)介紹一個(gè)實(shí)現(xiàn)方法,這個(gè)方法里添加和刪除觀察者是沒有任何動(dòng)態(tài)內(nèi)存分配的。
鏈接起來的觀察者們
在之前的代碼中,Subject被觀察者擁有一個(gè)指向每一個(gè)監(jiān)聽它的觀察者的指針列表。而Observer觀察者類本身對(duì)這個(gè)列表沒有做引用。它只是一個(gè)純虛接口。接口要比具體的,狀態(tài)類要更適合一些,所以這是一個(gè)好事情。
但是如果我們想給Observer類添加一些狀態(tài)的話,可以通過觀察者們自己組成被觀察者的列表來解決分配問題?,F(xiàn)在我們不再在Subject類中保存一個(gè)單獨(dú)的指針集合,而是用一個(gè)Observer組成的鏈表來代替:

為了實(shí)現(xiàn)這個(gè)結(jié)果,首先我們需要用一個(gè)指向觀察者鏈表頭的指針來取代Subject中的數(shù)組:
class Subject
{
Subject()
: head_(NULL)
{}
// Methods...
private:
Observer* head_;
};
接下來,我們?cè)贠bserver類中添加一個(gè)指向觀察者列表中下一個(gè)觀察者的指針:
class Observer
{
friend class Subject;
public:
Observer()
: next_(NULL)
{}
// Other stuff...
private:
Observer* next_;
}
在這里,我們還把Subject類聲明成了友元類(friend class)。Subject類擁有添加和刪除觀察者的API,但是它所管理的列表現(xiàn)在嵌入了Observer類中。讓被觀察者可以控制這個(gè)列表最簡(jiǎn)單的方法就是把它變成觀察者的友元類。
當(dāng)需要注冊(cè)一個(gè)新的觀察者時(shí),只要把它加入到列表中即可。我們選擇最簡(jiǎn)單的方式,把新的觀察者加入列表的頭部:
void Subject::addObserver(Observer* observer)
{
observer->next_ = head_;
head_ = observer;
}
另外一個(gè)選擇是插入鏈表的尾部。但是那么做的話會(huì)增加一定的復(fù)雜度。Subject需要去遍歷整個(gè)列表去找到尾部或者保存一個(gè)單獨(dú)的tail_指針去指向列表的最后一個(gè)節(jié)點(diǎn)。
添加到鏈表頭部要更簡(jiǎn)單一些,不過也有一點(diǎn)副作用。當(dāng)我們遍歷列表去給每一個(gè)觀察者發(fā)送通知時(shí),最后注冊(cè)的觀察者會(huì)第一個(gè)收到消息。所以如果你以A、B、C的順序注冊(cè)觀察者的話,它們收到通知的順序是C、B、A。
理論上,這并沒有什么影響。一個(gè)好的觀察者設(shè)計(jì)需要遵循的一個(gè)原則是,兩個(gè)監(jiān)聽同一個(gè)Subject的觀察者,應(yīng)該彼此之間沒有順序關(guān)系上的依賴。因?yàn)槿绻许樞蜿P(guān)系的話,就意味著這兩個(gè)觀察者會(huì)有一些微妙的耦合關(guān)系,而這樣的耦合最終可能會(huì)對(duì)你的代碼造成影響。
接下來讓我們看看移除的功能:
void Subject::removeObserver(Observer* observer)
{
if (head_ == observer)
{
head_ = observer->next_;
observer->next_ = NULL;
return;
}
Observer* current = head_;
while (current != NULL)
{
if (current->next_ == observer)
{
current->next_ = observer->next_;
observer->next_ = NULL;
return;
}
current = current->next_;
}
}
在從列表中移除節(jié)點(diǎn)的代碼里,通常需要一些特殊的代碼去控制隊(duì)首節(jié)點(diǎn)的刪除,就像你在上面看到的這樣。其實(shí)有一個(gè)更好的解決方案,就是用指針去指向指針。
我在這里沒有那么寫,因?yàn)槲夜烙?jì)看到這種方法的人里面可能有一半都會(huì)被弄得頭大。我覺得這是一個(gè)值得你自己去嘗試的練習(xí),它會(huì)對(duì)你理解指針有非常大的幫助。
因?yàn)槭褂玫氖菃捂湵?,所以我們需要遍歷它去找到需要?jiǎng)h除的觀察者。如果我們使用的是常規(guī)數(shù)組的話,也需要進(jìn)行同樣的遍歷。而在每一個(gè)觀察者都擁有指向前一個(gè)和后一個(gè)觀察者的雙鏈表中,我們可以在一場(chǎng)常數(shù)時(shí)間復(fù)雜度上把觀察者移除。所以在實(shí)際的代碼中,我會(huì)選擇雙鏈表。
最后一件工作就是發(fā)送通知了。這和遍歷列表一樣簡(jiǎn)單:
void Subject::notify(const Entity& entity, Event event)
{
Observer* observer = head_;
while (observer != NULL)
{
observer->onNotify(entity, event);
observer = observer->next_;
}
}
這里我們遍歷了整個(gè)列表,對(duì)列表中每一個(gè)觀察者都發(fā)送了通知。這保證了所有的觀察者都有相同的優(yōu)先級(jí),并且相互之間的獨(dú)立的。
我們也可以這樣修改:當(dāng)一個(gè)觀察者接收到通知后,會(huì)返回一個(gè)標(biāo)識(shí),告訴被觀察者要不要繼續(xù)遍歷列表。如果你這么做的話,那你就很接近責(zé)任鏈(Chain of Responsibility)模式了
還不錯(cuò),不是么?一個(gè)被觀察者可以擁有無數(shù)個(gè)觀察者,而不需要進(jìn)行任何動(dòng)態(tài)分配。注冊(cè)和注銷監(jiān)聽都和使用數(shù)組的時(shí)候是一樣快的。不過我們犧牲了一個(gè)小特性。
因?yàn)槲覀兪褂玫氖怯^察者對(duì)象本身作為列表節(jié)點(diǎn),這意味著它只能屬于一個(gè)被觀察者的觀察者列表。也就是說,一個(gè)觀察者在同一時(shí)間內(nèi)只能監(jiān)聽一個(gè)被觀察者。而在早前我們的實(shí)現(xiàn)中,每一個(gè)被觀察者擁有自己的觀察者列表時(shí),每一個(gè)觀察者是可以同時(shí)監(jiān)聽多個(gè)被觀察者的。
你也許可以接受這個(gè)限制。而且我發(fā)現(xiàn)一個(gè)被觀察者擁有多個(gè)觀察者要比一個(gè)觀察者同時(shí)監(jiān)聽多個(gè)被觀察者要常見的多。如果這對(duì)你來說是個(gè)問題的話,還有另外一個(gè)更復(fù)雜的解決方案可以使用,這同樣是沒有動(dòng)態(tài)分配的。這個(gè)方法在這一章里展開說的話就太長(zhǎng)了,不過我可以把框架在這里介紹一下,剩下來的留給你們自己去補(bǔ)充...
列表節(jié)點(diǎn)池
讓我們回到之前那個(gè)例子中那樣,每一個(gè)Subject都保存了一個(gè)觀察者的鏈表。不過,這個(gè)列表中的節(jié)點(diǎn)不再是觀察者對(duì)象本身。取而代之的是,它們是一個(gè)獨(dú)立的“列表節(jié)點(diǎn)”,包含了一個(gè)指向觀察者的指針和列表中下一個(gè)節(jié)點(diǎn)的指針。
鏈表通常有兩種實(shí)現(xiàn)形式。一種是你在學(xué)校里學(xué)到的那樣,在列表節(jié)點(diǎn)對(duì)象中包含著數(shù)據(jù)。而在我們之前的例子中,這個(gè)情況倒過來了:數(shù)據(jù)(例子中的觀察者Observer)包含了列表節(jié)點(diǎn)(next_ 指針)
后者被稱為“介入式(intrusive)”鏈表,因?yàn)樗蚜斜砉?jié)點(diǎn)介入到了列表中實(shí)際需要包含的對(duì)象本身的定義中。這使得介入式鏈表缺少了一定的靈活性,但是,就像我們之前看到的那樣,會(huì)有更高的效率。這種鏈表在像Linux系統(tǒng)內(nèi)核這樣的,值得用一定的靈活性去換取性能的地方,是非常流行的。

因?yàn)槎鄠€(gè)列表節(jié)點(diǎn)可以指向同一個(gè)觀察者,這也就意味著一個(gè)觀察者可以同時(shí)存在于多個(gè)被觀察者的列表中。我們又回到了一開始可以同時(shí)監(jiān)聽多個(gè)被觀察者的時(shí)候。
這里避免動(dòng)態(tài)分配的方法也很簡(jiǎn)單:因?yàn)樗械牧斜砉?jié)點(diǎn)都是相同的大小和類型,你可以預(yù)分配一個(gè)節(jié)點(diǎn)的對(duì)象池(object pool)。這樣你就擁有了一定數(shù)量的節(jié)點(diǎn)去進(jìn)行操作,你可以對(duì)它們進(jìn)行使用和回收而不用任何內(nèi)存重分配。
剩下來的問題
我想我們已經(jīng)解決了這個(gè)模式可能會(huì)把人們嚇跑的三個(gè)問題。就像我們看到的那樣,現(xiàn)在它簡(jiǎn)單、快速并且在內(nèi)存分配方面優(yōu)化良好。但是這是不是意味著不管什么時(shí)候你都應(yīng)該使用觀察者模式呢?
這就是另外一個(gè)問題了。就像所有的設(shè)計(jì)模式那樣,觀察者模式也不是萬靈藥。即使正確并高效地實(shí)現(xiàn),它也不一定是正確的解決方案。設(shè)計(jì)模式為人們所詬病的原因正是使用者把一個(gè)優(yōu)秀的設(shè)計(jì)模式用在一個(gè)錯(cuò)誤的地方,最終把事情變得更糟。
現(xiàn)在還剩下兩個(gè)挑戰(zhàn)需要解決,一個(gè)技術(shù)性的問題,另一個(gè)更偏向于可維護(hù)性的問題。我們先來解決技術(shù)性的問題,因?yàn)檫@總是最容易解決的。
銷毀被觀察者和觀察者
之前的例子代碼是可靠的,但是它回避了一個(gè)重要的情況:在你刪除一個(gè)被觀察者或者觀察者時(shí)會(huì)發(fā)生什么?如果你草率地在觀察者中調(diào)用delete方法,那么在被觀察者中可能還留有了一個(gè)指向它的指針。這個(gè)指針現(xiàn)在是一個(gè)指向未分配內(nèi)存的野指針。接下來當(dāng)Subject嘗試去發(fā)送通知的時(shí)候,好吧...我只能說你可能會(huì)變得不太開心。
并不是推卸責(zé)任,不過我不得不說設(shè)計(jì)模式確實(shí)沒有提到這個(gè)情況。
刪除Subject的時(shí)候要簡(jiǎn)單一些,因?yàn)樵诖蠖鄶?shù)的實(shí)現(xiàn)中,Observer是不會(huì)對(duì)它有任何引用的。但是即使這樣,對(duì)Subject進(jìn)行內(nèi)存回收之后仍然可能引發(fā)一些問題。觀察者們可能仍在期待能夠接收到通知,但是它們不知道這永遠(yuǎn)不可能發(fā)生了。它們不再是觀察者了,但它們認(rèn)為自己還是。
你可以用很多方法來解決這個(gè)問題。最簡(jiǎn)單的就是像我接下來這樣做。當(dāng)觀察者被刪除時(shí),從被觀察者注銷監(jiān)聽?wèi)?yīng)該是觀察者自己的工作。而通常,觀察者是知道自己在監(jiān)聽哪個(gè)被觀察者的,所以我們只需要在Observer的析構(gòu)函數(shù)中添加一個(gè)removeObserver()調(diào)用就可以了。
通常的情況是,最難的部分不是如何去做,而是記得去做。
如果你不想在Subject銷毀之后觀察者們?nèi)匀辉诒O(jiān)聽,也是很好解決的。只要讓Subject在銷毀時(shí)發(fā)送一個(gè)“瀕死”通知。這樣,每一個(gè)觀察者都會(huì)收到這個(gè)消息,然后做它們認(rèn)為合適的處理。
哀悼,送鮮花,組成挽歌,等等。
大多數(shù)人--即使是像我們這樣已經(jīng)工作了很多年的人--有的時(shí)候并不是那么可靠。這就是為什么我們發(fā)明了計(jì)算機(jī):它們不會(huì)犯那些我們經(jīng)常犯的錯(cuò)誤。
一個(gè)更安全的解決方案是讓觀察者們?cè)阡N毀時(shí)自動(dòng)注銷它們?cè)诒O(jiān)聽的被觀察者。如果你在代碼庫中實(shí)現(xiàn)了這個(gè)功能的話,后面使用這套代碼的人就不需要再記得進(jìn)行相關(guān)處理了。這可能會(huì)增加一定的復(fù)雜度,因?yàn)槊總€(gè)觀察者都需要一個(gè)自己監(jiān)聽的被觀察者列表。最終觀察者和被觀察者中都有了指向?qū)Ψ降闹羔槨?/p>
不用擔(dān)心,我有GC
你們這些使用帶有垃圾回收機(jī)制的高級(jí)語言的家伙們現(xiàn)在一定覺得自鳴得意。你們覺得不需要去擔(dān)心這個(gè)事情,因?yàn)槟銈儚牟伙@式地去刪除任何東西?再好好想想!
想象這樣一個(gè)場(chǎng)景:你有一個(gè)UI界面,用來顯示玩家角色的血量、裝備等信息。當(dāng)玩家打開這個(gè)界面時(shí),你創(chuàng)建了一個(gè)這個(gè)界面的對(duì)象。當(dāng)玩家關(guān)閉界面時(shí),你忘記去刪除這個(gè)對(duì)象,讓GC去進(jìn)行回收處理。
每一次玩家被攻擊到臉部(或者其他地方,我猜)的時(shí)候,角色會(huì)發(fā)出被攻擊的通知。UI界面監(jiān)聽這個(gè)消息,然后更新HP血量條。好的,那么當(dāng)玩家關(guān)閉這個(gè)界面,而你并沒有注銷這個(gè)UI界面的監(jiān)聽時(shí)會(huì)發(fā)生什么呢?
這個(gè)UI界面不再可見了,但是它也不會(huì)被垃圾回收機(jī)制處理,因?yàn)榻巧挠^察者列表中仍然對(duì)它有引用。而每一次這個(gè)界面被加載時(shí),我們都會(huì)去創(chuàng)建一個(gè)新的實(shí)例,這導(dǎo)致角色的觀察者列表越來越長(zhǎng)。
在玩家的整個(gè)游戲過程中,角色四處跑動(dòng),或者進(jìn)入戰(zhàn)斗,都會(huì)發(fā)送出通知,而這些通知會(huì)被創(chuàng)建出的所有的UI界面接收到。它們并不在屏幕上可見,但是它們會(huì)接收消息,并且浪費(fèi)CPU資源去更新一些并不可見的UI元素。如果它們做了諸如播放聲音之類的工作的話,你就會(huì)發(fā)現(xiàn)一些明顯的錯(cuò)誤了。
這在消息系統(tǒng)中是一個(gè)常見的情況,它有一個(gè)名字:失效監(jiān)聽器問題。以為Subject保留了對(duì)觀察者們的引用,所以最后在內(nèi)存中會(huì)存在一大堆的僵尸UI對(duì)象。從這里我們得到的教訓(xùn)是,需要妥善處理觀察者的注銷問題。
這個(gè)問題很重要的另一個(gè)更明顯的標(biāo)志是:它有一個(gè)Wikipedia頁面。
發(fā)生了什么事?
另一個(gè)關(guān)于觀察者模式更深層次的問題是由其設(shè)計(jì)目的引發(fā)的。我們使用觀察者模式是為了介紹兩個(gè)模塊代碼之間的耦合。它讓Subject可以間接地和觀察者通訊,而不是靜態(tài)地綁定。
當(dāng)你想要探究Subject的行為功能時(shí),觀察者模式是非常好的選擇,它保證了沒有任何額外的東西來對(duì)你進(jìn)行干擾。當(dāng)你想要研究物理引擎的時(shí)候,你一定不想你的編輯器或者你的大腦被一堆成就相關(guān)的東西所擾亂。
而另一方面,如果你的程序不能正常運(yùn)行,并且bug分布于整個(gè)觀察者鏈表中,這個(gè)時(shí)候去探究通訊流的話會(huì)變得更加困難。使用顯式耦合時(shí),想要找到被調(diào)用的方法會(huì)很容易。這對(duì)于IDE來說也是很容易完成的工作,因?yàn)轳詈鲜庆o態(tài)的。
但是問題如果發(fā)生在觀察者鏈表中的話,唯一的解決辦法只能是找出運(yùn)行中有哪些觀察者在鏈表中。這時(shí)就不再有靜態(tài)的通訊結(jié)構(gòu)來供你探究,取而代之的是動(dòng)態(tài)的通訊行為。
我對(duì)如何解決這個(gè)問題的指導(dǎo)方針是非常簡(jiǎn)單的。如果你需要經(jīng)常去考慮通訊兩側(cè)的對(duì)象,才能更好地理解你的程序, 那么就不要用觀察者模式來進(jìn)行連接了。你可以用一些顯式的關(guān)系來取代。
當(dāng)你在研究一些大型項(xiàng)目的源代碼時(shí),你會(huì)發(fā)現(xiàn)它們都是有一些代碼模塊組成的。對(duì)這個(gè)我們有很多術(shù)語來表示,比如“關(guān)注分離(separation of concerns)”、“耦合和內(nèi)聚(coherence and cohesion)”以及“模塊化(modularity)”等。歸根結(jié)底可以總結(jié)成一句話:“這些模塊相互配合而并不相互依賴?!?/p>
觀察者模式是讓這些幾乎沒有關(guān)聯(lián)的模塊相互通訊而并不需要把它們整合到一個(gè)巨大的代碼塊中的最好方法。而它的作用在一個(gè)單一功能模塊的代碼中則要小得多。
這就是為什么觀察者模式很適合于我們的例子:成就系統(tǒng)和物理引擎是兩個(gè)幾乎沒有關(guān)聯(lián)的模塊,而且可能都是由不同的人來實(shí)現(xiàn)的。我們想要兩者之間進(jìn)行通訊時(shí)需要的關(guān)聯(lián)盡可能的少,這樣我們?cè)诟髯詥为?dú)的模塊中工作時(shí),并不需要知道太多另一個(gè)模塊中的相關(guān)知識(shí)。
時(shí)至今日的觀察者模式
設(shè)計(jì)模式誕生于1994年。那是,面向?qū)ο缶幊淌亲顭衢T的編程范本。地球上的每一個(gè)程序員都想要“30天學(xué)會(huì)面向?qū)ο缶幊獭保袑庸芾碚邆儎t基于程序員們創(chuàng)建的類來給他們發(fā)工資。工程師們則根據(jù)他們繼承的層級(jí)數(shù)量來判斷他們的能力。
同一年,Ace of Base有三首單曲上了暢銷榜,這也許能讓你了解一些我們那個(gè)年代的品味和欣賞水平。
因?yàn)樗接邢?,翻譯的文字會(huì)有不妥之處,歡迎大家指正
“本譯文僅供個(gè)人研習(xí)、欣賞語言之用,謝絕任何轉(zhuǎn)載及用于任何商業(yè)用途。本譯文所涉法律后果均由本人承擔(dān)。本人同意簡(jiǎn)書平臺(tái)在接獲有關(guān)著作權(quán)人的通知后,刪除文章?!?/strong>