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

前言

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


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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容