美文网首页自个儿读IOS特技iOS开发
关于网络层的设计(一)——和业务层的对接

关于网络层的设计(一)——和业务层的对接

作者: Wang66 | 来源:发表于2015-12-27 23:19 被阅读2174次

    前言

    关于网络层的设计,最主要的是和业务层的对接问题。
    网络层设计得好,可以让业务层开发事半功倍;反之,若网络层设计地很糟糕,则会让业务层开发事倍功半,心里法克连连。


    p1.jpeg

    关于网络层和业务层的对接,我们一般从下面几个方面进行考量:

    • 选择哪种方式请求网络数据,系统自带的还是AFNetworking?
    • 以什么模式给业务层交付数据?delegate 还是block?
    • 交付给业务层什么形式的数据?直接返回dict就行了,还是把dict在网络层转换为ResultModel再交给业务层?
    • 封装API应该选择集约型还是离散型?

    第一个问题,“选择哪种方式请求网络数据?”。第三方库AFNetworking很强大而且使用起来比较简单,所以一般我们选择AFNetworking。苹果自带的NSURLSession等以后再研究。

    第二个问题,“以什么模式给业务层交付数据?”。一般选择Delegate和block。关于它们,应该说各有利弊吧,具体使用场景具体选择使用。block使用起来较方便,但也有调试时不好追踪,容易出现循环引用等坑的缺点。而且若在业务层block返回数据后,要做比较复杂的逻辑处理的话,那在block里会写有大段代码,这样阅读起来也不好,使代码整体结构显得很不清晰。
    但是,在此,我们仍先以block为例来理解网络层的设计。

    第三个问题,“交付给业务层什么形式的数据?”。我们设计网络层,就要想着能尽量减轻业务层的开发量,最好把网络层从后台拿到的一大串数据,剥离、加工、整理成业务层需要的数据格式然后再交付给它。

    第四个问题,“封装API应该选择集约型还是离散型?”。所谓集约型,就是只能业务层提供一个方法,所有业务层的网络请求都要通过该方法完成。因此,该方法至少要能传入接口路径(path)、请求方式(get/post)、请求参数(param)等。集约型的好处是对于网络层的编写来说方便快捷,但对业务层来说要传入这么多参数并不太好。我们设计的目的就是尽量使业务层使用起来简单轻巧,所以我们常常采用离散型方式。(说得不太恰当。集约型为所有的业务请求提供一个接口,省去了编写业务模块xxxManager的工作量。但对集约型而言,提供的这个唯一的网络请求方法得有接口地址,请求方式,接口参数等多个参数。这是其繁琐之处,而离散型则为了避免给业务层带来这样的繁琐,而在xxxManager提供的接口方法里自己配置了接口地址interface和请求方式,并以方法名加以体现。那对业务层开发来说就简洁明了了许多。一个比如用户模块UserManager里的对登录请求的封装,只需业务层传入accountpassword两个参数,而接口地址和请求方式已封装在其方法里了login:password:success:failure,而且方法名也体现出了请求接口login。但离散型的问题是无疑为增加代码量,为编写xxxManager层将花费大量时间。)
    不言而喻,和集约型相对的,离散型就是根据功能模块分为不同的模块,分别提供不同的方法给业务层调用。比如,把和用户有关的所有网络请求,放在一个叫UserManager的类中,登录、注册、修改密码等分别提供不同的方法,这样的好处在于,一、不同功能模块放在不同的文件中,使项目结构更清晰,维护升级更容易;二、对于业务开发人员来说,不同的功能叫不同的方法名这样更友好易懂。三、更重要的是,你可以在xxxManager这一层做一些针对该模块的个性化处理。没错,你可以在这一层完成上个问题中所说的数据加工后再交付给业务层。我们把和用户相关的网络请求API都定义在UserManager类中,并在其中转换为UserObject然后交付给业务层。
    除此外,“离散”不仅体现在提供的API方法上,还体现在网络请求连接上。我们定义一个HttpClient类,在该类中专门完成对服务器的网络请求。并且给xxxManager这一层提供不同请求方式对应的方法。

    好了,基本结构就是这样,下面上代码。我们“从内至外”的看代码。
    首先就是HttpClient这个类了,该类是完成网络请求连接的核心。并给xxxManager提供网络连接的接口方法。

    HttpClient.h

    #import <Foundation/Foundation.h>
    #import "AFNetworking.h"
    
    #define BaseURL @"http://192.168.1.125/v1/" // 服务器地址
    
    typedef NS_ENUM(NSInteger, RequestMethod)
    {
        POST = 0,
        GET,
        PUT,
        DELETE,
    };
    
    
    @interface HttpClient : NSObject
    
    // get请求
    - (void)getOfPath:(NSString *)path
              prama:(id)prama
            success:(void(^)(id result))success
            failure:(void(^)(NSError *error))failure;
    
    
    // post请求
    - (void)postOfPath:(NSString *)path
              prama:(id)prama
            success:(void(^)(id result))success
            failure:(void(^)(NSError *error))failure;
    
    @end
    

    HttpClient.m
    注意,我们提供给外部get和post请求对应的两个方法调用,但其实在内部,我们是定义了一个“全能方法”来完成网络连接的,这才是核心。

    #import "HttpClient.h"
    
    @implementation HttpClient
    
    // get
    - (void)getOfPath:(NSString *)path
                prama:(id)prama
              success:(void(^)(id result))success
              failure:(void(^)(NSError *error))failure
    {
        [self sendRequestOfType:GET path:path prama:prama success:success failure:failure];
    }
    
    // post
    - (void)postOfPath:(NSString *)path
                 prama:(id)prama
               success:(void(^)(id result))success
               failure:(void(^)(NSError *error))failure
    {
        [self sendRequestOfType:POST path:path prama:prama success:success failure:failure];
    }
    
    
    
    // 完成网络连接的核心
    - (void)sendRequestOfType:(RequestMethod)requestType
                         path:(NSString *)path
                        prama:(id)prama
                      success:(void(^)(id result))success
                      failure:(void(^)(NSError *error))failure
    {
        // 拼接参数,得到完整的接口地址
        NSURL *baseUrl = [NSURL URLWithString:BaseURL];
        NSString *pathUrl = [NSString stringWithFormat:@"%@%@",BaseURL,path];
        
        /**
         通常在此,可以完成拼接公共参数、密码加密、或者签名认证等操作。
         */
    
        AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseUrl];
        
        switch (requestType)
        {
                
    // ------------ GET -------------
            case GET:
            {
                [manager GET:pathUrl
                  parameters:prama
                     success:^(AFHTTPRequestOperation *operation, id responseObject) {
                            success(responseObject);
                } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                            failure(error);
                }];
                break;
            }
                
    // ------------ POST -------------
                
            case POST:
            {
                [manager POST:pathUrl
                   parameters:prama success:^(AFHTTPRequestOperation *operation, id responseObject) {
                            success(responseObject);
                } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                            failure(error);
                }];
                break;
            }
                
            default:
                break;
        }
        
    }
    
    @end
    
    

    好了,现在看看xxxManager层。
    UserManager.h

    #import <Foundation/Foundation.h>
    #import "UserObject.h"
    
    @interface UserManager : NSObject
    
    // 提供获取UserManager实例的类方法
    + (UserManager *)getInstance;
    
    // 给业务层提供的“登录”功能的网络数据请求方法
    - (void)login:(NSString *)account
         password:(NSString *)password
          success:(void(^)(UserObject *userObj))success
          failure:(void(^)(NSError *error))failure;
    
    @end
    
    

    UserManager.m
    登录、注册、修改密码、修改个人资料分别提供不同的方法。在相应方法里通过调用HttpClient提供的网络连接方法完成网络连接,然后把数据转换加工成业务层需要的数据格式UserObject,再交付之。

    #import "UserManager.h"
    #import "HttpClient.h"
    #include "MJExtension.h"
    
    
    @implementation UserManager
    
    
    //=================================== UserManager ==========================================//
    // 和User有关的所有请求接口路径
    NSString *const kUserLogin              = @"user/login";
    NSString *const kUserRegister           = @"user/register";
    
    
    + (UserManager *)getInstance
    {
        return [UserManager new];
    }
    
    - (void)login:(NSString *)account
         password:(NSString *)password
          success:(void(^)(UserObject *userObj))success
          failure:(void(^)(NSError *error))failure
    {
        NSDictionary *pramaDict = @{@"account":account, @"password":password};
    
        // 通过HttpClient提供的请求方法完成网络请求
        HttpClient *httpClient = [HttpClient new];
        
        [httpClient postOfPath:kUserLogin prama:pramaDict success:^(id result) {
            // 把服务器返回的json数据result转换为UserObject类型的userObj
            UserObject *userObj = [UserObject mj_objectWithKeyValues:result];
            success(userObj);
        } failure:^(NSError *error) {
            failure(error);
        }];
    }
    
    @end
    
    

    好了。当业务层开发人员需要完成“登录”功能时,只需调用UserManager中我们定义的login方法就得到了网络数据,并且已转为UserObject给我们。

    #import "ViewController.h"
    #import "UserManager.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        [[UserManager getInstance] login:@"wang66" password:@"123456" success:^(UserObject *userObj) {
            NSLog(@"登录后后台返回用户信息----%@",userObj.description);
        } failure:^(NSError *error) {
            NSLog(@"登录失败----%@",error);
        }];
        
        
    }
    
    @end
    
    


    补充和优化

    上面我们实现了一个简单的网络层,但其实是比较简陋的。真是情况要考虑很多地方的。

    1. 在请求中添加签名认证,保证请求来源于我们自己的APP。

    2. 取消无用的请求。
    比如,比如我们刚进入一个界面后,此刻便会发出一条该界面数据的请求,但是此时用户却点了“返回”,退回了上个界面。此时上个界面的请求已经飞出但还未完成。这时,我们应当取消上个界面的请求,释放带宽。这样对于下来的网络请求是有利的。

    3. 错误信息的处理

    // ------------ GET -------------
            case GET:
            {
                [manager GET:pathUrl
                  parameters:prama
                     success:^(AFHTTPRequestOperation *operation, id responseObject) {
                            success(responseObject);
                } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                            failure(error);
                }];
                break;
            }
    

    这段代码,AFNetworking提供的GET请求,请求成功时回调block返回responseObject,失败时返回error。但是请注意,这里的错误回调仅仅指网络请求错误,要注意区分网络错误和业务错误(也就是网络请求是成功的,但是对于我们的业务来说,是有问题的)。这些信息同样是会在responseObject返回。实际上一般网络请求成功后,后台返回的responseObject一般都有errorCode字段,只有当errorCode=0时,就说明一切OK,正常返回了我们需要的数据。所以,为了给业务层提供方便,我们还得在网络层做些处理。使交付给业务层的数据里,成功回调的block里就纯粹了业务逻辑意义上正确的数据,而失败的回调里的数据则包括一切错误信息。所说的处理就是在该方法里对回调block做层包装。

    // 完成网络连接的核心
    - (void)sendRequestOfType:(RequestMethod)requestType
                         path:(NSString *)path
                        prama:(id)prama
                      success:(void(^)(id result))success
                      failure:(void(^)(NSError *error))failure
    {
        // 拼接参数,得到完整的接口地址
        NSURL *baseUrl = [NSURL URLWithString:BaseURL];
        NSString *pathUrl = [NSString stringWithFormat:@"%@%@",BaseURL,path];
        
        /**
         通常在此,可以拼接一些公共参数,或者签名认证参数。
         */
    
        AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseUrl];
        
        // ------------------ 包装回调block ------------------
        //请求成功block
        void(^ok)(id responseObject) = ^(id responseObject){
            if([responseObject isKindOfClass:[NSDictionary class]])
            {
                Result *result = [Result mj_objectWithKeyValues:responseObject];
                if (result.errorCode == 0)
                {
                    success(responseObject); //业务逻辑意义上的正确返回。
                }
                else
                {
                    // 有错误。
                    NSError *error = [NSError errorWithDomain:result.message code:result.errorCode userInfo:nil];
                    failure(error);
                }
            }
            else
            {
                NSError *error = [NSError errorWithDomain:@"服务返回数据异常" code:-1 userInfo:nil];
                failure(error);
            }
        };
        
        //请求失败block
        void(^fail)(NSError *error) = ^(NSError *error){
            failure(error);
        };
        
        // ------------------------------------
    
    
        
        switch (requestType)
        {
    // ------------ GET -------------
            case GET:
            {
                [manager GET:pathUrl
                  parameters:prama
                     success:^(AFHTTPRequestOperation *operation, id responseObject) {
    //                        success(responseObject);
                         ok(responseObject);
                } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    //                        failure(error);
                        fail(error);
                    
                }];
                break;
            }
                
    // ------------ POST -------------
                
            case POST:
            {
                [manager POST:pathUrl
                   parameters:prama success:^(AFHTTPRequestOperation *operation, id responseObject) {
    //                        success(responseObject);
                       ok(responseObject);
                } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    //                        failure(error);
                        fail(error);
                }];
                break;
            }
                
            default:
                break;
        }
    }
    

    ** 4.多服务器多环境切换:**
    一般比较规范的项目都有开发环境、测试环境、预发布环境、正式环境(生产环境)四种环境,它们对应的服务器地址分别是不同的。在项目版本迭代过程以“开发——>测试——>预发布——>正式”这个顺序进行的。开发环境就是新需求下来后的更改。测试环境就是给测试打了包后,改bug时的更改。预发布环境就是测试基本完成,交付给运营测试,改动基本比较小。正式环境不用解释,不言而喻。
    我们可以把多环境的配置写在预编译头文件中:

    /************环境配置开关**********
     * OPEN_TEST  0:为开发环境
     *            1:为测试环境
     *            2:为预发布外网环境
     *            其他:为生产环境
     ***************************/
    
    
    #define OPEN_TEST 0
    
    #if (OPEN_TEST == 0)/************开发环境************/
    #define HTTPSURLEVER  @"http://www.runedu.test/api"
    
    
    #elif (OPEN_TEST == 1)/************测试环境************/
    #define HTTPSURLEVER  @"http://www.rjy.rd/api"
    
    
    #elif (OPEN_TEST == 2)/************预发布环境************/
    #define HTTPSURLEVER  @"http://www.prerjy.com/api"
    
    
    #else/************生产环境************/
    #define HTTPSURLEVER @"http://www.runjiaoyu.com.cn/api"
    
    #endif
    

    相关文章

      网友评论

      • liusong007:希望看到一个完整的哈
        而且直接进行模型返回,跟模型耦合度较高,而且后台许多时候最外面一两层就没有什么东西,第一层 一个数组key 还有状态码 及状态信息(直接返回需要多建这个没有什么锤子用的模型) 返回字典就好了,最里面需要用到的数据再单独建立一个模型就好,外层的层级通过字典的key objectForkey找到所要的数据使用MJ转化就好
        外面那些就不用通过模型点语法,还有各一层无用的遍历了

        网络状态
        数据缓存
        错误处理
        用户是否登录进行相应的页面
        相关数据库处理
        上传下载(断点)
        安全验证
        ......
      • Thebloodelves:和我现在喜欢的封装差不多,不错
      • Mr_Zander:离开页面时需要取消本页面的网络请求,那么是不是需要在每个页面都创建一个UserObject的实例,这样是不是造成需要写很多重复代码?取消请求的时候,怎么才能知道应该取消哪些请求呢?
        csqingyang:@施治昂 不用,如果使用 AFN 3.0 以上版本,可以直接操作对应的 manager 取消网络请求
      • 48e030ceaec7:楼主,可以加你好友么
        48e030ceaec7:@Wang66 加我Q:2418955186,希望能有机会和你交流
        Wang66:@吴慕 可以的啊
      • c0789b75492d:实际上在网络层的最底层模块,httpclient还是属于集约型的api咯,对不对呢?
        Wang66:@王大先森咯 网络层关于网络连接也是离散的,针对不同的网络请求方式提供不同方法,但最底层也可以说是集约的,因为是通过一个方法完成网络连接的

      本文标题:关于网络层的设计(一)——和业务层的对接

      本文链接:https://www.haomeiwen.com/subject/ztsuhttx.html