#單例模式(Singleton Pattern)
##簡介
單例模式,也叫單子模式,是一種常用的軟件設計模式。在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候整個系統只需要擁有一個的全局對象,這樣有利于我們協調系統整體的行為。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然后服務進程中的其他對象再通過這個單例對象獲取這些配置信息。這種方式簡化了在復雜環境下的配置管理。
實現單例模式的思路是:一個類能返回對象一個引用(永遠是同一個)和一個獲得該實例的方法(必須是靜態方法,通常使用getInstance這個名稱);當我們調用這個方法時,如果類持有的引用不為空就返回這個引用,如果類保持的引用為空就創建該類的實例并將實例的引用賦予該類保持的引用;同時我們還將該類的構造函數定義為私有方法,這樣其他處的代碼就無法通過調用該類的構造函數來實例化該類的對象,只有通過該類提供的靜態方法來得到該類的唯一實例。
單例模式在多線程的應用場合下必須小心使用。如果當唯一實例尚未創建時,有兩個線程同時調用創建方法,那么它們同時沒有檢測到唯一實例的存在,從而同時各自創建了一個實例,這樣就有兩個實例被構造出來,從而違反了單例模式中實例唯一的原則。 解決這個問題的辦法是為指示類是否已經實例化的變量提供一個互斥鎖(雖然這樣會降低效率)
JAVA的單例實現
```
public class Singleton {
private static volatile Singleton INSTANCE = null;
// Private constructor suppresses
// default public constructor
private Singleton() {}
//thread safe and performance promote
public static Singleton getInstance() {
if(INSTANCE == null){
synchronized(Singleton.class){
//when more than two threads run into the first null check same time, to avoid instanced more than one time, it needs to be checked again.
if(INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
```
###避免單例的濫用
單例模式固然好用,但感覺有點過度,將接口設計成單例入口前需要考慮一下:
* 這個類表達的含義真的只能有一個實例么?(如UIApplication)還是只是為了好調用而已?
* 這個單例持有的內存一直存在
* 是否能用類方法代替?
* 這個單例對象是否能成為另一個單例對象的屬性?如果是,應該作為屬性
##實例
***單例模式:保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。***
單例是整個 Cocoa 中被廣泛使用的核心設計模式之一。事實上,蘋果開發者庫把單例作為 "Cocoa 核心競爭力" 之一。作為一個iOS開發者,我們經常和單例打交道,比如 UIApplication 和 NSFileManager 等等。我們在開源項目、蘋果示例代碼和 StackOverflow 中見過了無數使用單例的例子。Xcode 甚至有一個默認的 "Dispatch Once" 代碼片段,可以使我們非常簡單地在代碼中添加一個單例:
```
+ (instancetype)sharedInstance
{
static dispatch_once_t once;
static id sharedInstance;
dispatch_once(&once, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
```
由于這些原因,單例在 iOS 開發中隨處可見。問題是,它們很容易被濫用。
盡管有些人認為單例是 '反模式', '魔鬼' 以及 '病態的說謊者',我不會去完全否認單例所帶來的的好處,而是會展示一些使用單例所帶來的問題,這樣下一次在使用 dispatch_once 代碼片段的自動補全功能時,你可以對它的影響進行評估,三思而行。
###全局狀態
大多數的開發者都認同使用全局可變的狀態是不好的行為。太多狀態使得程序難以理解,難以調試。我們這些面向對象的程序員在最小化代碼的狀態復雜程度的方面,有很多需要向函數式編程學習的地方。
```
@implementation SPMath {
NSUInteger _a;
NSUInteger _b;
}
- (NSUInteger)computeSum
{
return _a + _b;
}
```
在上面這個簡單的數學庫的實現中,程序員需要在調用 `computeSum` 前正確的設置實例變量 `_a` 和 `_b`。這樣有以下問題:
* computeSum 沒有顯式地通過使用參數的形式聲明它依賴于 _a 和 _b 的狀態。與僅僅通過查看函數聲明就可以知道這個函數的輸出依賴于哪些變量不同的是,另一個開發者必須查看這個函數的具體實現才能明白這個函數依賴那些變量。隱藏依賴是不好的。
* 當為調用 `computeSum` 做準備而修改 `_a` 和 `_b` 的數值時,程序員需要保證這些修改不會影響任何其他依賴于這兩個變量的代碼的正確性。而這在多線程的環境中是尤其困難的。
把下面的代碼和上面的例子做對比:
```
+ (NSUInteger)computeSumOf:(NSUInteger)a plus:(NSUInteger)b
{
return a + b;
}
```
這里,對變量 `a` 和 `b` 的依賴被顯式地聲明了。我們不需要為了調用這個方法而去改變實例變量的狀態。并且我們也不需要擔心調用這個函數會留下持久的副作用。我們甚至可以把這個方法聲明為類方法,這樣就告訴了代碼的閱讀者這個方法不會修改任何實例的狀態。
那么,這個例子和單例又有什么關系呢?用 Mi?ko Hevery 的話來說,[單例就是披著羊皮的全局狀態](http://misko.hevery.com/2008/08/25/root-cause-of-singletons/)。一個單例可以被使用在任何地方,而不需要顯式地聲明依賴。就像變量 `_a` 和 `_b` 在 `computeSum` 內部被使用了,卻沒有被顯式聲明一樣,程序的任意模塊都可以調用 `[SPMySingleton sharedInstance]` 并且訪問這個單例。***這意味著任何和這個單例交互產生的副作用都會影響程序其他地方的任意代碼。***
```
@interface SPSingleton : NSObject
+ (instancetype)sharedInstance;
- (NSUInteger)badMutableState;
- (void)setBadMutableState:(NSUInteger)badMutableState;
@end
@implementation SPConsumerA
- (void)someMethod
{
if ([[SPSingleton sharedInstance] badMutableState]) {
// ...
}
}
@end
@implementation SPConsumerB
- (void)someOtherMethod
{
[[SPSingleton sharedInstance] setBadMutableState:0];
}
@end
```
在上面的例子中,`SPConsumerA` 和 `SPConsumerB` 是兩個完全獨立的模塊。但是 `SPConsumerB` 可以通過使用單例提供的共享狀態來影響 `SPConsumerA` 的行為。這種情況應該只能發生在 `consumer B` 顯式引用了 A,并表明了兩者之間的關系時。這里使用了單例,由于其具有全局和多狀態的特性,導致隱式地在兩個看起來完全不相關的模塊之間建立了耦合。
讓我們來看一個更具體的例子,并且暴露一個使用全局可變狀態的額外問題。比如我們想要在我們的應用中構建一個網頁查看器。為了支持這個查看器,我們構建了一個簡單的 URL cache:
```
@interface SPURLCache
+ (SPCache *)sharedURLCache;
- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request;
@end
```
這個開發者開始寫一些單元測試來保證代碼在一些不同的情況下都能達到預期。首先,他寫了一個測試用例來保證網頁查看器在設備沒有連接時能夠展示出錯誤信息。然后他寫了一個測試用例來保證網頁查看器能夠正確的處理服務器錯誤。最后,他為成功情況時寫了一個測試用例,來保證返回的網絡內容能夠被正確的顯示出來。這個開發者運行了所有的測試用例,并且它們都如預期一樣正確。贊!
幾個月以后,這些測試用例開始出現失敗,盡管網頁查看器的代碼從它寫完后就從來沒有再改動過!到底發生了什么?
原來,有人改變了測試的順序。處理成功的那個測試用例首先被運行,然后再運行其他兩個。處理錯誤的那兩個測試用例現在竟然成功了,和預期不一樣,因為 URL cache 這個單例把不同測試用例之間的 response 緩存起來了。
持久化狀態是單元測試的敵人,因為單元測試在各個測試用例相互獨立的情況下才有效。如果狀態從一個測試用例傳遞到了另外一個,這樣就和測試用例的執行順序就有關系了。有 bug 的測試用例,尤其是那些本來不應該通過的測試用例,是非常糟糕的事情。
###對象的生命周期
另外一個關鍵問題就是單例的生命周期。當你在程序中添加一個單例時,很容易會認為 “永遠只會有一個實例”。但是在很多我看到過的 iOS 代碼中,這種假定都可能被打破。
比如,假設我們正在構建一個應用,在這個應用里用戶可以看到他們的好友列表。他們的每個朋友都有一張個人信息的圖片,并且我們想使我們的應用能夠下載并且在設備上緩存這些圖片。 使用 dispatch_once 代碼片段,我們可以寫一個 SPThumbnailCache 單例:
```
@interface SPThumbnailCache : NSObject
+ (instancetype)sharedThumbnailCache;
- (void)cacheProfileImage:(NSData *)imageData forUserId:(NSString *)userId;
- (NSData *)cachedProfileImageForUserId:(NSString *)userId;
@end
```
我們繼續構建我們的應用,一切看起來都很正常,直到有一天,我們決定去實現‘注銷’功能,這樣用戶可以在應用中進行賬號切換。突然我們發現我們將要面臨一個討厭的問題:用戶相關的狀態存儲在全局單例中。當用戶注銷后,我們希望能夠清理掉所有的硬盤上的持久化狀態。否則,我們將會把這些被遺棄的數據殘留在用戶的設備上,浪費寶貴的硬盤空間。對于用戶登出又登錄了一個新的賬號這種情況,我們也想能夠對這個新用戶使用一個全新的 SPThumbnailCache 實例。
問題在于按照定義單例被認為是“創建一次,永久有效”的實例。你可以想到一些對于上述問題的解決方案。或許我們可以在用戶登出時移除這個單例:
```
static SPThumbnailCache *sharedThumbnailCache;
+ (instancetype)sharedThumbnailCache
{
if (!sharedThumbnailCache) {
sharedThumbnailCache = [[self alloc] init];
}
return sharedThumbnailCache;
}
+ (void)tearDown
{
// The SPThumbnailCache will clean up persistent states when deallocated
sharedThumbnailCache = nil;
}
```
這是一個明顯的對單例模式的濫用,但是它可以工作,對吧?
我們當然可以使用這種方式去解決,但是代價實在是太大了。我們不能使用簡單的的 `dispatch_once` 方案了,而這個方案能夠保證線程安全以及所有調用 `[SPThumbnailCache sharedThumbnailCache]` 的地方都能訪問到同一個實例。現在我們需要對使用縮略圖 cache 的代碼的執行順序非常小心。假設當用戶正在執行登出操作時,有一些后臺任務正在執行把圖片保存到緩存中的操作:
```
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[SPThumbnailCache sharedThumbnailCache] cacheProfileImage:newImage forUserId:userId];
});
```
我們需要保證在所有的后臺任務完成前, tearDown 一定不能被執行。這確保了 newImage 數據可以被正確的清理掉。或者,我們需要保證在縮略圖 cache 被移除時,后臺緩存任務一定要被取消掉。否則,一個新的縮略圖 cache 的實例將會被延遲創建,并且之前用戶的數據 (newImage 對象) 會被存儲在它里面。
由于對于單例實例來說它沒有明確的所有者,(因為單例自己管理自己的生命周期),“關閉”一個單例變得非常的困難。
分析到這里,我希望你能夠意識到,“這個縮略圖 cache 從來就不應該作為一個單例!”。問題在于一個對象得生命周期可能在項目的最初階段沒有被很好得考慮清楚。舉一個具體的例子,Dropbox 的 iOS 客戶端曾經只支持一個賬號登錄。它以這樣的狀態存在了數年,直到有一天我們希望能夠同時支持多個用戶賬號登錄 (同時登陸私人賬號和工作賬號)。突然之間,我們以前的的假設“只能夠同時有一個用戶處于登錄狀態”就不成立了。如果假定了一個對象的生命周期和應用的生命周期一致,那你的代碼的靈活擴展就受到了限制,早晚有一天當產品的需求產生變化時,你會為當初的這個假定付出代價的。
這里我們得到的教訓是,單例應該只用來保存全局的狀態,并且不能和任何作用域綁定。如果這些狀態的作用域比一個完整的應用程序的生命周期要短,那么這個狀態就不應該使用單例來管理。用一個單例來管理用戶綁定的狀態,是代碼的壞味道,你應該認真的重新評估你的對象圖的設計。
###避免使用單例
既然單例對局部作用域的狀態有這么多的壞處,那么我們應該怎樣避免使用它們呢?
讓我們來重溫一下上面的例子。既然我們的縮略圖 cache 的緩存狀態是和具體的用戶綁定的,那么讓我們來定義一個user對象吧:
```
@interface SPUser : NSObject
@property (nonatomic, readonly) SPThumbnailCache *thumbnailCache;
@end
@implementation SPUser
- (instancetype)init
{
if ((self = [super init])) {
_thumbnailCache = [[SPThumbnailCache alloc] init];
// Initialize other user-specific state...
}
return self;
}
@end
```
我們現在用一個對象來作為一個經過認證的用戶會話的模型類,并且我們可以把所有和用戶相關的狀態存儲在這個對象中。現在假設我們有一個view controller來展現好友列表:
```
@interface SPFriendListViewController : UIViewController
- (instancetype)initWithUser:(SPUser *)user;
@end
```
我們可以顯式地把經過認證的 user 對象作為參數傳遞給這個 view controller。這種把依賴性傳遞給依賴對象的技術正式的叫法是依賴注入,它有很多優點:
1. 對于閱讀這個 SPFriendListViewController 頭文件的讀者來說,可以很清楚的知道它只有在有登錄用戶的情況下才會被展示。
2. 這個 SPFriendListViewController 只要還在使用中,就可以強引用 user 對象。舉例來說,對于前面的例子,我們可以像下面這樣在后臺任務中保存一個圖片到縮略圖 cache 中:
```
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[_user.thumbnailCache cacheProfileImage:newImage forUserId:userId];
});
```
就算后臺任務還沒有完成,應用其他地方的代碼也可以創建和使用一個全新的 SPUser 對象,而不會在清理第一個實例時阻塞用戶交互。
為了更詳細的說明一下第二點,讓我們畫一下在使用依賴注入之前和之后的對象圖。
假設我們的 SPFriendListViewController 是當前 window 的 root view controller。使用單例時,我們的對象圖看起來如下所示:

view controller 自己,以及自定義的 image view 的列表,都會和 sharedThumbnailCache 產生交互。當用戶登出后,我們想要清理 root view controller 并且退出到登錄頁面:

這里的問題在于這個好友列表的 view controller 可能仍然在執行代碼 (由于后臺操作的原因),并且可能因此仍然有一些沒有執行的涉及到 sharedThumbnailCache 的調用。
和使用依賴注入的解決方案對比一下:

簡單起見,假設 SPApplicationDelegate 管理 SPUser 的實例 (在實踐中,你可能會把這些用戶狀態的管理工作交給另外一個對象來做,這樣可以使你的 application delegate 簡化)。當展現好友列表 view controller 時,會傳遞進去一個 user 的引用。這個引用也會向下傳遞給 profile image views。現在,當用戶登出時,我們的對象圖如下所示:

這個對象圖看起來和使用單例時很像。那么,區別是什么呢?
關鍵問題是作用域。在單例那種情況中,sharedThumbnailCache 仍然可以被程序的任意模塊訪問。假如用戶快速的登錄了一個新的賬號。該用戶也想看看他的好友列表,這也就意味著需要再一次的和縮略圖 cache 產生交互:

當用戶登錄一個新賬號,我們應該能夠構建并且與全新的 SPThumbnailCache 交互,而不需要再在銷毀老的縮略圖 cache 上花費精力。基于對象管理的典型規則,老的 view controllers 和老的縮略圖 cache 應該能夠自己在后臺延遲被清理掉。簡而言之,我們應該隔離用戶 A 相關聯的狀態和用戶 B 相關聯的狀態:

***這一切的關鍵點是,在面向對象編程中我們想要最小化可變狀態的作用域。但是單例卻因為使可變的狀態可以被程序中的任何地方訪問,而站在了對立面。下一次你想使用單例時,我希望你能夠好好考慮一下使用依賴注入作為替代方案。***