objc文章之避免滥用单例
这是一篇objc上的文章,解了我很多的关于单例的困惑,原文地址 中文翻译地址
1.单例介绍
单例是Cocoa中被广泛使用的设计模式之一, 比如系统的UIApplication和NSFileManager等。
+(instancetype)sharedInstance{
static dispatch_once_t once;
static id sharedInstance;
dispatch_once(&once,^{
sharedInstance = [[self alloc]init];
});
return sharedInstance;
}
单例的含义是:一个应用程序中,只有一个该类的实例。它在第一次被访问的时候创建,在应用结束的时候结束。
2. 单例的问题1:全局状态
大多数人认为全局状态是不好的行为,太多的状态难以理解,难以调试。
首先我们看一个例子1
@implementation SPMath
{
NSInteger _a;
NSInteger _b;
}
-(NSInteger)computeSum{
return _a + _b;
}
//例子1 有两个问题:
- computeSum函数没有显式的声明它依赖 _a 和 _b 。 我们需要看这个函数具体的实现才能明白这个函数依赖那些变量。 隐藏依赖不好。
- 当为调用computeSum做准备而修改_a 和 _b的数值的时候,我们需要保证这些修改不会影响其他依赖于这两个变量的代码的正确性,而这在多线程环境是尤其困难的。
下边是例子2:
+(NSInteger)computeSumOf:(NSInteger)a plus:(NSInteger)b{
return a+b
}
例子2中,我们显式的声明了依赖,我们不需要为了调用这个方法而去改变实例变量的状态。
这个例子和单例的关系: 单例是全局状态,它可以被使用在任何地方,而不需要显式的声明依赖。我们在代码的任何地方都可以调用[Singleton sharedInstance]来和这个单例交互,同时它也会影响到程序其他用到该单例地方的代码。
例子3:单例的全局状态对其他地方代码的影响
@interface SPSingleton : NSObject
+ (instancetype)sharedInstance;
- (NSUInteger)badMutableState;
- (void)setBadMutableState:(NSUInteger)badMutableState;
@end
@implementation SPConsumerA
- (void)someMethod
{
if ([[SPSingleton sharedInstance] badMutableState] ==0 ) {
//do somethingA
}else{
//do somethingB
}
}
@end
@implementation SPConsumerB
- (void)someOtherMethod
{
[[SPSingleton sharedInstance] setBadMutableState:0];
}
@end
// 上边的例子中,SPConsumerA和SPConsumerB是两个完全独立的模块,但是呢,SPConsumerB却可以通过使用单例,来影响到SPConsumerA的行为,这种情况只能发生在B模块显式的引用A模块来表明二者之间关系。 但是这里送单例,导致隐式地两个不相关的模块建立了耦合。
例子4(因为我对测试这块没掌握好,这里不是很了解)
@interface SPURLCache
+ (SPCache *)sharedURLCache;
- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request;
@end
我们写了一个网页查看器,并构建了一个URLCache。然后我们写了3个测试用例,没有网络的测试用例,失败的测试用例,成功的测试用例。 过一段时间,当有人改变了测试用例执行顺序的时候,发现测试用例不能正常运行了。
处理成功的那个测试用例首先被运行,然后再运行其他两个。处理错误的那两个测试用例现在竟然成功了,和预期不一样,因为 URL cache 这个单例把不同测试用例之间的 response 缓存起来了。
// 这里的结论是: 持久化的状态是单元测试的敌人。因为单元测试在各个测试用例相互独立的情况下才有效。如果状态从一个用例传递到另一个,这样就和测试用例的执行顺序有关系了。
3. 单例的问题2: 对象的生命周期。
当我们在程序中添加一个单例时,很容易认为,永远只会有一个实例。但是,在很多iOS代码中,这种假定很可能被打破。下面是一个例子:当单例的生命周期不是整个应用的时候,使用单例会不恰当。
//我们有一个应用程序,应用的用户可以看好友列表。有一个thumbnailCache单例,来在设备上缓存这些好友的图片信息。
例子5:
//使用dispatch_once创建单例
@interface SPThumbnailCache : NSObject
+ (instancetype)sharedThumbnailCache;
- (void)cacheProfileImage:(NSData *)imageData forUserId:(NSString *)userId;
@end
//考虑这样一种情形,当我们有一天实现注销功能,我们退出登录用户1,然后使用新的账号用户2登录。这时候我们需要清除之前缓存的信息。如果此时,单例在子线程执行缓存图片任务,那么因为单例一直存在,我们无法取消单例继续用户1的好友图片。 而此时,我们登录的是用户2。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[SPThumbnailCache sharedThumbnailCache] cacheProfileImage:newImage forUserId:userId];
});
结论:单例应该只用来保存全局的状态,并且不能和任何作用域绑定。如果这些状态的作用域比一个完整的应用程序的生命周期要短,那么这个状态就不应该使用单例来管理。用一个单例来管理用户绑定的状态,是不恰当的设计模式。
4. 如何避免使用单例:
依赖注入:我们可以显式的把对象作为参数传递给依赖对象。这种技术叫做依赖注入。 通过依赖注入的方式,避免使用单例。