业务层网络封装

背景

网络请求经常会出现一部分公共业务,比如加密,缓存方式等等,所以我们需要对网络做一层偏业务的封装,既可以统一管理一些公共业务,也可以方便网络库的升级AFNetworking。

随着需求的增加网络层对应调整

基础需求

提供URL、参数、简单实现Get、Post、Put、Delete实现,成功返回结果,失败返回错误原因

typedef NS_ENUM (NSUInteger, APIRequestType)
{
   APIRequestTypeGet,
   APIRequestTypePost,
   APIRequestTypePut,
   APIRequestTypeDelete,
};

@interface APIManager : NSObject

+ (void)loadDataWithURLString:(NSString *)urlString
                      params:(NSDictionary *)params
                 requestType:(APIRequestType)requestType
             completeHandler:(void(^)(BOOL success, id result, NSString *message))completeHandler;

@end

使用举例

[APIManager loadDataWithURLString:@"https://192.168.1.1:8080/login"
                           params:@{@"name":@"1000", @"password": @"123456"}
                      requestType: APIRequestTypeGet
                  completeHandler:^(BOOL success, id result, NSString *message) {
                        
                  }];

每次都要写URLString的拼接比较麻烦,而且还要有Debug和Release的判断,很自然会把固定的host部分https://192.168.1.1:8080/统一管理起来,而对外暴露的接口可以优化为

@interface APIManager : NSObject

+ (void)loadDataWithAction:(NSString *)action
                   params:(NSDictionary *)params
              requestType:(APIRequestType)requestType
          completeHandler:(void(^)(BOOL success, id result, NSString *message))completeHandler;

@end

使用举例

[APIManager loadDataWithAction:@"login"
                        params:@{@"name":@"1000", @"password": @"123456"}
                   requestType: APIRequestTypeGet
               completeHandler:^(BOOL success, id result, NSString *message) {
                     
               }];

接下来我们简单对host的管理介绍一下

  • 如果整个项目host是固定的而且在一个环境(debug、release环境)中有且只有一个,那么我们可以使用宏定义。比如
#ifdef DEBUG
    #define APIManagerHost @"http://192.168.40.100:8085/"
#else
    #define APIManagerHost @"https://www.google.com:8085/"
#endif
  • 如果我们的host需要动态变化,我们一般会把host相关的业务做成一个单例,这样就比较方便的切换了。假设我们有这样的需求,开发环境、生产环境、预发布环境可以手动自由切换。
typedef NS_ENUM(NSInteger, APIServiceType)
{
    APIServiceTypeDebug,         //开发环境 
    APIServiceTypeRelease,       //生产环境
    APIServiceTypeReleaseDebug,  //预发布环境
};

@interface APIService : NSObject

+ (instancetype)shareInstance;

///默认会根据build configuration的选择自动设置
@property (nonatomic, assign) APIServiceType serviceType;

///当前host地址
- (NSString *)currentHost;

@end

以上的方案针对比较简单的网络需求这样设计基本上就可以解决问题,而且给其他人使用理解成本也比较小。实际情况我们的需求是在不断增加的

比如:我们要对部分接口进行加密,很自然就会想到下面处理

@interface APIManager : NSObject

+ (void)loadDataWithAction:(NSString *)action
                    params:(NSDictionary *)params
               requestType:(APIRequestType)requestType
           completeHandler:(void(^)(BOOL success, id result, NSString *message))completeHandler;

+ (void)loadDataWithAction:(NSString *)action
                    params:(NSDictionary *)params
               requestType:(APIRequestType)requestType
             shouldEncrypt:(BOOL)shouldEncrypt
           completeHandler:(void(^)(BOOL success, id result, NSString *message))completeHandler;

@end

我们接着增加需求,部分接口需要指定超时时间

@interface APIManager : NSObject

+ (void)loadDataWithAction:(NSString *)action
                    params:(NSDictionary *)params
               requestType:(APIRequestType)requestType
           completeHandler:(void(^)(BOOL success, id result, NSString *message))completeHandler;

+ (void)loadDataWithAction:(NSString *)action
                    params:(NSDictionary *)params
               requestType:(APIRequestType)requestType
             shouldEncrypt:(BOOL)shouldEncrypt
           completeHandler:(void(^)(BOOL success, id result, NSString *message))completeHandler;

