1. 程式人生 > >單例模式與單例工廠

單例模式與單例工廠

單例模式

什麼是單例模式? 單例模式想一個大獨裁者,他規定在他的國度裡面,所有資料的訪問和請求都得經過他,甚至你要呼叫相關的函式也得經過它。學術一點就是,單例模式,為某一類需求和資料提供了統一的程式介面。主要的實現技術就是,確保全域性只有一個物件的例項存在。舉個例子把,比如NSNotificationCenter 中的 defaultCenter 負責全域性的訊息分發、NSFileManager 的 defaultManager 統一負責物理檔案的管理、NSUserDefaults 的 standardUserDefaults 統一管理使用者的配置檔案……不一而足。在整個iOS框架中,可以說是大規模使用了單例模式。

單例模式的原理及實現

在非ARC情況下實現一個單例

@implementation DZSinglonNoARC
+ (DZSinglonNoARC*) shareInstance
{
    static DZSinglonNoARC* share = nil;
    @synchronized(self)
    {
        if (!share) {
            share = [[super allocWithZone:NULL] init];
        }
    }
    return share;
}
+ (instancetype) allocWithZone:(struct _NSZone *)zone
{
    return [self shareInstance];
}
- (instancetype) copyWithZone:(NSZone*)zone
{
    return self;
}
- (id) retain
{
    return [DZSinglonNoARC shareInstance];
}
- (oneway void) release
{
 
}
- (instancetype) autorelease
{
    return self;
}
- (unsigned) retainCount
{
    return UINT_MAX;
}
@end
首先要初始化一個該類的靜態化變數

static DZSinglonNoARC* share = nil;
    @synchronized(self)
    {
        if (!share) {
            share = [[super allocWithZone:NULL] init];
        }
    }
    return share;
在這裡進行了加鎖處理,是為了防止多執行緒重入的情況下,造成靜態變數多次分配記憶體和初始化,從而會導致資料混亂。這裡加鎖的物件是self,實際上是

[DZSinglonNoARC class]
類DZSinglonNoARC的class物件,也就是說加鎖物件是全域性唯一的一個Class物件。而且在這裡share的定義放在了函式shareInstance之內,是要讓share變成一個函式內的區域性變數這樣可以防止,外部的異常訪問。看到網上有些教程中,把share的定義放在函式之外,變成了檔案內的一個全域性靜態變數,這樣會存在其他函式異常操作share的情況。

注意在初始化share的時候我們使用的是

share = [[super allocWithZone:NULL] init];
之所以這樣做,是為了防止想用[DZSinglonNoARC alloc]等函式的時候,會與allocWithZone引起的死鎖和死迴圈。

而關於此處加鎖的處理還有一個優化版本,在4.0以上的SDK有了閉包之後,我們可以這麼做

+ (DZSinglonNoARC*) share2
{
    static DZSinglonNoARC* share = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{    
        share = [[super allocWithZone:NULL] init];
    });    
    return share;
}
使用dispatch_once體帶原來的加鎖操作,這樣做的好處就是可以減少每次加鎖的時間,優化了程式效能。dispatch_once函式接收一個dispatch_once_t用於檢查該程式碼塊是否已經被排程的謂詞(是一個長整型,實際上作為BOOL使用)。它還接收一個希望在應用的生命週期內僅被排程一次的程式碼塊,對於本例就用於share例項的例項化。dispatch_once不僅意味著程式碼僅會被執行一次,而且還是執行緒安全的。完全可以替代相對低效的加鎖操作。

然後是過載了一些列的函式,過載這些函式的目的,就是修改原有的NSOjbect的記憶體操作相關的函式,保持記憶體中有且只有一個該類的物件。

arc下的實現

我們先看一個例子

@implementation DZSinglonARC
+  (DZSinglonARC*) shareInstance
{
    static DZSinglonARC* share = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        share = [[super allocWithZone:NULL] init];
    });
    return share;
}
+ (instancetype) allocWithZone:(struct _NSZone *)zone
{
    return [self shareInstance];
}
@end
通過上面的程式碼我們能夠看出其實在ARC下單例的實現方式與非ARC下大同小異,只不過是沒有過載記憶體管理的函式而已。而這也得益於ARC這種技術。

