自个儿读IOS特技iOS开发

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

2015-12-27  本文已影响2174人  Wang66

前言

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


p1.jpeg

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


第一个问题,“选择哪种方式请求网络数据?”。第三方库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
上一篇下一篇

猜你喜欢

热点阅读