【轉(zhuǎn)】面試中的Singleton
Posted on 2012-07-19 13:09 小胡子 閱讀(205) 評(píng)論(0) 編輯 收藏 所屬分類: 設(shè)計(jì)模式“請(qǐng)寫一個(gè)Singleton。”面試官微笑著和我說(shuō)。
“這可真簡(jiǎn)單。”我心里想著,并在白板上寫下了下面的Singleton實(shí)現(xiàn):
public: static Singleton& Instance() {
static Singleton singleton;
return singleton;
}
private: Singleton() { };
};
“那請(qǐng)你講解一下該實(shí)現(xiàn)的各組成。”面試官的臉上仍然帶著微笑。
“首先要說(shuō)的就是Singleton的構(gòu)造函數(shù)。由于Singleton限制其類型實(shí)例有且只能有一個(gè),因此我們應(yīng)通過(guò)將構(gòu)造函數(shù)設(shè)置為非公有 來(lái)保證其不會(huì)被用戶代碼隨意創(chuàng)建。而在類型實(shí)例訪問(wèn)函數(shù)中,我們通過(guò)局部靜態(tài)變量達(dá)到實(shí)例僅有一個(gè)的要求。另外,通過(guò)該靜態(tài)變量,我們可以將該實(shí)例的創(chuàng)建 延遲到實(shí)例訪問(wèn)函數(shù)被調(diào)用時(shí)才執(zhí)行,以提高程序的啟動(dòng)速度。”
保護(hù)
“說(shuō)得不錯(cuò),而且更可貴的是你能注意到對(duì)構(gòu)造函數(shù)進(jìn)行保護(hù)。畢竟中間件代碼需要非常嚴(yán)謹(jǐn)才能防止用戶代碼的誤用。那么,除了構(gòu)造函數(shù)以外,我們還需要對(duì)哪些組成進(jìn)行保護(hù)?”
“還需要保護(hù)的有拷貝構(gòu)造函數(shù),析構(gòu)函數(shù)以及賦值運(yùn)算符。或許,我們還需要考慮取址運(yùn)算符。這是因?yàn)榫幾g器會(huì)在需要的時(shí)候?yàn)檫@些成員創(chuàng)建一個(gè)默認(rèn)的實(shí)現(xiàn)。”
“那你能詳細(xì)說(shuō)一下編譯器會(huì)在什么情況下創(chuàng)建默認(rèn)實(shí)現(xiàn),以及創(chuàng)建這些默認(rèn)實(shí)現(xiàn)的原因嗎?”面試官繼續(xù)問(wèn)道。
“在這些成員沒(méi)有被聲明的情況下,編譯器將使用一系列默認(rèn)行為:對(duì)實(shí)例的構(gòu)造就是分配一部分內(nèi)存,而不對(duì)該部分內(nèi)存做任何事情;對(duì)實(shí)例的拷貝也 僅僅是將原實(shí)例中的內(nèi)存按位拷貝到新實(shí)例中;而賦值運(yùn)算符也是對(duì)類型實(shí)例所擁有的各信息進(jìn)行拷貝。而在某些情況下,這些默認(rèn)行為不再滿足條件,那么編譯器 將嘗試根據(jù)已有信息創(chuàng)建這些成員的默認(rèn)實(shí)現(xiàn)。這些影響因素可以分為幾種:類型所提供的相應(yīng)成員,類型中的虛函數(shù)以及類型的虛基類。”
“就以構(gòu)造函數(shù)為例,如果當(dāng)前類型的成員或基類提供了由用戶定義的構(gòu)造函數(shù),那么僅進(jìn)行內(nèi)存拷貝可能已經(jīng)不是正確的行為。這是因?yàn)樵摮蓡T的構(gòu)造 函數(shù)可能包含了成員初始化,成員函數(shù)調(diào)用等眾多執(zhí)行邏輯。此時(shí)編譯器就需要為這個(gè)類型生成一個(gè)默認(rèn)構(gòu)造函數(shù),以執(zhí)行對(duì)成員或基類構(gòu)造函數(shù)的調(diào)用。另外,如 果一個(gè)類型聲明了一個(gè)虛函數(shù),那么編譯器仍需要生成一個(gè)構(gòu)造函數(shù),以初始化指向該虛函數(shù)表的指針。如果一個(gè)類型的各個(gè)派生類中擁有一個(gè)虛基類,那么編譯器 同樣需要生成構(gòu)造函數(shù),以初始化該虛基類的位置。這些情況同樣需要在拷貝構(gòu)造函數(shù)中考慮:如果一個(gè)類型的成員變量擁有一個(gè)拷貝構(gòu)造函數(shù),或者其基類擁有一 個(gè)拷貝構(gòu)造函數(shù),位拷貝就不再滿足要求了,因?yàn)榭截悩?gòu)造函數(shù)內(nèi)可能執(zhí)行了某些并不是位拷貝的邏輯。同時(shí)如果一個(gè)類型聲明了虛函數(shù),拷貝構(gòu)造函數(shù)需要根據(jù)目 標(biāo)類型初始化虛函數(shù)表指針。如基類實(shí)例經(jīng)過(guò)拷貝后,其虛函數(shù)表指針不應(yīng)指向派生類的虛函數(shù)表。同理,如果一個(gè)類型的各個(gè)派生類中擁有一個(gè)虛派生,那么編譯 器也應(yīng)為其生成拷貝構(gòu)造函數(shù),以正確設(shè)置各個(gè)虛基類的偏移。”
“當(dāng)然,析構(gòu)函數(shù)的情況則略為簡(jiǎn)單一些:只需要調(diào)用其成員的析構(gòu)函數(shù)以及基類的析構(gòu)函數(shù)即可,而不需要再考慮對(duì)虛基類偏移的設(shè)置及虛函數(shù)表指針的設(shè)置。”
“在這些默認(rèn)實(shí)現(xiàn)中,類型實(shí)例的各個(gè)原生類型成員并沒(méi)有得到初始化的機(jī)會(huì)。但是這一般被認(rèn)為是軟件開(kāi)發(fā)人員的責(zé)任,而不是編譯器的責(zé)任。”說(shuō)完這些,我長(zhǎng)出一口氣,心里也暗自慶幸曾經(jīng)研究過(guò)該部分內(nèi)容。
“你剛才提到需要考慮保護(hù)取址運(yùn)算符,是嗎?我想知道。”
“好的。首先要聲明的是,幾乎所有的人都會(huì)認(rèn)為對(duì)取址運(yùn)算符的重載是邪惡的。甚至說(shuō),boost為了防止該行為所產(chǎn)生的錯(cuò)誤更是提供了 addressof()函數(shù)。而另一方面,我們需要討論用戶為什么要用取址運(yùn)算符。Singleton所返回的常常是一個(gè)引用,對(duì)引用進(jìn)行取址將得到相應(yīng) 類型的指針。而從語(yǔ)法上來(lái)說(shuō),引用和指針的最大區(qū)別在于是否可以被delete關(guān)鍵字刪除以及是否可以為NULL。但是Singleton返回一個(gè)引用也 就表示其生存期由非用戶代碼所管理。因此使用取址運(yùn)算符獲得指針后又用delete關(guān)鍵字刪除Singleton所返回的實(shí)例明顯是一個(gè)用戶錯(cuò)誤。綜上所 述,通過(guò)將取址運(yùn)算符設(shè)置為私有沒(méi)有多少意義。”
重用
“好的,現(xiàn)在我們換個(gè)話題。如果我現(xiàn)在有幾個(gè)類型都需要實(shí)現(xiàn)為Singleton,那我應(yīng)怎樣使用你所編寫的這段代碼呢?”
剛剛還在洋洋自得的我恍然大悟:這個(gè)Singleton實(shí)現(xiàn)是無(wú)法重用的。沒(méi)辦法,只好一邊想一邊說(shuō):“一般來(lái)說(shuō),較為流行的重用方法一共有三 種:組合、派生以及模板。首先可以想到的是,對(duì)Singleton的重用僅僅是對(duì)Instance()函數(shù)的重用,因此通過(guò)從Singleton派生以繼 承該函數(shù)的實(shí)現(xiàn)是一個(gè)很好的選擇。而Instance()函數(shù)如果能根據(jù)實(shí)際類型更改返回類型則更好了。因此奇異遞歸模板(CRTP,The Curiously Recurring Template Pattern)模式則是一個(gè)非常好的選擇。”于是我在白板上飛快地寫下了下面的代碼:
1 template <typename T> 2 class Singleton 3 { 4 public: 5 static T& Instance() 6 { 7 static T s_Instance; 8 return s_Instance; 9 } 10 11 protected: 12 Singleton(void) {} 13 ~Singleton(void) {} 14 15 private: 16 Singleton(const Singleton& rhs) {} 17 Singleton& operator = (const Singleton& rhs) {} 18 };
同時(shí)我也在白板上寫下了對(duì)該Singleton實(shí)現(xiàn)進(jìn)行重用的方法:
1 class SingletonInstance : public Singleton<SingletonInstance>…
“在需要重用該Singleton實(shí)現(xiàn)時(shí),我們僅僅需要從Singleton派生并將Singleton的泛型參數(shù)設(shè)置為該類型即可。”
生存期管理
“我看你在實(shí)現(xiàn)中使用了靜態(tài)變量,那你是否能介紹一下上面Singleton實(shí)現(xiàn)中有關(guān)生存期的一些特征嗎?畢竟生存期管理也是編程中的一個(gè)重要話題。”面試官提出了下一個(gè)問(wèn)題。
“嗯,讓我想一想。我認(rèn)為對(duì)Singleton的生存期特性的討論需要分為兩個(gè)方面:Singleton內(nèi)使用的靜態(tài)變量的生存期以及 Singleton外在用戶代碼中所表現(xiàn)的生存期。Singleton內(nèi)使用的靜態(tài)變量是一個(gè)局部靜態(tài)變量,因此只有在Singleton的 Instance()函數(shù)被調(diào)用時(shí)其才會(huì)被創(chuàng)建,從而擁有了延遲初始化(Lazy)的效果,提高了程序的啟動(dòng)性能。同時(shí)該實(shí)例將生存至程序執(zhí)行完畢。而就 Singleton的用戶代碼而言,其生存期貫穿于整個(gè)程序生命周期,從程序啟動(dòng)開(kāi)始直到程序執(zhí)行完畢。當(dāng)然,Singleton在生存期上的一個(gè)缺陷就 是創(chuàng)建和析構(gòu)時(shí)的不確定性。由于Singleton實(shí)例會(huì)在Instance()函數(shù)被訪問(wèn)時(shí)被創(chuàng)建,因此在某處新添加的一處對(duì)Singleton的訪問(wèn) 將可能導(dǎo)致Singleton的生存期發(fā)生變化。如果其依賴于其它組成,如另一個(gè)Singleton,那么對(duì)它們的生存期進(jìn)行管理將成為一個(gè)災(zāi)難。甚至可 以說(shuō),還不如不用Singleton,而使用明確的實(shí)例生存期管理。”
“很好,你能提到程序初始化及關(guān)閉時(shí)單件的構(gòu)造及析構(gòu)順序的不確定可能導(dǎo)致致命的錯(cuò)誤這一情況。可以說(shuō),這是通過(guò)局部靜態(tài)變量實(shí)現(xiàn) Singleton的一個(gè)重要缺點(diǎn)。而對(duì)于你所提到的多個(gè)Singleton之間相互關(guān)聯(lián)所導(dǎo)致的生存期管理問(wèn)題,你是否有解決該問(wèn)題的方法呢?”
我突然間意識(shí)到自己給自己出了一個(gè)難題:“有,我們可以將Singleton的實(shí)現(xiàn)更改為使用全局靜態(tài)變量,并將這些全局靜態(tài)變量在文件中按照特定順序排序即可。”
“但是這樣的話,靜態(tài)變量將使用eager initialization的方式完成初始化,可能會(huì)對(duì)性能影響較大。其實(shí),我想聽(tīng)你說(shuō)的是,對(duì)于具有關(guān)聯(lián)的兩個(gè)Singleton,對(duì)它們進(jìn)行使用的 代碼常常局限在同一區(qū)域內(nèi)。該問(wèn)題的一個(gè)解決方法常常是將對(duì)它們進(jìn)行使用的管理邏輯實(shí)現(xiàn)為Singleton,而在內(nèi)部邏輯中對(duì)它們進(jìn)行明確的生存期管 理。但不用擔(dān)心,因?yàn)檫@個(gè)答案也過(guò)于經(jīng)驗(yàn)之談。那么下一個(gè)問(wèn)題,你既然提到了全局靜態(tài)變量能解決這個(gè)問(wèn)題,那是否可以講解一下全局靜態(tài)變量的生命周期是怎 樣的呢?”
“編譯器會(huì)在程序的main()函數(shù)執(zhí)行之前插入一段代碼,用來(lái)初始化全局變量。當(dāng)然,靜態(tài)變量也包含在內(nèi)。該過(guò)程被稱為靜態(tài)初始化。”
“嗯,很好。使用全局靜態(tài)變量實(shí)現(xiàn)Singleton的確會(huì)對(duì)性能造成一定影響。但是你是否注意到它也有一定的優(yōu)點(diǎn)呢?”
見(jiàn)我許久沒(méi)有回答,面試官主動(dòng)幫我解了圍:“是線程安全性。由于在靜態(tài)初始化時(shí)用戶代碼還沒(méi)有來(lái)得及執(zhí)行,因此其常常處于單線程環(huán)境下,從而保 證了Singleton真的只有一個(gè)實(shí)例。當(dāng)然,這并不是一個(gè)好的解決方法。所以,我們來(lái)談?wù)凷ingleton的多線程實(shí)現(xiàn)吧。”
多線程
“首先請(qǐng)你寫一個(gè)線程安全的Singleton實(shí)現(xiàn)。”
我拿起筆,在白板上寫下早已爛熟于心的多線程安全實(shí)現(xiàn):
1 template <typename T> 2 class Singleton 3 { 4 public: 5 static T& Instance() 6 { 7 if (m_pInstance == NULL) 8 { 9 Lock lock; 10 if (m_pInstance == NULL) 11 { 12 m_pInstance = new T(); 13 atexit(Destroy); 14 } 15 return *m_pInstance; 16 } 17 return *m_pInstance; 18 } 19 20 protected: 21 Singleton(void) {} 22 ~Singleton(void) {} 23 24 private: 25 Singleton(const Singleton& rhs) {} 26 Singleton& operator = (const Singleton& rhs) {} 27 28 void Destroy() 29 { 30 if (m_pInstance != NULL) 31 delete m_pInstance; 32 m_pInstance = NULL; 33 } 34 35 static T* volatile m_pInstance; 36 }; 37 38 template <typename T> 39 T* Singleton<T>::m_pInstance = NULL;
“寫得很精彩。那你是否能逐行講解一下你寫的這個(gè)Singleton實(shí)現(xiàn)呢?”
“好的。首先,我使用了一個(gè)指針記錄創(chuàng)建的Singleton實(shí)例,而不再是局部靜態(tài)變量。這是因?yàn)榫植快o態(tài)變量可能在多線程環(huán)境下出現(xiàn)問(wèn)題。”
“我想插一句話,為什么局部靜態(tài)變量會(huì)在多線程環(huán)境下出現(xiàn)問(wèn)題?”
“這是由局部靜態(tài)變量的實(shí)際實(shí)現(xiàn)所決定的。為了能滿足局部靜態(tài)變量只被初始化一次的需求,很多編譯器會(huì)通過(guò)一個(gè)全局的標(biāo)志位記錄該靜態(tài)變量是否已經(jīng)被初始化的信息。那么,對(duì)靜態(tài)變量進(jìn)行初始化的偽碼就變成下面這個(gè)樣子:”。
1 bool flag = false; 2 if (!flag) 3 { 4 flag = true; 5 staticVar = initStatic(); 6 }
“那么在第一個(gè)線程執(zhí)行完對(duì)flag的檢查并進(jìn)入if分支后,第二個(gè)線程將可能被啟動(dòng),從而也進(jìn)入if分支。這樣,兩個(gè)線程都將執(zhí)行對(duì)靜態(tài)變量 的初始化。因此在這里,我使用了指針,并在對(duì)指針進(jìn)行賦值之前使用鎖保證在同一時(shí)間內(nèi)只能有一個(gè)線程對(duì)指針進(jìn)行初始化。同時(shí)基于性能的考慮,我們需要在每 次訪問(wèn)實(shí)例之前檢查指針是否已經(jīng)經(jīng)過(guò)初始化,以避免每次對(duì)Singleton的訪問(wèn)都需要請(qǐng)求對(duì)鎖的控制權(quán)。”
“同時(shí),”我咽了口口水繼續(xù)說(shuō),“因?yàn)閚ew運(yùn)算符的調(diào)用分為分配內(nèi)存、調(diào)用構(gòu)造函數(shù)以及為指針賦值三步,就像下面的構(gòu)造函數(shù)調(diào)用:”
1 SingletonInstance pInstance = new SingletonInstance();
“這行代碼會(huì)轉(zhuǎn)化為以下形式:”
1 SingletonInstance pHeap = __new(sizeof(SingletonInstance)); 2 pHeap->SingletonInstance::SingletonInstance(); 3 SingletonInstance pInstance = pHeap;
“這樣轉(zhuǎn)換是因?yàn)樵贑++標(biāo)準(zhǔn)中規(guī)定,如果內(nèi)存分配失敗,或者構(gòu)造函數(shù)沒(méi)有成功執(zhí)行, new運(yùn)算符所返回的將是空。一般情況下,編譯器不會(huì)輕易調(diào)整這三步的執(zhí)行順序,但是在滿足特定條件時(shí),如構(gòu)造函數(shù)不會(huì)拋出異常等,編譯器可能出于優(yōu)化的 目的將第一步和第三步合并為同一步:”
1 SingletonInstance pInstance = __new(sizeof(SingletonInstance)); 2 pInstance->SingletonInstance::SingletonInstance();
“這樣就可能導(dǎo)致其中一個(gè)線程在完成了內(nèi)存分配后就被切換到另一線程,而另一線程對(duì)Singleton的再次訪問(wèn)將由于pInstance已經(jīng) 賦值而越過(guò)if分支,從而返回一個(gè)不完整的對(duì)象。因此,我在這個(gè)實(shí)現(xiàn)中為靜態(tài)成員指針添加了volatile關(guān)鍵字。該關(guān)鍵字的實(shí)際意義是由其修飾的變量 可能會(huì)被意想不到地改變,因此每次對(duì)其所修飾的變量進(jìn)行操作都需要從內(nèi)存中取得它的實(shí)際值。它可以用來(lái)阻止編譯器對(duì)指令順序的調(diào)整。只是由于該關(guān)鍵字所提 供的禁止重排代碼是假定在單線程環(huán)境下的,因此并不能禁止多線程環(huán)境下的指令重排。”
“最后來(lái)說(shuō)說(shuō)我對(duì)atexit()關(guān)鍵字的使用。在通過(guò)new關(guān)鍵字創(chuàng)建類型實(shí)例的時(shí)候,我們同時(shí)通過(guò)atexit()函數(shù)注冊(cè)了釋放該實(shí)例的 函數(shù),從而保證了這些實(shí)例能夠在程序退出前正確地析構(gòu)。該函數(shù)的特性也能保證后被創(chuàng)建的實(shí)例首先被析構(gòu)。其實(shí),對(duì)靜態(tài)類型實(shí)例進(jìn)行析構(gòu)的過(guò)程與前面所提到 的在main()函數(shù)執(zhí)行之前插入靜態(tài)初始化邏輯相對(duì)應(yīng)。”
引用還是指針
“既然你在實(shí)現(xiàn)中使用了指針,為什么仍然在Instance()函數(shù)中返回引用呢?”面試官又拋出了新的問(wèn)題。
“這是因?yàn)镾ingleton返回的實(shí)例的生存期是由Singleton本身所決定的,而不是用戶代碼。我們知道,指針和引用在語(yǔ)法上的最大區(qū) 別就是指針可以為NULL,并可以通過(guò)delete運(yùn)算符刪除指針?biāo)傅膶?shí)例,而引用則不可以。由該語(yǔ)法區(qū)別引申出的語(yǔ)義區(qū)別之一就是這些實(shí)例的生存期意 義:通過(guò)引用所返回的實(shí)例,生存期由非用戶代碼管理,而通過(guò)指針?lè)祷氐膶?shí)例,其可能在某個(gè)時(shí)間點(diǎn)沒(méi)有被創(chuàng)建,或是可以被刪除的。但是這兩條 Singleton都不滿足,因此在這里,我使用指針,而不是引用。”
“指針和引用除了你提到的這些之外,還有其它的區(qū)別嗎?”
“有的。指針和引用的區(qū)別主要存在于幾個(gè)方面。從低層次向高層次上來(lái)說(shuō),分為編譯器實(shí)現(xiàn)上的,語(yǔ)法上的以及語(yǔ)義上的區(qū)別。就編譯器的實(shí)現(xiàn)來(lái)說(shuō), 聲明一個(gè)引用并沒(méi)有為引用分配內(nèi)存,而僅僅是為該變量賦予了一個(gè)別名。而聲明一個(gè)指針則分配了內(nèi)存。這種實(shí)現(xiàn)上的差異就導(dǎo)致了語(yǔ)法上的眾多區(qū)別:對(duì)引用進(jìn) 行更改將導(dǎo)致其原本指向的實(shí)例被賦值,而對(duì)指針進(jìn)行更改將導(dǎo)致其指向另一個(gè)實(shí)例;引用將永遠(yuǎn)指向一個(gè)類型實(shí)例,從而導(dǎo)致其不能為NULL,并由于該限制而 導(dǎo)致了眾多語(yǔ)法上的區(qū)別,如dynamic_cast對(duì)引用和指針在無(wú)法成功進(jìn)行轉(zhuǎn)化時(shí)的行為不一致。而就語(yǔ)義而言,前面所提到的生存期語(yǔ)義是一個(gè)區(qū)別, 同時(shí)一個(gè)返回引用的函數(shù)常常保證其返回結(jié)果有效。一般來(lái)說(shuō),語(yǔ)義區(qū)別的根源常常是語(yǔ)法上的區(qū)別,因此上面的語(yǔ)義區(qū)別僅僅是列舉了一些例子,而真正語(yǔ)義上的 差別常常需要考慮它們的語(yǔ)境。”
“你在前面說(shuō)到了你的多線程內(nèi)部實(shí)現(xiàn)使用了指針,而返回類型是引用。在編寫過(guò)程中,你是否考慮了實(shí)例構(gòu)造不成功的情況,如new運(yùn)算符運(yùn)行失敗?”
“是的。在和其它人進(jìn)行討論的過(guò)程中,大家對(duì)于這種問(wèn)題有各自的理解。首先,對(duì)一個(gè)實(shí)例的構(gòu)造將可能在兩處拋出異常:new運(yùn)算符的執(zhí)行以及構(gòu) 造函數(shù)拋出的異常。對(duì)于new運(yùn)算符,我想說(shuō)的是幾點(diǎn)。對(duì)于某些操作系統(tǒng),例如Windows,其常常使用虛擬地址,因此其運(yùn)行常常不受物理內(nèi)存實(shí)際大小 的限制。而對(duì)于構(gòu)造函數(shù)中拋出的異常,我們有兩種策略可以選擇:在構(gòu)造函數(shù)內(nèi)對(duì)異常進(jìn)行處理,以及在構(gòu)造函數(shù)之外對(duì)異常進(jìn)行處理。在構(gòu)造函數(shù)內(nèi)對(duì)異常進(jìn)行 處理可以保證類型實(shí)例處于一個(gè)有效的狀態(tài),但一般不是我們想要的實(shí)例狀態(tài)。這樣一個(gè)實(shí)例會(huì)導(dǎo)致后面對(duì)它的使用更為繁瑣,例如需要更多的處理邏輯或再次導(dǎo)致 程序執(zhí)行異常。反過(guò)來(lái),在構(gòu)造函數(shù)之外對(duì)異常進(jìn)行處理常常是更好的選擇,因?yàn)檐浖_(kāi)發(fā)人員可以根據(jù)產(chǎn)生異常時(shí)所構(gòu)造的實(shí)例的狀態(tài)將一定范圍內(nèi)的各個(gè)變量更 改為合法的狀態(tài)。舉例來(lái)說(shuō),我們?cè)谝粋€(gè)函數(shù)中嘗試創(chuàng)建一對(duì)相互關(guān)聯(lián)的類型實(shí)例,那么在一個(gè)實(shí)例的構(gòu)造函數(shù)拋出了異常時(shí),我們不應(yīng)該在構(gòu)造函數(shù)里對(duì)該實(shí)例的 狀態(tài)進(jìn)行維護(hù),因?yàn)榍耙粋€(gè)實(shí)例的構(gòu)造是按照后一個(gè)實(shí)例會(huì)正常創(chuàng)建來(lái)進(jìn)行的。相對(duì)來(lái)說(shuō),放棄后一個(gè)實(shí)例,并將前一個(gè)實(shí)例刪除是一個(gè)比較好的選擇。”
我在白板上比劃了一下,繼續(xù)說(shuō)到:“我們知道,異常有兩個(gè)非常明顯的缺陷:效率,以及對(duì)代碼的污染。在太小的粒度中使用異常,就會(huì)導(dǎo)致異常使用次數(shù)的增加,對(duì)于效率以及代碼的整潔型都是傷害。同樣地,對(duì)拷貝構(gòu)造函數(shù)等組成常常需要使用類似的原則。”
“反過(guò)來(lái)說(shuō),Singleton的使用也可以保持著這種原則。Singleton僅僅是一個(gè)包裝好的全局實(shí)例,對(duì)其的創(chuàng)建如果一旦不成功,在較高層次上保持正常狀態(tài)同樣是一個(gè)較好的選擇。”
Anti-Patten
“既然你提到了Singleton僅僅是一個(gè)包裝好的全局變量,那你能說(shuō)說(shuō)它和全局變量的相同與不同么?”
“單件可以說(shuō)是全局變量的替代品。其擁有全局變量的眾多特點(diǎn):全局可見(jiàn)且貫穿應(yīng)用程序的整個(gè)生命周期。除此之外,單件模式還擁有一些全局變量所 不具有的性質(zhì):同一類型的對(duì)象實(shí)例只能有一個(gè),同時(shí)適當(dāng)?shù)膶?shí)現(xiàn)還擁有延遲初始化(Lazy)的功能,可以避免耗時(shí)的全局變量初始化所導(dǎo)致的啟動(dòng)速度不佳等 問(wèn)題。要說(shuō)明的是,Singleton的最主要目的并不是作為一個(gè)全局變量使用,而是保證類型實(shí)例有且僅有一個(gè)。它所具有的全局訪問(wèn)特性僅僅是它的一個(gè)副 作用。但正是這個(gè)副作用使它更類似于包裝好的全局變量,從而允許各部分代碼對(duì)其直接進(jìn)行操作。軟件開(kāi)發(fā)人員需要通過(guò)仔細(xì)地閱讀各部分對(duì)其進(jìn)行操作的代碼才 能了解其真正的使用方式,而不能通過(guò)接口得到組件依賴性等信息。如果Singleton記錄了程序的運(yùn)行狀態(tài),那么該狀態(tài)將是一個(gè)全局狀態(tài)。各個(gè)組件對(duì)其 進(jìn)行操作的調(diào)用時(shí)序?qū)⒆兊檬种匾瑥亩垢鱾€(gè)組件之間存在著一種隱式的依賴。”
“從語(yǔ)法上來(lái)講,首先Singleton模式實(shí)際上將類型功能與類型實(shí)例個(gè)數(shù)限制的代碼混合在了一起,違反了SRP。其次Singleton模式在Instance()函數(shù)中將創(chuàng)建一個(gè)確定的類型,從而禁止了通過(guò)多態(tài)提供另一種實(shí)現(xiàn)的可能。”
“但是從系統(tǒng)的角度來(lái)講,對(duì)Singleton的使用則是無(wú)法避免的:假設(shè)一個(gè)系統(tǒng)擁有成百上千個(gè)服務(wù),那么對(duì)它們的傳遞將會(huì)成為系統(tǒng)的一個(gè)災(zāi) 難。從微軟所提供的眾多類庫(kù)上來(lái)看,其常常提供一種方式獲得服務(wù)的函數(shù),如GetService()等。另外一個(gè)可以減輕Singleton模式所帶來(lái)不 良影響的方法則是為Singleton模式提供無(wú)狀態(tài)或狀態(tài)關(guān)聯(lián)很小的實(shí)現(xiàn)。”
“也就是說(shuō),Singleton本身并不是一個(gè)非常差的模式,對(duì)其使用的關(guān)鍵在于何時(shí)使用它并正確的使用它。”
面試官抬起手腕看了看時(shí)間:“好了,時(shí)間已經(jīng)到了。你的C++功底已經(jīng)很好了。我相信,我們會(huì)在不久的將來(lái)成為同事。”
筆者注:這本是Writing Patterns Line by Line的一篇文章,但最后想想,寫模式的人太多了,我還是省省吧。。。
下一篇回歸WPF,環(huán)境剛好。可能中間穿插些別的內(nèi)容,比如HTML5,JS,安全等等。
頭一次寫小品文,不知道效果是不是好。因?yàn)檫@種文章的特點(diǎn)是知識(shí)點(diǎn)分散,而且隱藏在文章的每一句話中。。。好處就是寫起來(lái)輕松,呵呵。。。