+ (void)loadDataWithAction:(NSString *)action
                    params:(NSDictionary *)params
               requestType:(APIRequestType)requestType
                   timeOut:(CGFloat)timeOut
           completeHandler:(void(^)(BOOL success, id result, NSString *message))completeHandler;

+ (void)loadDataWithAction:(NSString *)action
                    params:(NSDictionary *)params
               requestType:(APIRequestType)requestType
             shouldEncrypt:(BOOL)shouldEncrypt
                   timeOut:(CGFloat)timeOut
           completeHandler:(void(^)(BOOL success, id result, NSString *message))completeHandler;

@end

当产品经理提出要支持缓存的时候,发现这样加接口有些难受了。

为了不需要持续修改接口,我们可能会想到把功能类的也使用一个字典来存储,这样就可以不需要每次都去改接口了

extern NSString *const APIManagerFeatureKeyEncrypt;  //加密,BOOL值
extern NSString *const APIManagerFeatureKeyTimeOut;  //超时,CGFloat值
extern NSString *const APIManagerFeatureKeyCache;    //缓存,BOOL值
//内存缓存、硬盘缓存、缓存时间等等...

@interface APIManager : NSObject

+ (void)loadDataWithAction:(NSString *)action
                    params:(NSDictionary *)params
               requestType:(APIRequestType)requestType
                  features:(NSDictionary *)features
           completeHandler:(void(^)(BOOL success, id result, NSString *message))completeHandler;

@end

这样设计之后,每次增加功能,暂时有了统一解决方式。

我们再来分析一下回调块。

在支持缓存数据的时候,我们有可能需要知道当前返回的数据是不是来着缓存,这时会发现BOOL success, id result, NSString *message的结构是很难满足需求的,如果通过增加个返回变量BOOL success, BOOL cache, id result, NSString *message来解决,根据上面的经验,很容易出现需要持续修改接口,比如,需要知道缓存来着内存还是硬盘,或者需要知道这次请求用了多长时间,这时很容易确定通过增加变量很难扩展;这时可能会想到要不也像features一样返回,也定义一些key,比如下面的方式

extern NSString *const APIManagerFeatureKeyEncrypt;  //加密,BOOL值
extern NSString *const APIManagerFeatureKeyTimeOut;  //超时,CGFloat值
extern NSString *const APIManagerFeatureKeyCache;    //缓存,BOOL值
//内存缓存、硬盘缓存、缓存时间等等...

extern NSString *const APIManagerExtendDataKeyCacheRAM;           //内存缓存,BOOL值
extern NSString *const APIManagerExtendDataKeyCacheNative;        //硬盘缓存,BOOL值
extern NSString *const APIManagerExtendDataKeyRequstTimeInterval; //请求时间,CGFloat值
//等等...

@interface APIManager : NSObject

+ (void)loadDataWithAction:(NSString *)action
                    params:(NSDictionary *)params
               requestType:(APIRequestType)requestType
                  features:(NSDictionary *)features
           completeHandler:(void(^)(BOOL success, id result, NSDictionary *extend, NSString *message))completeHandler;

@end

考虑到字典的局限性,不能有基础类型(int,CGFloat...)。我们可能会想到,把字典换成对象,于是简单封装成请求一个对象,返回一个对象。比如下面的实现方式


@interface APIRequest : NSObject

@property (nonatomic, strong) NSString *action;
@property (nonatomic, strong) NSDictionary *params;
@property (nonatomic, assign) APIRequestType requestType;   //默认Get

@property (nonatomic, strong) NSString *host;               //默认APIService.currentHost
@property (nonatomic, assign) BOOL shouldEncrypt;           //默认NO
@property (nonatomic, assign) BOOL shouldCache;             //默认NO
@property (nonatomic, assign) CGFloat timeOut;              //默认10s

@end

@interface APIResponse : NSObject

@property (nonatomic, assign) BOOL success;
@property (nonatomic, strong) id jsonObject;
@property (nonatomic, strong) NSString *message;
@property (nonatomic, assign) BOOL fromCache;

@end

@interface APIManager : NSObject

+ (void)loadDataWithRequest:(APIRequest *)request
            completeHandler:(void(^)(APIResponse *response))completeHandler;

@end

