局部单例模式
除了上文提到的MVVM模式之外,为了解决纯代码MVC中View和ViewController代码冗余问题,我个人还采取过一种局部单例的设计模式.
整个设计由以下部分组成
- Model: 用于管理数据,同MVC
- ViewController: 兼顾View和Controller功能
- View: 一些独立的View,如Cell等
- ModuleSingleton: 关键!一个局部单例,负责模块功能和模块数据
基本思想(哲⛢学)
任何事物都分型和质
- 型: 就是看到的东西,程序本身并不需要界面甚至模型,这些都是为了质而存在的
- 质: 就是事物的本质,程序本身是为了完成一些功能,完成功能需要的条件只是信息
那么型存在的理由是什么呢?也就是为了方便信息交流给人看和使用的.所以说MVC也好,MVVM也好,都没有把功能从Controller里分离出来,每个ViewController.m文件都有自己的功能,各自独立.为了减少Controller层的冗余,并不一定非要把View拆出去,也可以拆除对于用户和程序进行交流不必要的部分,也就是质.
模式分析
1. 业务分析
第一次实际操作的工作是设计一个支付工具的登录注册模块,首先我们来分析登录模块需要什么:
- 账户
- 登录密码
- 验证码
然后分析注册模块需要用到什么:
- 账户
- 验证码
- 登录密码
- 支付密码
- 密保问题
- 密保答案
- 身份证
根据以上分析,如果不是计算机,而是一个真实的人,比如我去银行柜台办理开户,我需要告诉柜员我要办理什么业务(登录or注册),然后给他必须提供的信息,然后柜员告诉我办理好了没有.只要我信息提供的足够全,柜员给我的结果应该只有两个,办理好了(YES)或没有办理好(NO).
我们假设这个柜员一次只能办理一个业务(实际上也是这样),你给出信息,执行业务,反馈结果,你给出第二次信息,执行第二个业务,反馈第二次结果,如此循环下去.
2. 模块单例数据池
经过业务分析,我们需要建立一个数据池,由于登录和注册有部分可以进行复用,所以我们的数据池包括
- 账户
- 验证码
- 登录密码
- 支付密码
- 密保问题
- 密保答案
- 身份证
数据池用于从外部接收数据
3. 模块单例业务池
所谓业务池,就是我这个模块能干什么.比如登录注册模块,一定会有登录和注册.
4. 模块单例业务逻辑
就如同步骤1中分析的,发起业务的我们只关心业务有没有办好,并不关心业务具体需要怎么办,就拿登录来的流程举例
- 接收账户密码
- 检查账户是不是正确(比如为一个手机号)
- 密码是不是符合要求
- 网络是不是正常
- 是否有验证码的需求
- 进行登录
而注册的流程就更为复杂
- 接收账户密码
- 检查账户是不是符合规范
- 登录密码是不是符合规范
- 支付密码是不是符合规范
- 密保问题是不是可以正常获取
- 密保答案是不是符合规范
- 身份证号是不是符合规范
- 网络是否正常
- 进行注册开户
从以上分析结合步骤3中的业务池来看,除去接收账户密码与进行登录/开户注册步骤之外,中间的步骤对于业务请求者应该是封闭的,可以单独在模块内部完成的.
5. 模块单例的成型
根据步骤1-4的分析,我们已经可以建立一个脱离了界面,本身就可以完成登录注册功能的模块,这个模块的特性是:
- 有一个数据池用于接收数据
- 有一个业务池用于接收指令并反馈
- 内部可以独立完成当前业务所有功能
- 单例声明周期可以被控制
这四个特征的1-3已经在前四步分析中进行归纳,第四点可以参考我的单例文章
模块的单例代码解析
ModuleSingleton.h
typedef void(^ModuleSingletonCompletion)(BOOL success, NSString *code, NSString *message);
@interface ModuleSingleton : NSObject
#pragma mark - ModuleSingleton Life Controller
+ (instancetype)sharedInstance;//开启单例
+ (void)haltSharedInstance;//关闭单例
#pragma mark - ModuleSingleton Data Manager //数据管理
- (void)cleanDataPool;//清除所有数据
- (void)cleanDataPoolExceptAccount;//清除除去帐号外的数据
#pragma mark - DataPool //数据池
@property (nonatomic, copy) NSString *account;
@property (nonatomic, copy) NSString *verifyCode;
@property (nonatomic, copy) NSString *loginPassword;
@property (nonatomic, copy) NSString *payPassword;
@property (nonatomic, copy) NSString *secQuestion;
@property (nonatomic, copy) NSString *secAnswer;
@property (nonatomic, copy) NSString *IDCardNo;
#pragma mark - FunctionPool //方法池
- (void)loginWithPasswordCompletion:(ModuleSingletonCompletion)completion;
- (void)registerOpenAccountCompletion:(ModuleSingletonCompletion)completion;
@end
代码解析
根据头文件可以知道暴露在外的只有方法和数据,这个模块就像一个柜员一样,你只需要告诉它必要的信息就可以,然后下达指令就可以了.
也就是说在ViewController中,界面本身并不和逻辑或者网络层对接,所有的功能都在单例中进行统一的对接管理,ViewController的.m文件只负责可视化的展示和视觉逻辑.
最后啰嗦一遍:
业务逻辑(比如例子中的检查)和网络通讯全部由ModuleSingleton单例处理,ViewController只负责可视化,没有可视化,Module本身也可以完成工作
VC中调用登录代码
[[ModuleSingleton shareInstance] setAccount:@"15010XXXXXX"];
[[ModuleSingleton shareInstance] setLoginPassword:@"**********"];
[[ModuleSingleton shareInstance] loginWithPasswordCompletion:^(BOOL success, NSString *code, NSString *message) {
if(success) { }else {}
}];
ModuleSingleton.m
static ModuleSingleton *_sharedInstance = nil;
static dispatch_once_t _onceToken;
NSString *const CheckPassed = @"CheckPassed";
NSString *const CheckErrorCode = @"-999999";
@implementation ModuleSingleton
#pragma mark - ModuleSingleton Life Controller
+ (instancetype)sharedInstance{
dispatch_once(&_onceToken, ^{
_sharedInstance = [[BPLoginManager alloc] initSharedInstance];
});
return _sharedInstance;
}
+ (void)haltSharedInstance {
if (_sharedInstance) {
_sharedInstance = nil;
_onceToken = 0;
}
}
- (void)dealloc {
NSLog(@"ModuleSingleton SharedInstance Did Halted, Address: %p ",self);
}
- (instancetype)initSharedInstance {
if (_sharedInstance) {
return self;
}
self = [super init];
if (self) {
[self cleanDataPool];
}
return self;
}
- (instancetype)init {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"ModuleSingleton must accessed by shareInstance" userInfo:nil];
}
#pragma mark - ModuleSingleton Data Manager //数据管理
- (void)cleanDataPool {
[self cleanDataPoolWithAccount:YES];
}
- (void)cleanDataPoolExceptAccount {
[self cleanDataPoolWithAccount:NO];
}
- (void)cleanDataPoolWithAccount:(BOOL)withAccount {
if(withAccount) {
account = @"";
}
verifyCode = @"";
loginPassword = @"";
payPassword = @"";
secQuestion = @"";
secAnswer = @"";
IDCardNo = @"";
}
#pragma mark - FunctionPool //方法池
- (void)loginWithPasswordCompletion:(ModuleSingletonCompletion)completion {
//检查发送数据
NSString *checkResult = [self checkLoginWithPassword];
if (![checkResult isEqualToString:CheckPassed]) {
completion(NO,CheckErrorCode,checkResult);
return;
}
//登录的网络层
[NetworkModuleService loginWithAccount:_account loginPassword:_loginPassword verifyCode:_verifycode block:^(ServiceResponse *response) {
if (response.isSuccess) {
completion(YES,response.code,@"登陆成功");
}
else {
completion(NO,response.code,response.message);
}
}];
}
- (void)registerOpenAccountCompletion:(ModuleSingletonCompletion)completion {
//检查发送数据
NSString *checkResult = [self checkRegisterOpenAccount];
if (![checkResult isEqualToString:CheckPassed]) {
completion(NO,CheckErrorCode,checkResult);
return;
}
//注册的网络层
[NetworkModuleService openAccount:_account loginPwd:_loginPassword payPwd:_payPassword idNo:_IDCardNo securityQuestion:_secQuestion securityAnswer:_secAnswer block:^(ServiceResponse *response) {
if (response.isSuccess) {
completion(YES,response.code,@"注册成功");
}
else {
completion(NO,response.code,response.message);
}
}];
}
#pragma mark - CheckPool //检查池
- (NSString *)checkAccount {
if ([_account isEqualToString:@""]) {
return @"请输入手机号";
}
return CheckPassed;
}//检查账号
- (NSString *)checkVerifyCode {
return CheckPassed;
}
- (NSString *)checkLoginPassword {
return CheckPassed;
}
- (NSString *)checkPayPassword {
return CheckPassed;
}
- (NSString *)checkSecQuestion {
return CheckPassed;
}
- (NSString *)checkSecAnswer {
return CheckPassed;
}
- (NSString *)checkIDCardNo {
return CheckPassed;
}
- (NSString *)checkLoginWithPassword {
NSString *checkResult;
checkResult = [self checkAccount];
if (![checkResult isEqualToString:CheckPassed]) {
return checkResult;
}
checkResult = [self checkLoginPassword];
if (![checkResult isEqualToString:CheckPassed]) {
return checkResult;
}
return CheckPassed;
}
- (NSString *)checkRegisterOpenAccount {
NSString *checkResult;
checkResult = [self checkAccount];
if (![checkResult isEqualToString:CheckPassed]) {
return checkResult;
}
checkResult = [self checkLoginPassword];
if (![checkResult isEqualToString:CheckPassed]) {
return checkResult;
}
checkResult = [self checkPayPassword];
if (![checkResult isEqualToString:CheckPassed]) {
return checkResult;
}
checkResult = [self checkSecQuestion];
if (![checkResult isEqualToString:CheckPassed]) {
return checkResult;
}
checkResult = [self checkSecAnswer];
if (![checkResult isEqualToString:CheckPassed]) {
return checkResult;
}
checkResult = [self checkIDCardNo];
if (![checkResult isEqualToString:CheckPassed]) {
return checkResult;
}
return CheckPassed;
}
@end
设计隐患
通常来讲单例都是伴随着整个App的生命周期,不推荐释放的,因为会造成数据的不安全,所以使用这个模式存在一种隐患,虽然在举例的代码中并没有发生.
假设存在一种异步情况用来获取某个账户的密保问题
NSString *question = @"";
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//某个函数通过单例函数获取密保问题
question = [self getSecurityQuestionByAccount:[ModuleSingleton shareInstance].account];//第一个Module
dispatch_async(dispatch_get_main_queue(), ^{
//然后放回单例
[[ModuleSingleton shareInstance] setSecQuestion:question];//被释放后重新生成的Module
});
});
如果在执行dispatch_async(dispatch_get_main_queue(), ^{})之前,ModuleSingleton被释放了,那么函数内部的setSecQuestion将会把question赋给一个全新的ModuleSingleton,而不是第一个ModuleSingleton.
也就是说不恰当的使用,会造成数据的混乱,这个问题是文章单例模式的滥用让我发现的.