APP重構(gòu)之路 網(wǎng)絡(luò)請求框架
APP重構(gòu)之路 Model的設(shè)計
前言
在現(xiàn)在的app大审,網(wǎng)絡(luò)請求是一個很重要的部分铃诬,app中很多部分都有或多或少的網(wǎng)絡(luò)請求祭陷,所以在一個項目重構(gòu)時,我會選擇網(wǎng)絡(luò)請求框架作為我重構(gòu)的起點趣席。在這篇文章中我所提出的架構(gòu)兵志,并不是所謂的 最好 的網(wǎng)絡(luò)請求架構(gòu),因為我只基于我這個app原有架構(gòu)進行改善宣肚,更多的情況下我是以app為出發(fā)點想罕,讓這個網(wǎng)絡(luò)架構(gòu)能夠在原app的環(huán)境下給我一個完美的結(jié)果,當(dāng)然如果有更好的改進意見霉涨,我會很樂于嘗試按价。
關(guān)于網(wǎng)絡(luò)請求框架
一個好的網(wǎng)絡(luò)請求框架對于一個團隊來說是十分重要的。如果一個網(wǎng)絡(luò)請求框架沒有封裝好笙瑟,或者是在設(shè)計上存在問題楼镐,那么在開發(fā)上會造成許多問題,就拿這段代碼作為例子:
[leaveAPI startWithCompletionBlockWith:^(BaseRequest *baseRequest, id responseObject) {
//check the response object
BOOL isSuccess = [leaveAPI validResponseObject:responseObject];
if (isSuccess) {
//do something...
}
} failure:^(BaseRequest *baseRequest) {
//do something...
}];
上面這段代碼存在著不少的問題往枷,比如把請求數(shù)據(jù)的判斷放到了每一個請求中框产、在leaveAPI的塊方法中再次調(diào)用leaveAPI、塊參數(shù)中的baseRequest并沒有實質(zhì)作用等等……針對這些問題我會一一進行修正错洁。
不要讓其他人做請求數(shù)據(jù)有效與否的判斷
在上面的代碼中茅信,對resposeObject
是否有效的判斷被設(shè)計成了BaseRequest
類中的一個方法,程序員需要在調(diào)用網(wǎng)絡(luò)請求后墓臭,再調(diào)用該方法對responseObject
進行判斷蘸鲸,這樣的設(shè)計存在很大的弊端。
在實際應(yīng)用中窿锉,很多時候程序員在調(diào)用網(wǎng)絡(luò)請求后往往會忘記調(diào)用該方法對返回結(jié)果進行判斷酌摇,甚至忘記了存在這個方法膝舅,自行對responseObject
進行判斷。首先這造成了大規(guī)模的代碼重復(fù)窑多,另一方面仍稀,不同程序員自己編寫的判斷方法散落在各個請求中,假如app在日后更新過程中改變了這個判斷標(biāo)準埂息,會給修改帶來很大困難技潘。
注意在塊方法中的循環(huán)調(diào)用
上面的代碼中,在leaveAPI
的塊方法中千康,再次調(diào)用了leaveAPI
中的方法享幽,這樣導(dǎo)致了“retain cycle“,實際上正確的調(diào)用方法應(yīng)該是:
[leaveAPI startWithCompletionBlockWith:^(LeaveAPI *api, id responseObject) {
//check the response object
BOOL isSuccess = [api validResponseObject:responseObject];
if (isSuccess) {
//do something...
}
}];
為什么會出現(xiàn)這樣的情況拾弃,首先主要是因為整個請求框架的注釋不清晰值桩,導(dǎo)致其他程序員對方法的理解存在偏差,進而天馬行空豪椿,發(fā)揮自己的想象力來調(diào)用方法奔坟。另外由于各個API與BaseRequest
的設(shè)計上存在問題,導(dǎo)致整個網(wǎng)絡(luò)請求框架的混亂搭盾。
不要在單獨的API中實現(xiàn)上傳下載操作
在舊的網(wǎng)絡(luò)請求框架中咳秉,BaseRequest
一開始的設(shè)計中并沒有針對上傳和下載操作進行處理,而且整個BaseRequest
的設(shè)計中并沒有AOP鸯隅,這個導(dǎo)致了在日后需要增加上傳和下載功能的時候只能將他們寫到單獨的API中澜建,這個導(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
中霎奢,上傳操作是這樣實現(xiàn)的户誓。寫下這段代碼的時候是使用AFNetworking 2.0
饼灿,而現(xiàn)在使用的是AFNetworking 3.0
,AFHTTPRequestOperationManager
也變成了AFHTTPSessionManger
帝美,這個時候散落在各個API的上傳方法修改起來就變的很麻煩碍彭。
BaseRequest中的設(shè)計缺陷
在上文中一直在指出各個API中的缺陷,而也提到很多地方是歸咎于BaseReuqest
的問題悼潭,現(xiàn)在就來看一下它里面的一些缺陷:
首先在整個BaseRequest
中庇忌,它包括了地址的組裝、網(wǎng)絡(luò)環(huán)境的判斷舰褪、請求的發(fā)送等等皆疹,基本網(wǎng)絡(luò)請求的所有操作都是由這一個類來實現(xiàn)。這樣就導(dǎo)致了整個類十分龐大占拍,在需要添加新的請求類型如我上文提到的上傳與下載時略就,會難以下手捎迫,這就導(dǎo)致了我上文提到的種種問題。
另一方面BaseRequest
中沒有針對返回數(shù)據(jù)的處理表牢,這里的處理是指返回數(shù)據(jù)的緩存操作窄绒、數(shù)據(jù)過濾操作、請求數(shù)據(jù)為空的處理操作等等崔兴,如果這些問題都交給方法調(diào)用者來完成的話彰导,會導(dǎo)致某一模塊的代碼量暴漲(在本app是VC),而且很多時候數(shù)據(jù)需要的只是一個默認的緩存操作敲茄、默認的過濾操作位谋,這個時候重復(fù)性的代碼會很多,倒不如把這些操作統(tǒng)一處理好折汞,假如有特殊的API需要進行特殊的配置倔幼,再由該API對這些配置進行修改,而不需要把這些默認操作交由其他程序員來完成爽待。
我是如何設(shè)計新的網(wǎng)絡(luò)請求框架
上文提到了各種各樣的不足损同,所以是時候針對這些不足進行改進了。
先看大局鸟款,再看細節(jié)膏燃。首先是整個架構(gòu)的數(shù)據(jù)流向:
整個網(wǎng)絡(luò)請求框架中最重要的是其中的NetworkManage
,它主要是負責(zé)整個請求的處理何什。
設(shè)計中的一些關(guān)注重點
首先檢測網(wǎng)絡(luò)狀態(tài)
當(dāng)一個請求發(fā)起的時候组哩,首先它會檢測網(wǎng)絡(luò)是否聯(lián)通,假如沒有聯(lián)通的時候會直接彈出一個窗口提醒用戶需要先連接網(wǎng)絡(luò)处渣,而不會進行下一步的請求伶贰。而在舊的網(wǎng)絡(luò)請求框架中,很多時候把這段代碼放到了vc罐栈,現(xià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;
}
活性組裝請求地址
而在進行完網(wǎng)絡(luò)聯(lián)通的判斷之后,就會對請求的地址進行組裝荠诬。組裝地址的方法并沒有太大的變化琅翻,但是在舊的請求框架開發(fā)的時候,我注意到一個問題:在增加新需求增加新的接口的時候柑贞,往往需要連接到測試服務(wù)器上進行調(diào)試方椎,這時候就需要將請求的地址改成測試服務(wù)器的地址。但這往往引發(fā)一些問題钧嘶,因為測試服務(wù)器上可能沒有正式服務(wù)器的一些數(shù)據(jù)棠众,在測試時往往沒有問題,但是轉(zhuǎn)移到正式服務(wù)器上就出現(xiàn)了各種問題有决,所以我就想能不能改成程序員可以改變API連接的地址闸拿,而不改變?nèi)值恼埱罂蚣芙瘟粒尭鱾€API在請求的時候判斷自己是否需要連接到測試服務(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能夠獨立配置
組裝地址完畢之后胸墙,就開始根據(jù)API自身的設(shè)置來進行配置我注,在舊的請求框架中,API的是直接繼承自BaseRequest
這個類迟隅,導(dǎo)致了BaseRequest
需要完成大量的工作但骨,或是存有大量空方法,可讀性與穩(wěn)定性都很差智袭,很多東西也沒有辦法讓API自己進行獨立設(shè)置奔缠。在新的框架中,我選擇將API的設(shè)置通過一個叫做APIProtocol
的協(xié)議來完成吼野,API需要配置的內(nèi)容可以通過實現(xiàn)該協(xié)議的方法來進行配置校哎,否則就會直接使用默認配置
//TODO: 檢查是否使用自定義超時時間
if ([request respondsToSelector:@selector(requestTimeoutInterval)]) {
self.manager.requestSerializer.timeoutInterval = [request requestTimeoutInterval];
}
else{
self.manager.requestSerializer.timeoutInterval = 60.0;
}
more methods ...
完善返回數(shù)據(jù)的基礎(chǔ)判斷
最后在進行完請求判斷后,將會對responseObject
的有效性進行判斷瞳步。關(guān)于數(shù)據(jù)的判斷我一開始是打算放在BaseRequest
中的闷哆,因為一開始的想法是希望能夠在BaseRequest
中做一個默認的判斷,假如API自身需要再度對responseObject
進行進一步的判斷時单起,可以通過協(xié)議方法來重新編寫該API獨立的判定方法抱怔。但這種方法最終被我棄用了,首先responseObject
的基礎(chǔ)判斷在我看來是不應(yīng)該放在BaseRequest
中的嘀倒,因為BaseRequest
是作為一個請求的"中心"屈留,不應(yīng)該把數(shù)據(jù)處理的問題交給它處理。另一方面是因為我們需要設(shè)計的是基礎(chǔ)判斷测蘑,它和各個API獨立的判斷方式不是平行關(guān)系灌危,而是層次關(guān)系,因為在設(shè)計的是每一個API都需要進行的判斷碳胳,假如在整個app中有很多API需要進行獨立判斷勇蝙,就意味著需要編寫很多次基礎(chǔ)判斷邏輯,同時假如在日后需要修改這個基礎(chǔ)判斷內(nèi)容固逗,代碼也散落在各個地方浅蚪,這不是我們想要的結(jié)果藕帜。
所以在設(shè)計上我最終把這個判斷方法放到了NetworkConfig
中烫罩,新增了一個BaseFilter
類,專門用于返回數(shù)據(jù)的判斷洽故,假如我的API需要增加獨特的判斷方法時贝攒,可以直接在請求方法中直接對responseObject
進行進一步判斷。
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è)計中并不存在最好或者是最正確的架構(gòu)时甚,因為這是一個很抽象的工作隘弊,但我相信我們應(yīng)該可以設(shè)計出一個擴展性良好和簡單明了的架構(gòu)哈踱,能夠讓新加入的程序員快速上手,能夠適應(yīng)軟件接下來的開發(fā)需要梨熙,那這大概是一個好的架構(gòu)开镣。
想了解更多內(nèi)容可以查看我的主頁