这样设计完,加功能什么的只要通过APIRequest中加功能就行了,使用起来虽然没有最上面那么直观,但好歹也可以基本上解决问题了,而且接口也不需要变了。但是随着功能的增加,APIRequest和APIManager会越来越杂,可读性越来越差。比如我们增加公共请求参数、公共header参数、取消功能、XX模块统一使用加密方式1,YY模块统一使用加密方式2 等等...

@interface APIRequest : NSObject

@property (nonatomic, strong) NSString *action;
@property (nonatomic, strong) NSDictionary *params;
@property (nonatomic, strong) NSDictionary *headerParams;   //默认nil
@property (nonatomic, assign) APIRequestType requestType;   //默认Get

@property (nonatomic, strong) NSString *host;                   //默认APIService.currentHost
@property (nonatomic, strong) NSDictionary *commonParams;       //默认nil
@property (nonatomic, strong) NSDictionary *commonHeaderParams; //默认nil

@property (nonatomic, assign) BOOL shouldEncrypt;           //默认NO
@property (nonatomic, assign) BOOL shouldCache;             //默认NO
@property (nonatomic, assign) CGFloat timeOut;              //默认10s
//...

@end

这时我们开始考虑如何给APIRequest和APIManager瘦身,可以把同一类的业务拆分出去。

APIRequest拆分

host、commonParams、commonHeaderParams这类不怎么变的可以使用配置类统一管理

@interface APIConfig : NSObject

@property (nonatomic, strong) NSString *host;                   //默认APIService.currentHost
@property (nonatomic, strong) NSDictionary *commonParams;       //默认nil
@property (nonatomic, strong) NSDictionary *commonHeaderParams; //默认nil

@end

@interface APIRequest : NSObject

@property (nonatomic, strong) NSString *action;
@property (nonatomic, strong) NSDictionary *params;
@property (nonatomic, strong) NSDictionary *headerParams;   //默认nil
@property (nonatomic, assign) APIRequestType requestType;   //默认Get

@property (nonatomic, assign) BOOL shouldEncrypt;           //默认NO
@property (nonatomic, assign) BOOL shouldCache;             //默认NO
@property (nonatomic, assign) CGFloat timeOut;              //默认10s

@end

APIManager拆分

如果使用类方法做统一入口,拆分起来只能在实现中做很多工具方法,统一的入口还是集中在.m文件中,如果是某些模块定制的请求方式,其他模块不应该暴露的,于是我们会联想到继承

网络层结构.png

这时我们可以很好的定制一类API了,简单封装如下

typedef NS_ENUM (NSUInteger, APIRequestType)
{
    APIRequestTypeGet,
    APIRequestTypePost,
    APIRequestTypePut,
    APIRequestTypeDelete,
};

@interface APIConfig : NSObject

@property (nonatomic, strong) NSString *host;
@property (nonatomic, strong) NSDictionary *commonParams;
@property (nonatomic, strong) NSDictionary *commonHeaderParams;

@end

@interface APIRequest : NSObject

@property (nonatomic, strong) NSString *action;
@property (nonatomic, strong) NSDictionary *params;
@property (nonatomic, assign) APIRequestType requestType;

@property (nonatomic, strong) NSDictionary *headerParams;  //默认为nil
@property (nonatomic, assign) BOOL shouldEncrypt;          //默认NO
@property (nonatomic, assign) BOOL shouldCache;            //默认NO
@property (nonatomic, assign) CGFloat timeOut;             //默认10s

@end

@interface APIResponse : NSObject

@property (nonatomic, assign) BOOL success;
@property (nonatomic, strong) id jsonObject;
@property (nonatomic, strong) NSString *message;
@property (nonatomic, assign) BOOL fromCache;

@end

@interface APIManager : NSObject

@property (nonatomic, readonly) APIConfig *config;
@property (nonatomic, readonly) APIRequest *request;
@property (nonatomic, readonly) APIResponse *response;
@property (nonatomic, readonly) NSURLSessionTask *currentTask;

- (instancetype)initWithConfig:(APIConfig *)config;
- (void)loadDataWithRequest:(APIRequest *)request
            completeHandler:(void(^)(APIResponse *response))completeHandler;
- (void)cancel;

@end

接口修改成这样,扩展性相比之前过程化的设计还是好很多,增加功能也不在需要改接口。为了给上层定制提供更方便的定制,我们可以增加一些通用的拦截器。

@interface APIManager : NSObject