單例工廠

在實際的程式設計工作中,隨著專案規模的不斷擴大,我們往往發現在整個專案中存在著大量的單例。於是我們就會遇到一個問題如何去管理這些單例。 同時,也會遇到每一次都要按照上面的實現方式從頭來一遍來實現一個單例,從程式設計效率上看難免有些低下,畢竟很多程式碼都是相同的。使用設計模式的目標之一就是合理的幹掉重複的程式碼。那麼有沒有一個好的方式來管理這些單例呢,我們很自然的想到了工廠模式。

工廠方法模式(英語:Factory method pattern)是一種實現了“工廠”概念的面向物件設計模式。就像其他建立型模式一樣,它也是處理在不指定物件具體型別的情況下建立物件的問題。工廠方法模式的實質是“定義一個建立物件的介面,但讓實現這個介面的類來決定例項化哪個類。工廠方法讓類的例項化推遲到子類中進行。” 建立一個物件常常需要複雜的過程,所以不適合包含在一個複合物件中。建立物件可能會導致大量的重複程式碼,可能會需要複合物件訪問不到的資訊,也可能提供不了足夠級別的抽象,還可能並不是複合物件概念的一部分。工廠方法模式通過定義一個單獨的建立物件的方法來解決這些問題。由子類實現這個方法來建立具體型別的物件。(引用自WIKI)

工廠模式解決的就是這種,重建同類型物件的問題。而這裡,我們可以把單例看成同類型的一系列物件。那就建立一個單例工廠吧。 專案地址:https://github.com/yishuiliunian/DZSinglonFactory.git

先看一下如何實現一個簡單的單例工廠


@interface DZSingletonFactory()
{
    NSMutableDictionary* data;
}
@end
@implementation DZSingletonFactory
- (id) init
{
    self = [super init];
    if (self) {
        data = [[NSMutableDictionary alloc] init];
    }
    return self;
}
+ (DZSingletonFactory*) shareFactory
{
    static DZSingletonFactory* share = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        share = [[DZSingletonFactory alloc] init];
    });
    return share;
}
- (id) copyWithZone:(NSZone*)zone
{
    return self;
}
//over singlong
- (void) setShareData:(id)shareData  forKey:(NSString*)key
{
        if (shareData == nil) {
            return;
        }
        [data setObject:shareData forKey:key];
}
- (id) shareDataForKey:(NSString*)key
{
        return [data objectForKey:key];
}
- (id) shareInstanceFor:(Class)aclass
{
    NSString* className = [NSString stringWithFormat:@"%@",aclass];
     @synchronized(className)
    {
        id shareData = [self shareDataForKey:className];
        if (shareData == nil) {
            shareData = [[NSClassFromString(className) alloc] init];
            [self setShareData:shareData forKey:className];
        }
        return shareData;
    }
 
}
- (id) shareInstanceFor:(Class)aclass category:(NSString *)key
{
    NSString* className = [NSString stringWithFormat:@"%@",aclass];
    NSString* classKey = [NSString stringWithFormat:@"%@-%@",aclass,key];
    @synchronized(classKey)
    {
        id shareData = [self shareDataForKey:classKey];
        if (shareData == nil) {
            shareData = [[NSClassFromString(className) alloc] init];
            [self setShareData:shareData forKey:classKey];
        }
        return shareData;
    }
}
@end
其實單例工廠類也是一個例項,之所以這麼做,是因為我們所生產的單例總得有個倉庫存著吧。而這個單例工廠類除了有生產單例的功能,也擔負著倉庫儲存生產的單例的功能。私有變數NSMutableDictionary* data;就是倉庫。

而生產的車間是

