APP重構(gòu)之路 網(wǎng)絡(luò)請(qǐng)求框架

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.0AFHTTPRequestOperationManager也變成了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ù)流向:

網(wǎng)絡(luò)請(qǐng)求架構(gòu)-數(shù)據(jù)流.jpg

整個(gè)網(wǎng)絡(luò)請(qǐng)求框架中最重要的是其中的NetworkManage,它主要是負(fù)責(zé)整個(gè)請(qǐng)求的處理。

網(wǎng)絡(luò)請(qǐng)求架構(gòu)-請(qǐng)求過程.jpg

設(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)容可以查看我的主頁

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容