@property (nonatomic, readonly) APIConfig *config;
@property (nonatomic, readonly) APIRequest *request;
@property (nonatomic, readonly) APIResponse *response;
@property (nonatomic, readonly) NSURLSessionTask *currentTask;

- (instancetype)initWithConfig:(APIConfig *)config;
- (void)loadDataWithRequest:(APIRequest *)request
            completeHandler:(void(^)(APIResponse *response))completeHandler;
- (void)cancel;

- (BOOL)willSendRequest:(APIRequest *)request;         //将要发起请求,返回NO,进入失败回调
- (void)didSendRequest:(APIRequest *)request;          //发起请求完成
- (BOOL)willSuccessCallBack:(APIResponse *)response;   //与服务器交互成功,返回NO,进入失败回调
- (void)didSuccessCallBack:(APIResponse *)response;    //回调完成
- (BOOL)willFailureCallBack:(APIResponse *)response;   //与服务器交互失败,返回NO,不回调
- (void)didFailureCallBack:(APIResponse *)response;    //回调完成

@end

XX模块定制特殊的加密方式1

@interface XXAPIRequest : NSObject

///默认NO
@property (nonatomic, assign) BOOL shouldEncrypt1;   

@end

@interface XXAPIManager: APIBaseManager

@end

@implementation XXAPIManager

- (instancetype)init
{
    APIConfig *config = [[APIConfig alloc] init];
    config.host = [XXAPIService shareInstance].currentHost;
    config.commonParams = @{};
    config.commonHeaderParams = @{};
    return [self initWithConfig:config];
}

- (BOOL)willSendRequest:(APIRequest *)request
{
    if (self. shouldEncrypt1)
    {
        //加密方式1...
    }
    return YES;
}

@end

XX模块业务调用

NSDictionary *params = @{@"name":@"1000", @"password": @"123456"};
XXAPIRequest *request = [XXAPIRequest requestWithAction:@"login" params:params];
request.shouldEncrypt1 = YES;
XXAPIManager *api = [[XXAPIManager alloc] init];
[api loadDataWithRequest:request completeHandler:^(APIResponse *response) {
    
}];

为了让业务调用的更舒服,我们还可以通过拦截器做一些通用的校验,假设XX模块的服务器返回的通用结构为以下方式

{
    "code": 100, 
    "message": "请求成功", 
    "result": {
        "key1": "value1", 
        "key2": "value2"
    }
}

当code为100的时候,请求成功,业务调用完全没有必要每次判断code的值,完全可以在XXAPIManager这一层做一次校验

@implementation XXAPIManager

- (instancetype)init
{
    APIConfig *config = [[APIConfig alloc] init];
    config.host = [XXAPIService shareInstance].currentHost;
    config.commonParams = @{};
    config.commonHeaderParams = @{};
    return [self initWithConfig:config];
}

- (BOOL)willSendRequest:(APIRequest *)request
{
    if (self. shouldEncrypt1)
    {
        //加密方式1...
    }
    return YES;
}

- (BOOL)willSuccessCallBack:(APIResponse *)response
{
    NSDictionary *jsonObject = response.jsonObject;
    if ([jsonObject isKindOfClass:[NSDictionary class]])
    {
        if ([jsonObject[@"code"] integerValue] == 100)
        {
            return YES;
        }
        else
        {
            response.message = jsonObject[@"message"];
            return NO;
        }
    }
    response.message = @"服务器数据结构异常,请稍后再试";
    return NO;
}

@end

网络层应该具备的特点

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

推荐阅读更多精彩内容

  • iOS网络架构讨论梳理整理中。。。 其实如果没有APIManager这一层是没法使用delegate的,毕竟多个单...
    yhtang阅读 5,160评论 1 23
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,579评论 18 139
  • 开源框架地址:https://github.com/square/retrofit 英文文档官网:http://s...
    于加泽阅读 493评论 0 0
  • “经常不断地学习,你就什么都知道。你知道得越多,你就越有力量。”我想,高尔基的这句话,便道出了所有。 阅读是我们终...
    记忆的路线阅读 392评论 0 1
  • 不知从什么时候起,社会上有了闲人。 闲人总是笑笑的。“喂,哥们!”他一跳一跃地迈雀步过来了,还趿着鞋,光身子穿一件...
    子非宴遇阅读 366评论 0 1