objc文章之避免滥用单例

2018-04-20  本文已影响8人  你weixiao的时候很美

这是一篇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 有两个问题:

下边是例子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. 如何避免使用单例:

依赖注入:我们可以显式的把对象作为参数传递给依赖对象。这种技术叫做依赖注入。 通过依赖注入的方式,避免使用单例。

上一篇 下一篇

猜你喜欢

热点阅读