APP重構(gòu)之路 網(wǎng)絡(luò)請(qǐng)求框架
APP重構(gòu)之路 Model的設(shè)計(jì)
前言
在現(xiàn)在的app,網(wǎng)絡(luò)請(qǐng)求是一個(gè)很重要的部分,app中很多部分都有或多或少的網(wǎng)絡(luò)請(qǐng)求,所以在一個(gè)項(xiàng)目重構(gòu)時(shí),我會(huì)選擇網(wǎng)絡(luò)請(qǐng)求框架作為我重構(gòu)的起點(diǎn)。在這篇文章中我所提出的架構(gòu),并不是所謂的 最好 的網(wǎng)絡(luò)請(qǐng)求架構(gòu),因?yàn)槲抑换?strong>我這個(gè)app原有架構(gòu)進(jìn)行改善,更多的情況下我是以app為出發(fā)點(diǎn),讓這個(gè)網(wǎng)絡(luò)架構(gòu)能夠在原app的環(huán)境下給我一個(gè)完美的結(jié)果,當(dāng)然如果有更好的改進(jìn)意見,我會(huì)很樂于嘗試。
關(guān)于網(wǎng)絡(luò)請(qǐng)求框架
一個(gè)好的網(wǎng)絡(luò)請(qǐng)求框架對(duì)于一個(gè)團(tuán)隊(duì)來說是十分重要的。如果一個(gè)網(wǎng)絡(luò)請(qǐng)求框架沒有封裝好,或者是在設(shè)計(jì)上存在問題,那么在開發(fā)上會(huì)造成許多問題,就拿這段代碼作為例子:
[leaveAPI startWithCompletionBlockWith:^(BaseRequest *baseRequest, id responseObject) {
//check the response object
BOOL isSuccess = [leaveAPI validResponseObject:responseObject];
if (isSuccess) {
//do something...
}
} failure:^(BaseRequest *baseRequest) {
//do something...
}];
上面這段代碼存在著不少的問題,比如把請(qǐng)求數(shù)據(jù)的判斷放到了每一個(gè)請(qǐng)求中、在leaveAPI的塊方法中再次調(diào)用leaveAPI、塊參數(shù)中的baseRequest并沒有實(shí)質(zhì)作用等等……針對(duì)這些問題我會(huì)一一進(jìn)行修正。
不要讓其他人做請(qǐng)求數(shù)據(jù)有效與否的判斷
在上面的代碼中,對(duì)resposeObject
是否有效的判斷被設(shè)計(jì)成了BaseRequest
類中的一個(gè)方法,程序員需要在調(diào)用網(wǎng)絡(luò)請(qǐng)求后,再調(diào)用該方法對(duì)responseObject
進(jìn)行判斷,這樣的設(shè)計(jì)存在很大的弊端。
在實(shí)際應(yīng)用中,很多時(shí)候程序員在調(diào)用網(wǎng)絡(luò)請(qǐng)求后往往會(huì)忘記調(diào)用該方法對(duì)返回結(jié)果進(jìn)行判斷,甚至忘記了存在這個(gè)方法,自行對(duì)responseObject
進(jìn)行判斷。首先這造成了大規(guī)模的代碼重復(fù),另一方面,不同程序員自己編寫的判斷方法散落在各個(gè)請(qǐng)求中,假如app在日后更新過程中改變了這個(gè)判斷標(biāo)準(zhǔn),會(huì)給修改帶來很大困難。
注意在塊方法中的循環(huán)調(diào)用
上面的代碼中,在leaveAPI
的塊方法中,再次調(diào)用了leaveAPI
中的方法,這樣導(dǎo)致了“retain cycle“,實(shí)際上正確的調(diào)用方法應(yīng)該是:
[leaveAPI startWithCompletionBlockWith:^(LeaveAPI *api, id responseObject) {
//check the response object
BOOL isSuccess = [api validResponseObject:responseObject];
if (isSuccess) {
//do something...
}
}];
為什么會(huì)出現(xiàn)這樣的情況,首先主要是因?yàn)檎麄€(gè)請(qǐng)求框架的注釋不清晰,導(dǎo)致其他程序員對(duì)方法的理解存在偏差,進(jìn)而天馬行空,發(fā)揮自己的想象力來調(diào)用方法。另外由于各個(gè)API與BaseRequest
的設(shè)計(jì)上存在問題,導(dǎo)致整個(gè)網(wǎng)絡(luò)請(qǐng)求框架的混亂。
不要在單獨(dú)的API中實(shí)現(xiàn)上傳下載操作
在舊的網(wǎng)絡(luò)請(qǐng)求框架中,BaseRequest
一開始的設(shè)計(jì)中并沒有針對(duì)上傳和下載操作進(jìn)行處理,而且整個(gè)BaseRequest
的設(shè)計(jì)中并沒有AOP,這個(gè)導(dǎo)致了在日后需要增加上傳和下載功能的時(shí)候只能將他們寫到單獨(dú)的API中,這個(gè)導(dǎo)致了代碼重復(fù),代碼的復(fù)用性降低,如:
//
// FileAPI.m
//
...some methods...
#pragma mark - Upload & Download
-(void)uploadFile:(FileUploadCompleteBlock)uploadBlock errorBlock:(FileUploadFailBlock)errorBlock {
NSString *url = self.url
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.requestSerializer = [AFHTTPRequestSerializer serializer];
manager.operationQueue.maxConcurrentOperationCount = 5;
manager.requestSerializer.timeoutInterval = 30;
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/html",@"text/json",@"text/javascript",@"text/plain",nil];
[manager POST:url parameters:[self requestArgument] constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
// upload operation ...
}success:^(AFHTTPRequestOperation *operation, id responseObject) {
// do something ...
}failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// do something ...
}];
}
在FileAPI.m
中,上傳操作是這樣實(shí)現(xiàn)的。寫下這段代碼的時(shí)候是使用AFNetworking 2.0
,而現(xiàn)在使用的是AFNetworking 3.0
,AFHTTPRequestOperationManager
也變成了AFHTTPSessionManger
,這個(gè)時(shí)候散落在各個(gè)API的上傳方法修改起來就變的很麻煩。
BaseRequest中的設(shè)計(jì)缺陷
在上文中一直在指出各個(gè)API中的缺陷,而也提到很多地方是歸咎于BaseReuqest
的問題,現(xiàn)在就來看一下它里面的一些缺陷:
首先在整個(gè)BaseRequest
中,它包括了地址的組裝、網(wǎng)絡(luò)環(huán)境的判斷、請(qǐng)求的發(fā)送等等,基本網(wǎng)絡(luò)請(qǐng)求的所有操作都是由這一個(gè)類來實(shí)現(xiàn)。這樣就導(dǎo)致了整個(gè)類十分龐大,在需要添加新的請(qǐng)求類型如我上文提到的上傳與下載時(shí),會(huì)難以下手,這就導(dǎo)致了我上文提到的種種問題。
另一方面BaseRequest
中沒有針對(duì)返回?cái)?shù)據(jù)的處理,這里的處理是指返回?cái)?shù)據(jù)的緩存操作、數(shù)據(jù)過濾操作、請(qǐng)求數(shù)據(jù)為空的處理操作等等,如果這些問題都交給方法調(diào)用者來完成的話,會(huì)導(dǎo)致某一模塊的代碼量暴漲(在本app是VC),而且很多時(shí)候數(shù)據(jù)需要的只是一個(gè)默認(rèn)的緩存操作、默認(rèn)的過濾操作,這個(gè)時(shí)候重復(fù)性的代碼會(huì)很多,倒不如把這些操作統(tǒng)一處理好,假如有特殊的API需要進(jìn)行特殊的配置,再由該API對(duì)這些配置進(jìn)行修改,而不需要把這些默認(rèn)操作交由其他程序員來完成。
我是如何設(shè)計(jì)新的網(wǎng)絡(luò)請(qǐng)求框架
上文提到了各種各樣的不足,所以是時(shí)候針對(duì)這些不足進(jìn)行改進(jìn)了。
先看大局,再看細(xì)節(jié)。首先是整個(gè)架構(gòu)的數(shù)據(jù)流向:
整個(gè)網(wǎng)絡(luò)請(qǐng)求框架中最重要的是其中的NetworkManage
,它主要是負(fù)責(zé)整個(gè)請(qǐng)求的處理。
設(shè)計(jì)中的一些關(guān)注重點(diǎn)
首先檢測(cè)網(wǎng)絡(luò)狀態(tài)
當(dāng)一個(gè)請(qǐng)求發(fā)起的時(shí)候,首先它會(huì)檢測(cè)網(wǎng)絡(luò)是否聯(lián)通,假如沒有聯(lián)通的時(shí)候會(huì)直接彈出一個(gè)窗口提醒用戶需要先連接網(wǎng)絡(luò),而不會(huì)進(jìn)行下一步的請(qǐng)求。而在舊的網(wǎng)絡(luò)請(qǐng)求框架中,很多時(shí)候把這段代碼放到了vc,現(xiàn)在將它整合進(jìn)來。
- (void)addRequest:(BaseRequest*)request {
//TODO: 檢查網(wǎng)絡(luò)是否通暢
if(![self checkNetworkConnection])
{
[self showNetworkAlertForRequest:request];
return;
}
[self checkNetworkConnection]:
- (BOOL)checkNetworkConnection
{
struct sockaddr zeroAddress;
bzero(&zeroAddress, sizeof(zeroAddress));
zeroAddress.sa_len = sizeof(zeroAddress);
zeroAddress.sa_family = AF_INET;
SCNetworkReachabilityRef defaultRouteReachability = SCNetworkReachabilityCreateWithAddress(NULL, (struct sockaddr *)&zeroAddress);
SCNetworkReachabilityFlags flags;
BOOL didRetrieveFlags = SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags);
CFRelease(defaultRouteReachability);
if (!didRetrieveFlags) {
printf("Error. Count not recover network reachability flags\n");
return NO;
}
BOOL isReachable = flags & kSCNetworkFlagsReachable;
BOOL needsConnection = flags & kSCNetworkFlagsConnectionRequired;
return (isReachable && !needsConnection) ? YES : NO;
}
活性組裝請(qǐng)求地址
而在進(jìn)行完網(wǎng)絡(luò)聯(lián)通的判斷之后,就會(huì)對(duì)請(qǐng)求的地址進(jìn)行組裝。組裝地址的方法并沒有太大的變化,但是在舊的請(qǐng)求框架開發(fā)的時(shí)候,我注意到一個(gè)問題:在增加新需求增加新的接口的時(shí)候,往往需要連接到測(cè)試服務(wù)器上進(jìn)行調(diào)試,這時(shí)候就需要將請(qǐng)求的地址改成測(cè)試服務(wù)器的地址。但這往往引發(fā)一些問題,因?yàn)闇y(cè)試服務(wù)器上可能沒有正式服務(wù)器的一些數(shù)據(jù),在測(cè)試時(shí)往往沒有問題,但是轉(zhuǎn)移到正式服務(wù)器上就出現(xiàn)了各種問題,所以我就想能不能改成程序員可以改變API連接的地址,而不改變?nèi)值恼?qǐng)求框架,讓各個(gè)API在請(qǐng)求的時(shí)候判斷自己是否需要連接到測(cè)試服務(wù)器。
- (NSString *)urlString{
NSString *url = nil;
//TODO: 使用副地址
if ([self.child respondsToSelector:@selector(useViceUrl)] && [self.child useViceUrl]){
baseUrl = self.config.viceBaseUrl;
}
//TODO: 使用主地址
else{
baseUrl = self.config.mainBaseUrl;
}
}
讓API能夠獨(dú)立配置
組裝地址完畢之后,就開始根據(jù)API自身的設(shè)置來進(jìn)行配置,在舊的請(qǐng)求框架中,API的是直接繼承自BaseRequest
這個(gè)類,導(dǎo)致了BaseRequest
需要完成大量的工作,或是存有大量空方法,可讀性與穩(wěn)定性都很差,很多東西也沒有辦法讓API自己進(jìn)行獨(dú)立設(shè)置。在新的框架中,我選擇將API的設(shè)置通過一個(gè)叫做APIProtocol
的協(xié)議來完成,API需要配置的內(nèi)容可以通過實(shí)現(xiàn)該協(xié)議的方法來進(jìn)行配置,否則就會(huì)直接使用默認(rèn)配置
//TODO: 檢查是否使用自定義超時(shí)時(shí)間
if ([request respondsToSelector:@selector(requestTimeoutInterval)]) {
self.manager.requestSerializer.timeoutInterval = [request requestTimeoutInterval];
}
else{
self.manager.requestSerializer.timeoutInterval = 60.0;
}
more methods ...
完善返回?cái)?shù)據(jù)的基礎(chǔ)判斷
最后在進(jìn)行完請(qǐng)求判斷后,將會(huì)對(duì)responseObject
的有效性進(jìn)行判斷。關(guān)于數(shù)據(jù)的判斷我一開始是打算放在BaseRequest
中的,因?yàn)橐婚_始的想法是希望能夠在BaseRequest
中做一個(gè)默認(rèn)的判斷,假如API自身需要再度對(duì)responseObject
進(jìn)行進(jìn)一步的判斷時(shí),可以通過協(xié)議方法來重新編寫該API獨(dú)立的判定方法。但這種方法最終被我棄用了,首先responseObject
的基礎(chǔ)判斷在我看來是不應(yīng)該放在BaseRequest
中的,因?yàn)?code>BaseRequest是作為一個(gè)請(qǐng)求的"中心",不應(yīng)該把數(shù)據(jù)處理的問題交給它處理。另一方面是因?yàn)槲覀冃枰O(shè)計(jì)的是基礎(chǔ)判斷,它和各個(gè)API獨(dú)立的判斷方式不是平行關(guān)系,而是層次關(guān)系,因?yàn)樵谠O(shè)計(jì)的是每一個(gè)API都需要進(jìn)行的判斷,假如在整個(gè)app中有很多API需要進(jìn)行獨(dú)立判斷,就意味著需要編寫很多次基礎(chǔ)判斷邏輯,同時(shí)假如在日后需要修改這個(gè)基礎(chǔ)判斷內(nèi)容,代碼也散落在各個(gè)地方,這不是我們想要的結(jié)果。
所以在設(shè)計(jì)上我最終把這個(gè)判斷方法放到了NetworkConfig
中,新增了一個(gè)BaseFilter
類,專門用于返回?cái)?shù)據(jù)的判斷,假如我的API需要增加獨(dú)特的判斷方法時(shí),可以直接在請(qǐng)求方法中直接對(duì)responseObject
進(jìn)行進(jìn)一步判斷。
NetworkConfig.m:
//NetworkManage.m
if([self.networkConfig.baseFilter validResponseObject:responseObject])
{
request.responseObject = responseObject;
[self handleSuccessRequest:task];
}
else
{
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorBadServerResponse userInfo:nil];
request.responseObject = responseObject;
[self handleFailureRequest:task error:error];
}
BaseFilter.m
@implementation BaseFilter
- (BOOL)validResponseObject:(id)responseObject
{
//TODO: 檢查是否返回了數(shù)據(jù)且數(shù)據(jù)是否正確
if (!responseObject && ![responseObject isKindOfClass:[NSDictionary class]] && ![responseObject[@"success"] boolValue]) {
return NO;
}
else
return YES;
}
@end
結(jié)語
我相信在軟件設(shè)計(jì)中并不存在最好或者是最正確的架構(gòu),因?yàn)檫@是一個(gè)很抽象的工作,但我相信我們應(yīng)該可以設(shè)計(jì)出一個(gè)擴(kuò)展性良好和簡(jiǎn)單明了的架構(gòu),能夠讓新加入的程序員快速上手,能夠適應(yīng)軟件接下來的開發(fā)需要,那這大概是一個(gè)好的架構(gòu)。
想了解更多內(nèi)容可以查看我的主頁