2020,最新APP重构:网络请求框架

半腔热情 提交于 2020-08-16 05:20:10

在现在的app,网络请求是一个很重要的部分,app中很多部分都有或多或少的网络请求,所以在一个项目重构时,我会选择网络请求框架作为我重构的起点。在这篇文章中我所提出的架构,并不是所谓的 最好 的网络请求架构,因为我只基于我这个app原有架构进行改善,更多的情况下我是以app为出发点,让这个网络架构能够在原app的环境下给我一个完美的结果,当然如果有更好的改进意见,我会很乐于尝试。

关于网络请求框架

一个好的网络请求框架对于一个团队来说是十分重要的。如果一个网络请求框架没有封装好,或者是在设计上存在问题,那么在开发上会造成许多问题,就拿这段代码作为例子:

[leaveAPI startWithCompletionBlockWith:^(BaseRequest *baseRequest, id responseObject) {
    //check the response object
    BOOL isSuccess = [leaveAPI validResponseObject:responseObject];
    if (isSuccess) {
        //do something...
    }   
}failure:^(BaseRequest *baseRequest) {
    //do something...
}];

上面这段代码存在着不少的问题,比如把请求数据的判断放到了每一个请求中、在leaveAPI的块方法中再次调用leaveAPI、块参数中的baseRequest并没有实质作用等等……针对这些问题我会一一进行修正。

不要让其他人做请求数据有效与否的判断

在上面的代码中,对resposeObject是否有效的判断被设计成了BaseRequest类中的一个方法,程序员需要在调用网络请求后,再调用该方法对responseObject进行判断,这样的设计存在很大的弊端。

在实际应用中,很多时候程序员在调用网络请求后往往会忘记调用该方法对返回结果进行判断,甚至忘记了存在这个方法,自行对responseObject进行判断。首先这造成了大规模的代码重复,另一方面,不同程序员自己编写的判断方法散落在各个请求中,假如app在日后更新过程中改变了这个判断标准,会给修改带来很大困难。

注意在块方法中的循环调用

上面的代码中,在leaveAPI的块方法中,再次调用了leaveAPI中的方法,这样导致了"retain cycle",实际上正确的调用方法应该是:

[leaveAPI startWithCompletionBlockWith:^(LeaveAPI *api, id responseObject) {
    //check the response object
    BOOL isSuccess = [api validResponseObject:responseObject];
    if (isSuccess) {
        //do something...
    }
}];

为什么会出现这样的情况,首先主要是因为整个请求框架的注释不清晰,导致其他程序员对方法的理解存在偏差,进而天马行空,发挥自己的想象力来调用方法。另外由于各个API与BaseRequest的设计上存在问题,导致整个网络请求框架的混乱。

不要在单独的API中实现上传下载操作

在旧的网络请求框架中,BaseRequest一开始的设计中并没有针对上传和下载操作进行处理,而且整个BaseRequest的设计中并没有AOP,这个导致了在日后需要增加上传和下载功能的时候只能将他们写到单独的API中,这个导致了代码重复,代码的复用性降低,如:

//
//  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中,上传操作是这样实现的。写下这段代码的时候是使用AFNetworking 2.0,而现在使用的是AFNetworking 3.0AFHTTPRequestOperationManager也变成了AFHTTPSessionManger,这个时候散落在各个API的上传方法修改起来就变的很麻烦。

BaseRequest中的设计缺陷

在上文中一直在指出各个API中的缺陷,而也提到很多地方是归咎于BaseReuqest的问题,现在就来看一下它里面的一些缺陷:

首先在整个BaseRequest中,它包括了地址的组装、网络环境的判断、请求的发送等等,基本网络请求的所有操作都是由这一个类来实现。这样就导致了整个类十分庞大,在需要添加新的请求类型如我上文提到的上传与下载时,会难以下手,这就导致了我上文提到的种种问题。

另一方面BaseRequest中没有针对返回数据的处理,这里的处理是指返回数据的缓存操作、数据过滤操作、请求数据为空的处理操作等等,如果这些问题都交给方法调用者来完成的话,会导致某一模块的代码量暴涨(在本app是VC),而且很多时候数据需要的只是一个默认的缓存操作、默认的过滤操作,这个时候重复性的代码会很多,倒不如把这些操作统一处理好,假如有特殊的API需要进行特殊的配置,再由该API对这些配置进行修改,而不需要把这些默认操作交由其他程序员来完成。