- (id) shareInstanceFor:(Class)aclass
{
    NSString* className = [NSString stringWithFormat:@"%@",aclass];
     @synchronized(className)
    {
        id shareData = [self shareDataForKey:className];
        if (shareData == nil) {
            shareData = [[NSClassFromString(className) alloc] init];
            [self setShareData:shareData forKey:className];
        }
        return shareData;
    }
 
}
這個函式及其簡單,我們使用了Objective-C一些動態語言的特性,直接通過類Class物件來生成例項,如果你對objc的底層有些瞭解的話,應該知道其實Class也是一個物件,他也能夠執行objc的方法。也就是說,如果我們要生成類A的一個例項,只要我們有了類A的型別物件(Class)例項就OK,然後通過[(Class*)aClass new]你就能輕而易舉的生成一個例項。

在我們通過這種技術生成了一個單例的例項之後,將其儲存在倉庫data裡面,下次再次請求這個類的例項的時候,只要從倉庫中取出來用就行了。

我們甚至為了程式設計時再少寫點程式碼可以寫一個函式和巨集:

id  DZSingleForClass(Class a)
{
    return [DZShareSingleFactory shareInstanceFor:a];
}
..........
#define DZShareSingleFactory [DZSingletonFactory shareFactory]
#ifdef __cplusplus
extern "C" {
#endif
    id  DZSingleForClass(Class a);
#ifdef __cplusplus
}
#endif
這樣我們在需要建立單例的時候一句話就能搞定:

+ (DZSingletonFactory*) shareInstance
{
    return DZSingleForClass([DZShareSingleFactory class]);
}
不過這只是一個極度簡化版的單例工廠,很多保護性的措施還都沒做。比如對於allocWithZone的過載等,還有一些單例登出的操作。

其實這是一種這種的策略,我們沒有過載記憶體管理函式,是為了能夠在後面為了節省記憶體,在單例較長時間不用的時候將其銷燬掉,等下次用的時候再建立。

- (void) destoryInstanceFor:(Class)aclass
{
    NSString* className = [NSString stringWithFormat:@"%@",aclass];
    @synchronized(className) {
        if ([self shareDataForKey:className]) {
            [data removeObjectForKey:className];
        }
    }
}
當然這中使用方式,看起來不太像是嚴格意義上的單例模式,但是他卻完成單例模式最根本的意圖,把介面和功能統一。

模組管理系統

還記得文章的標題嗎?單例模式的進化,那麼這裡單例模式要進化到什麼地步呢?在實際的編碼過程中,隨著工程規模的不斷擴大,我們可能會在我們的專案中大規模的時候單例模式。就像是世界中,有了N多個獨裁者,協調這些獨裁者,對任何一個系統來說都是困難的。這個時候,就會遇到一個問題:如何有效的管理數量較多的單例。雖然我們使用單例工廠的方式,解決了單例統一構建的問題,但是沒有能夠解決統一管理的問題。這裡統一管理的問題包括:

單例初始化的控制,不同的單例在初始化的時候可能需要不同的引數。甚至有些單例可以延遲載入。
單例登出的統一管理。
能夠讓開發者或者SDK使用者,直接了當的看到整個工程中使用了多少單例,每個單例的大概模樣是怎麼樣子的。
管理各個單例之間的依賴關係。比如地理位置資訊單例可能會依賴資料庫的一個單例。
根據不同的需求,動態的載入某些單例。
其他等等……
這些需求一列,你會感覺怎麼像是在說一個包管理系統比如debian的apt-get或者node的npm之類的東西呢,甚至還有點像windows的動態連結庫或者linux的模組化機制。

其實如果我們剖析一下的話,其實單例模式是一種實現程式架構模組化的有利工具。他將某些內聚性非常高的功能,聚合在其一起,通過單一例項與外界互動。同時也降低了與其他單例之間的耦合性。從巨集觀的角度來看,我們可以把一個個單例看成一個個功能性模組,用一個形象的比喻就是外掛。這樣,一個充斥著大量單例,並且能夠對這些單例有效管理的系統,從巨集觀的角度看,就像是一個外掛系統(準確說是模組管理系統)。

而上面提到的專案DZSinglonFactory只是這個外掛系統開了個頭。有興趣的可以一起來玩上一個iOS上的簡單的外掛管理系統。