我是如何设计新的网络请求框架

上文提到了各种各样的不足,所以是时候针对这些不足进行改进了。

先看大局,再看细节。首先是整个架构的数据流向:


整个网络请求框架中最重要的是其中的NetworkManage,它主要是负责整个请求的处理。

设计中的一些关注重点

首先检测网络状态

当一个请求发起的时候,首先它会检测网络是否联通,假如没有联通的时候会直接弹出一个窗口提醒用户需要先连接网络,而不会进行下一步的请求。而在旧的网络请求框架中,很多时候把这段代码放到了vc,现在将它整合进来。

- (void)addRequest:(BaseRequest*)request {

    //TODO: 检查网络是否通畅
    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;
}

活性组装请求地址

而在进行完网络联通的判断之后,就会对请求的地址进行组装。组装地址的方法并没有太大的变化,但是在旧的请求框架开发的时候,我注意到一个问题:在增加新需求增加新的接口的时候,往往需要连接到测试服务器上进行调试,这时候就需要将请求的地址改成测试服务器的地址。但这往往引发一些问题,因为测试服务器上可能没有正式服务器的一些数据,在测试时往往没有问题,但是转移到正式服务器上就出现了各种问题,所以我就想能不能改成程序员可以改变API连接的地址,而不改变全局的请求框架,让各个API在请求的时候判断自己是否需要连接到测试服务器。

- (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能够独立配置

组装地址完毕之后,就开始根据API自身的设置来进行配置,在旧的请求框架中,API的是直接继承自BaseRequest这个类,导致了BaseRequest需要完成大量的工作,或是存有大量空方法,可读性与稳定性都很差,很多东西也没有办法让API自己进行独立设置。在新的框架中,我选择将API的设置通过一个叫做APIProtocol的协议来完成,API需要配置的内容可以通过实现该协议的方法来进行配置,否则就会直接使用默认配置

//TODO: 检查是否使用自定义超时时间
    if ([request respondsToSelector:@selector(requestTimeoutInterval)]) {
        self.manager.requestSerializer.timeoutInterval = [request requestTimeoutInterval];
    }
    else{
        self.manager.requestSerializer.timeoutInterval = 60.0;
    }

    more methods ...

完善返回数据的基础判断

最后在进行完请求判断后,将会对responseObject的有效性进行判断。关于数据的判断我一开始是打算放在BaseRequest中的,因为一开始的想法是希望能够在BaseRequest中做一个默认的判断,假如API自身需要再度对responseObject进行进一步的判断时,可以通过协议方法来重新编写该API独立的判定方法。但这种方法最终被我弃用了,首先responseObject的基础判断在我看来是不应该放在BaseRequest中的,因为BaseRequest是作为一个请求的”中心”,不应该把数据处理的问题交给它处理。另一方面是因为我们需要设计的是基础判断,它和各个API独立的判断方式不是平行关系,而是层次关系,因为在设计的是每一个API都需要进行的判断,假如在整个app中有很多API需要进行独立判断,就意味着需要编写很多次基础判断逻辑,同时假如在日后需要修改这个基础判断内容,代码也散落在各个地方,这不是我们想要的结果。

所以在设计上我最终把这个判断方法放到了NetworkConfig中,新增了一个BaseFilter类,专门用于返回数据的判断,假如我的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: 检查是否返回了数据且数据是否正确
    if (!responseObject && ![responseObject isKindOfClass:[NSDictionary class]] && ![responseObject[@"success"] boolValue]) {
        return NO;
    }
    else
        return YES;
}

@end

结语

我相信在软件设计中并不存在最好或者是最正确的架构,因为这是一个很抽象的工作,但我相信我们应该可以设计出一个扩展性良好和简单明了的架构,能够让新加入的程序员快速上手,能够适应软件接下来的开发需要,那这大概是一个好的架构。
以上内容就是本篇的全部内容以上内容希望对你有帮助,有被帮助到的朋友欢迎点赞,评论。
如果对软件测试、接口测试、自动化测试、面试经验交流。感兴趣可以关注我,我们会有同行一起技术交流哦。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!