BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

iOS应用架构谈(三):网络层设计方案(上)

| 作者 田伟宇 关注 1 他的粉丝 发布于 2015年7月21日. 估计阅读时间: 49 分钟 | 都知道硅谷人工智能做的好,你知道 硅谷的运维技术 也值得参考吗?QCon上海带你探索其中的奥义

编者按:iOS客户端应用架构看似简单,但实际上要考虑的事情不少。本文作者将以系列文章的形式来讨论iOS应用架构中的种种问题,本文是其中的第三篇,主要讲网络层设计以及安全机制和优化方案。

前言

网络层在一个App中也是一个不可缺少的部分,工程师们在网络层能够发挥的空间也比较大。另外,苹果对网络请求部分已经做了很好的封装,业界的AFNetworking也被广泛使用。其它的ASIHttpRequest,MKNetworkKit啥的其实也都还不错,但前者已经弃坑,后者也在弃坑的边缘。在实际的App开发中,Afnetworking已经成为了事实上各大App的标准配置。

网络层在一个App中承载了API调用,用户操作日志记录,甚至是即时通讯等任务。我接触过一些App(开源的和不开源的)的代码,在看到网络层这一块时,尤其是在看到各位架构师各显神通展示了各种技巧,我非常为之感到兴奋。但有的时候,往往也对于其中的一些缺陷感到失望。

关于网络层的设计方案会有很多,需要权衡的地方也会有很多,甚至于争议的地方都会有很多。但无论如何,我都不会对这些问题做出任何逃避,我会在这篇文章中给出我对它们的看法和解决方案,观点绝不中立,不会跟大家打太极。

这篇文章就主要会讲这些方面:

  1. 网络层跟业务对接部分的设计
  2. 网络层的安全机制实现
  3. 网络层的优化方案

网络层跟业务对接部分的设计

在安居客App的架构更新换代的时候,我深深地感觉到网络层跟业务对接部分的设计有多么重要,因此我对它做的最大改变就是针对网络层跟业务对接部分的改变。网络层跟业务层对接部分设计的好坏,会直接影响到业务工程师实现功能时的心情。

在正式开始讲设计之前,我们要先讨论几个问题:

  1. 使用哪种交互模式来跟业务层做对接?
  2. 是否有必要将API返回的数据封装成对象然后再交付给业务层?
  3. 使用集约化调用方式还是离散型调用方式去调用API?

这些问题讨论完毕之后,我会给出一个完整的设计方案来给大家做参考,设计方案是鱼,讨论的这些问题是渔,我什么都授了,大家各取所需。

使用哪种交互模式来跟业务层做对接?

这里其实有两个问题:一,以什么方式将数据交付给业务层?二,交付什么样的数据给业务层?

以什么方式将数据交付给业务层?

iOS开发领域有很多对象间数据的传递方式,我看到的大多数App在网络层所采用的方案主要集中于这三种:Delegate,Notification,Block。KVO和Target-Action我目前还没有看到有使用的。

目前我知道边锋主要是采用的block,大智慧主要采用的是Notification,安居客早期以Block为主,后面改成了以Delegate为主,阿里没发现有通过Notification来做数据传递的地方(可能有),Delegate、Block以及target-action都有,阿里iOS App网络层的作者说这是为了方便业务层选择自己合适的方法去使用。这里大家都是各显神通,每次我看到这部分的时候,我都喜欢问作者为什么采用这种交互方案,但很少有作者能够说出个条条框框来。

然而在我这边,我的意见是以Delegate为主,Notification为辅。原因如下:

  • 尽可能减少跨层数据交流的可能,限制耦合
  • 统一回调方法,便于调试和维护
  • 在跟业务层对接的部分只采用一种对接手段(在我这儿就是只采用delegate这一个手段)限制灵活性,以此来交换应用的可维护性

尽可能减少跨层数据交流的可能,限制耦合

什么叫跨层数据交流?就是某一层(或模块)跟另外的与之没有直接对接关系的层(或模块)产生了数据交换。为什么这种情况不好?严格来说应该是大部分情况都不好,有的时候跨层数据交流确实也是一种需求。之所以说不好的地方在于,它会导致代码混乱,破坏模块的封装性。我们在做分层架构的目的其中之一就在于下层对上层有一次抽象,让上层可以不必关心下层细节而执行自己的业务。

所以,如果下层细节被跨层暴露,一方面你很容易因此失去邻层对这个暴露细节的保护;另一方面,你又不可能不去处理这个细节,所以处理细节的相关代码就会散落各地,最终难以维护。

说得具象一点就是,我们考虑这样一种情况:A<-B<-C。当C有什么事件,通过某种方式告知B,然后B执行相应的逻辑。一旦告知方式不合理,让A有了跨层知道C的事件的可能,你 就很难保证A层业务工程师在将来不会对这个细节作处理。一旦业务工程师在A层产生处理操作,有可能是补充逻辑,也有可能是执行业务,那么这个细节的相关处理代码就会有一部分散落在A层。然而前者是不应该散落在A层的,后者有可能是需求。另外,因为B层是对A层抽象的,执行补充逻辑的时候,有可能和B层针对这个事件的处理逻辑产生冲突,这是我们很不希望看到的。

那么什么情况跨层数据交流会成为需求?在网络层这边,信号从2G变成3G变成4G变成Wi-Fi,这个是跨层数据交流的其中一个需求。不过其他的跨层数据交流需求我暂时也想不到了,哈哈,应该也就这一个吧。

严格来说,使用Notification来进行网络层和业务层之间数据的交换,并不代表这一定就是跨层数据交流,但是使用Notification给跨层数据交流开了一道口子,因为Notification的影响面不可控制,只要存在实例就存在被影响的可能。另外,这也会导致谁都不能保证相关处理代码就在唯一的那个地方,进而带来维护灾难。作为架构师,在这里给业务工程师限制其操作的灵活性是必要的。另外,Notification也支持一对多的情况,这也给代码散落提供了条件。同时,Notification所对应的响应方法很难在编译层面作限制,不同的业务工程师会给他取不同的名字,这也会给代码的可维护性带来灾难。

手机淘宝架构组的侠武同学曾经给我分享过一个问题,在这里我也分享给大家:曾经有一个工程师在监听Notification之后,没有写释放监听的代码,当然,找到这个原因又是很漫长的一段故事,现在找到原因了,然而监听这个Notification的对象有那么多,不知道具体是哪个Notificaiton,也不知道那个没释放监听的对象是谁。后来折腾了很久大家都没办法的时候,有一个经验丰富的工程师提出用hook(Method Swizzling)的方式,最终找到了那个没释放监听的对象,bug修复了。

我分享这个问题的目的并不是想强调Notification多么多么不好,Notification本身就是一种设计模式,在属于它的问题领域内,Notification是非常好的一种解决方案。但我想强调的是,对于网络层这个问题领域内来看,架构师首先一定要限制代码的影响范围,在能用影响范围小的方案的时候就尽量采用这种小的方案,否则将来要是有什么奇怪需求或者出了什么小问题,维护起来就非常麻烦。因此Notification这个方案不能作为首选方案,只能作为备选。

那么Notification也不是完全不能使用,当需求要求跨层时,我们就可以使用Notification,比如前面提到的网络条件切换,而且这个需求也是需要满足一对多的。

所以,为了符合前面所说的这些要求,使用Delegate能够很好地避免跨层访问,同时限制了响应代码的形式,相比Notification而言有更好的可维护性。

然后我们顺便来说说为什么尽量不要用block

1. block很难追踪,难以维护

我们在调试的时候经常会单步追踪到某一个地方之后,发现尼玛这里有个block,如果想知道这个block里面都做了些什么事情,这时候就比较蛋疼了。

- (void)someFunctionWithBlock:(SomeBlock *)block
{
    ... ...
 -> block();  //当你单步走到这儿的时候,要想知道block里面都做了哪些事情的话,就很麻烦。
    ... ...
}

2. block会延长相关对象的生命周期

block会给内部所有的对象引用计数加一,这一方面会带来潜在的retain cycle,不过我们可以通过Weak Self的手段解决。另一方面比较重要就是,它会延长对象的生命周期。

在网络回调中使用block,是block导致对象生命周期被延长的其中一个场合,当ViewController从window中卸下时,如果尚有请求带着block在外面飞,然后block里面引用了ViewController(这种场合非常常见),那么ViewController是不能被及时回收的,即便你已经取消了请求,那也还是必须得等到请求着陆之后才能被回收。

然而使用delegate就不会有这样的问题,delegate是弱引用,哪怕请求仍然在外面飞,,ViewController还是能够及时被回收的,回收之后指针自动被置为了nil,无伤大雅。

所以平时尽量不要滥用block,尤其是在网络层这里。

3. 统一回调方法,便于调试和维护

前面讲的是跨层问题,区分了Delegate和Notification,顺带谈了一下Block。然后现在谈到的这个情况,就是另一个采用Block方案不是很合适的情况。首先,Block本身无好坏对错之分,只有合适不合适。在这一节要讲的情况里,Block无法做到回调方法的统一,调试和维护的时候也很难在调用栈上显示出来,找的时候会很蛋疼。

在网络请求和网络层接受请求的地方时,使用Block没问题。但是在获得数据交给业务方时,最好还是通过Delegate去通知到业务方。因为Block所包含的回调代码跟调用逻辑放在同一个地方,会导致那部分代码变得很长,因为这里面包括了调用前和调用后的逻辑。从另一个角度说,这在一定程度上违背了single function,single task的原则,在需要调用API的地方,就只要写API调用相关的代码,在回调的地方,写回调的代码。

然后我看到大部分App里,当业务工程师写代码写到这边的时候,也意识到了这个问题。因此他们会在block里面写个一句话的方法接收参数,然后做转发,然后就可以把这个方法放在其他地方了,绕过了Block的回调着陆点不统一的情况。比如这样:

  [API callApiWithParam:param successed:^(Response *response){
        [self successedWithResponse:response];
    } failed:^(Request *request, NSError *error){
        [self failedWithRequest:request error:error];
    }];

这实质上跟使用Delegate的手段没有什么区别,只是绕了一下,不过还是没有解决统一回调方法的问题,因为block里面写的方法名字可能在不同的ViewController对象中都会不一样,毕竟业务工程师也是很多人,各人有各人的想法。所以架构师在这边不要贪图方便,还是使用delegate的手段吧,业务工程师那边就能不用那么绕了。Block是目前大部分第三方网络库都采用的方式,因为在发送请求的那一部分,使用Block能够比较简洁,因此在请求那一层是没有问题的,只是在交换数据之后,还是转变成delegate比较好,比如AFNetworking里面:

  [AFNetworkingAPI callApiWithParam:self.param successed:^(Response *response){
        if ([self.delegate respondsToSelector:@selector(successWithResponse:)]) {
            [self.delegate successedWithResponse:response];
        }
    } failed:^(Request *request, NSError *error){
        if ([self.delegate respondsToSelector:@selector(failedWithResponse:)]) {
            [self failedWithRequest:request error:error];
        }
    }];

这样在业务方这边回调函数就能够比较统一,便于维护。

综上,对于以什么方式将数据交付给业务层?这个问题的回答是这样:

尽可能通过Delegate的回调方式交付数据,这样可以避免不必要的跨层访问。当出现跨层访问的需求时(比如信号类型切换),通过Notification的方式交付数据。正常情况下应该是避免使用Block的。

交付什么样的数据给业务层?

我见过非常多的App的网络层在拿到JSON数据之后,会将数据转变成对应的对象原型。注意,我这里指的不是NSDictionary,而是类似Item这样的对象。这种做法是能够提高后续操作代码的可读性的。在比较直觉的思路里面,是需要这部分转化过程的,但这部分转化过程的成本是很大的,主要成本在于:

  1. 数组内容的转化成本较高:数组里面每项都要转化成Item对象,如果Item对象中还有类似数组,就很头疼。
  2. 转化之后的数据在大部分情况是不能直接被展示的,为了能够被展示,还需要第二次转化。
  3. 只有在API返回的数据高度标准化时,这些对象原型(Item)的可复用程度才高,否则容易出现类型爆炸,提高维护成本。
  4. 调试时通过对象原型查看数据内容不如直接通过NSDictionary/NSArray直观。
  5. 同一API的数据被不同View展示时,难以控制数据转化的代码,它们有可能会散落在任何需要的地方。

其实我们的理想情况是希望API的数据下发之后就能够直接被View所展示。首先要说的是,这种情况非常少。另外,这种做法使得View和API联系紧密,也是我们不希望发生的。

在设计安居客的网络层数据交付这部分时,我添加了reformer(名字而已,叫什么都好)这个对象用于封装数据转化的逻辑,这个对象是一个独立对象,事实上,它是作为Adaptor模式存在的。我们可以这么理解:想象一下我们洗澡时候使用的莲蓬头,水管里出来的水是API下发的原始数据。reformer就是莲蓬头上的不同水流挡板,需要什么模式,就拨到什么模式。

在实际使用时,代码观感是这样的:

先定义一个protocol:
@protocol ReformerProtocol 
- (NSDictionary)reformDataWithManager:(APIManager *)manager;
@end
在Controller里是这样:
@property (nonatomic, strong) id<ReformerProtocol> XXXReformer;
@property (nonatomic, strong) id<ReformerProtocol> YYYReformer;
#pragma mark - APIManagerDelegate
- (void)apiManagerDidSuccess:(APIManager *)manager
{
    NSDictionary *reformedXXXData = [manager fetchDataWithReformer:self.XXXReformer];
    [self.XXXView configWithData:reformedXXXData];
    NSDictionary *reformedYYYData = [manager fetchDataWithReformer:self.YYYReformer];
    [self.YYYView configWithData:reformedYYYData];
}
在APIManager里面,fetchDataWithReformer是这样:
- (NSDictionary)fetchDataWithReformer:(id)reformer
{
    if (reformer == nil) {
        return self.rawData;
    } else {
        return [reformer reformDataWithManager:self];
    }
}
  • 要点1:reformer是一个符合ReformerProtocol的对象,它提供了通用的方法供Manager使用。
  • 要点2:API的原始数据(JSON对象)由Manager实例保管,reformer方法里面取Manager的原始数据(manager.rawData)做转换,然后交付出去。莲蓬头的水管部分是Manager,负责提供原始水流(数据流),reformer就是不同的模式,换什么reformer就能出来什么水流。
  • 要点3:例子中举的场景是一个API数据被多个View使用的情况,体现了reformer的一个特点:可以根据需要改变同一数据来源的展示方式。比如API数据展示的是“附近的小区”,那么这个数据可以被列表(XXXView)和地图(YYYView)共用,不同的view使用的数据的转化方式不一样,这就通过不同的reformer解决了。
  • 要点4:在一个view用来同一展示不同API数据的情况,reformer是绝佳利器。比如安居客的列表view的数据来源可能有三个:二手房列表API,租房列表API,新房列表API。这些API返回来的数据的value可能一致,但是key都是不一致的。这时候就可以通过同一个reformer来做数据的标准化输出,这样就使得view代码复用成为可能。这体现了reformer另外一个特点:同一个reformer出来的数据是高度标准化的。形象点说就是:只要莲蓬头不换,哪怕水管的水变成海水或者污水了,也依旧能够输出符合洗澡要求的淡水水流。举个例子:
- (void)apiManagerDidSuccess:(APIManager *)manager
{
    // 这个回调方法有可能是来自二手房列表APIManager的回调,也有可能是
租房,也有可能是新房。但是在Controller层面我们不需要对它做额外区分,
只要是同一个reformer出来的数据,我们就能保证是一定能被self.XXXView使
用的。这样的保证由reformer的实现者来提供。
    NSDictionary *reformedXXXData = [manager 
fetchDataWithReformer:self.XXXReformer];
    [self.XXXView configWithData:reformedXXXData];
}
  • 要点5:有没有发现,使用reformer之后,Controller的代码简洁了很多?而且,数据原型在这种情况下就没有必要存在了,随之而来的成本也就被我们绕过了。

reformer本质上就是一个符合某个protocol的对象,在controller需要从api manager中获得数据的时候,顺便把reformer传进去,于是就能获得经过reformer重新洗过的数据,然后就可以直接使用了。

更抽象地说,reformer其实是对数据转化逻辑的一个封装。在controller从manager中取数据之后,并且把数据交给view之前,这期间或多或少都是要做一次数据转化的,有的时候不同的view,对应的转化逻辑还不一样,但是展示的数据是一样的。而且往往这一部分代码都非常复杂,且跟业务强相关,直接上代码,将来就会很难维护。所以我们可以考虑采用不同的reformer封装不同的转化逻辑,然后让controller根据需要选择一个合适的reformer装上,就像洗澡的莲蓬头,需要什么样的水流(数据的表现形式)就换什么样的头,然而水(数据)都是一样的。这种做法能够大大提高代码的可维护性,以及减少ViewController的体积。

总结一下,reformer事实上是把转化的代码封装之后再从主体业务中拆分了出来,拆分出来之后不光降低了原有业务的复杂度,更重要的是,它提高了数据交付的灵活性。另外,由于Controller负责调度Manager和View,因此它是知道Manager和View之间的关系的,Controller知道了这个关系之后,就有了充要条件来为不同的View选择不同的Reformer,并用这个Reformer去改造Mananger的数据,然后ViewController获得了经过reformer处理过的数据之后,就可以直接交付给view去使用。Controller因此得到瘦身,负责业务数据转化的这部分代码也不用写在Controller里面,提高了可维护性。

所以reformer机制能够带来以下好处:

  • 好处1:绕开了API数据原型的转换,避免了相关成本。
  • 好处2:在处理单View对多API,以及在单API对多View的情况时,reformer提供了非常优雅的手段来响应这种需求,隔离了转化逻辑和主体业务逻辑,避免了维护灾难。
  • 好处3:转化逻辑集中,且将转化次数转为只有一次。使用数据原型的转化逻辑至少有两次,第一次是把JSON映射成对应的原型,第二次是把原型转变成能被View处理的数据。reformer一步到位。另外,转化逻辑在reformer里面,将来如果API数据有变,就只要去找到对应reformer然后改掉就好了。
  • 好处4:Controller因此可以省去非常多的代码,降低了代码复杂度,同时提高了灵活性,任何时候切换reformer而不必切换业务逻辑就可以应对不同View对数据的需要。
  • 好处5:业务数据和业务有了适当的隔离。这么做的话,将来如果业务逻辑有修改,换一个reformer就好了。如果其他业务也有相同的数据转化逻辑,其他业务直接拿这个reformer就可以用了,不用重写。另外,如果controller有修改(比如UI交互方式改变),可以放心换controller,完全不用担心业务数据的处理。

在不使用特定对象表征数据的情况下,如何保持数据可读性?

不使用对象来表征数据的时候,事实上就是使用NSDictionary的时候。事实上,这个问题就是,如何在NSDictionary表征数据的情况下保持良好的可读性?

苹果已经给出了非常好的做法,用固定字符串做key,比如你在接收到KeyBoardWillShow的Notification时,带了一个userInfo,他的key就都是类似UIKeyboardAnimationCurveUserInfoKey这样的,所以我们采用这样的方案来维持可读性。下面我举一个例子:

PropertyListReformerKeys.h
extern NSString * const kPropertyListDataKeyID;
extern NSString * const kPropertyListDataKeyName;
extern NSString * const kPropertyListDataKeyTitle;
extern NSString * const kPropertyListDataKeyImage;
PropertyListReformer.h
#import "PropertyListReformerKeys.h"
... ...
PropertyListReformer.m
NSString * const kPropertyListDataKeyID = @"kPropertyListDataKeyID";
NSString * const kPropertyListDataKeyName = @"kPropertyListDataKeyName";
NSString * const kPropertyListDataKeyTitle = @"kPropertyListDataKeyTitle";
NSString * const kPropertyListDataKeyImage = @"kPropertyListDataKeyImage";
- (NSDictionary *)reformData:(NSDictionary *)originData fromManager:(APIManager *)manager
{
    ... ...
    ... ...
    NSDictionary *resultData = nil;
    if ([manager isKindOfClass:[ZuFangListAPIManager class]]) {
        resultData = @{
            kPropertyListDataKeyID:originData[@"id"],
            kPropertyListDataKeyName:originData[@"name"],
            kPropertyListDataKeyTitle:originData[@"title"],
            kPropertyListDataKeyImage:[UIImage imageWithUrlString:originData[@"imageUrl"]]
        };
    }
    if ([manager isKindOfClass:[XinFangListAPIManager class]]) {
        resultData = @{
            kPropertyListDataKeyID:originData[@"xinfang_id"],
            kPropertyListDataKeyName:originData[@"xinfang_name"],
            kPropertyListDataKeyTitle:originData[@"xinfang_title"],
            kPropertyListDataKeyImage:[UIImage imageWithUrlString:originData[@"xinfang_imageUrl"]]
        };
    }
    if ([manager isKindOfClass:[ErShouFangListAPIManager class]]) {
        resultData = @{
            kPropertyListDataKeyID:originData[@"esf_id"],
            kPropertyListDataKeyName:originData[@"esf_name"],
            kPropertyListDataKeyTitle:originData[@"esf_title"],
            kPropertyListDataKeyImage:[UIImage imageWithUrlString:originData[@"esf_imageUrl"]]
        };
    }
    return resultData;
}
PropertListCell.m
#import "PropertyListReformerKeys.h"
- (void)configWithData:(NSDictionary *)data
{
    self.imageView.image = data[kPropertyListDataKeyImage];
    self.idLabel.text = data[kPropertyListDataKeyID];
    self.nameLabel.text = data[kPropertyListDataKeyName];
    self.titleLabel.text = data[kPropertyListDataKeyTitle];
}

这一大段代码看下来,我如果不说一下要点,那基本上就白写了哈:

我们先看一下结构:

使用Const字符串来表征Key,字符串的定义跟着reformer的实现文件走,字符串的extern声明放在独立的头文件内。

这样reformer生成的数据的key都使用Const字符串来表示,然后每次别的地方需要使用相关数据的时候,把PropertyListReformerKeys.h这个头文件import进去就好了。

另外要注意的一点是,如果一个OriginData可能会被多个Reformer去处理的话,Key的命名规范需要能够表征出其对应的reformer名字。如果reformer是PropertyListReformer,那么Key的名字就是PropertyListKeyXXXX。

这么做的好处就是,将来迁移的时候相当方便,只要扔头文件就可以了,只扔头文件是不会导致拔出萝卜带出泥的情况的。而且也避免了自定义对象带来的额外代码体积。

另外,关于交付的NSDictionary,其实具体还是看view的需求,reformer的设计初衷是:通过reformer转化出来的可以直接是View,或者是view直接可以使用的对象(包括NSDictionary)。比如地图标点列表API的数据,通过reformer转化之后就可以直接变成MKAnnotation,然后MKMapView就可以直接使用了。这里说的只是当你的需求是交付NSDictionary时,如何保证可读性的情况,再强调一下哈,reformer交付的是view直接可以使用的对象,交付出去的可以是NSDictionary,也可以是UIView,跟DataSource结合之后交付的甚至可以是UITableViewCell/UICollectionViewCell。不要被NSDictionary或所谓的转化成model再交付的思想局限。

综上,我对交付什么样的数据给业务层?这个问题的回答就是这样:

对于业务层而言,由Controller根据View和APIManager之间的关系,选择合适的reformer将View可以直接使用的数据(甚至reformer可以用来直接生成view)转化好之后交付给View。对于网络层而言,只需要保持住原始数据即可,不需要主动转化成数据原型。然后数据采用NSDictionary加Const字符串key来表征,避免了使用对象来表征带来的迁移困难,同时不失去可读性。

集约型API调用方式和离散型API调用方式的选择?

集约型API调用其实就是所有API的调用只有一个类,然后这个类接收API名字,API参数,以及回调着陆点(可以是target-action,或者block,或者delegate等各种模式的着陆点)作为参数。然后执行类似startRequest这样的方法,它就会去根据这些参数起飞去调用API了,然后获得API数据之后再根据指定的着陆点去着陆。比如这样:

集约型API调用方式:

[APIRequest startRequestWithApiName:@"itemList.v1" params:params
 success:@selector(success:) fail:@selector(fail:) target:self];

离散型API调用是这样的,一个API对应于一个APIManager,然后这个APIManager只需要提供参数就能起飞,API名字、着陆方式都已经集成入APIManager中。比如这样:

离散型API调用方式:

@property (nonatomic, strong) ItemListAPIManager *itemListAPIManager;

// getter 
- (ItemListAPIManager *)itemListAPIManager 
{ 
if (_itemListAPIManager == nil) { 
_itemListAPIManager = [[ItemListAPIManager alloc] init]; 
_itemListAPIManager.delegate = self; 
}

return _itemListAPIManager;
}

// 使用的时候就这么写: 
[self.itemListAPIManager loadDataWithParams:params];

集约型API调用和离散型API调用这两者实现方案不是互斥的,单看下层,大家都是集约型。因为发起一个API请求之后,除去业务相关的部分(比如参数和API名字等),剩下的都是要统一处理的:加密,URL拼接,API请求的起飞和着陆,这些处理如果不用集约化的方式来实现,作者非癫即痴。然而对于整个网络层来说,尤其是业务方使用的那部分,我倾向于提供离散型的API调用方式,并不建议在业务层的代码直接使用集约型的API调用方式。原因如下:

原因1:当前请求正在外面飞着的时候,根据不同的业务需求存在两种不同的请求起飞策略:一个是取消新发起的请求,等待外面飞着的请求着陆。另一个是取消外面飞着的请求,让新发起的请求起飞。集约化的API调用方式如果要满足这样的需求,那么每次要调用的时候都要多写一部分判断和取消的代码,手段就做不到很干净。

前者的业务场景举个例子就是刷新页面的请求,刷新详情,刷新列表等。后者的业务场景举个例子是列表多维度筛选,比如你先筛选了商品类型,然后筛选了价格区间。当然,后者的情况不一定每次筛选都要调用API,我们先假设这种筛选每次都必须要通过调用API才能获得数据。

如果是离散型的API调用,在编写不同的APIManager时候就可以针对不同的API设置不同的起飞策略,在实际使用的时候,就可以不必关心起飞策略了,因为APIMananger里面已经写好了。

原因2:便于针对某个API请求来进行AOP。在集约型的API调用方式下,如果要针对某个API请求的起飞和着陆过程进行AOP,这代码得写成什么样。。。噢,尼玛这画面太美别说看了,我都不敢想。

原因3:当API请求的着陆点消失时,离散型的API调用方式能够更加透明地处理这种情况。

当一个页面的请求正在天上飞的时候,用户等了好久不耐烦了,小手点了个back,然后ViewController被pop被回收。此时请求的着陆点就没了。这是很危险的情况,着陆点要是没了,就很容易crash的。一般来说处理这个情况都是在dealloc的时候取消当前页面所有的请求。如果是集约型的API调用,这个代码就要写到ViewController的dealloc里面,但如果是离散型的API调用,这个代码写到APIManager里面就可以了,然后随着ViewController的回收进程,APIManager也会被跟着回收,这部分代码就得到了调用的机会。这样业务方在使用的时候就可以不必关心着陆点消失的情况了,从而更加关注业务。

原因4:离散型的API调用方式能够最大程度地给业务方提供灵活性,比如reformer机制就是基于离散型的API调用方式的。另外,如果是针对提供翻页机制的API,APIManager就能简单地提供loadNextPage方法去加载下一页,页码的管理就不用业务方去管理了。还有就是,如果要针对业务请求参数进行验证,比如用户填写注册信息,在离散型的APIManager里面实现就会非常轻松。

综上,关于集约型的API调用和离散型的API调用,我倾向于这样:对外提供一个BaseAPIManager来给业务方做派生,在BaseManager里面采用集约化的手段组装请求,放飞请求,然而业务方调用API的时候,则是以离散的API调用方式来调用。如果你的App只提供了集约化的方式,而没有离散方式的通道,那么我建议你再封装一层,便于业务方使用离散的API调用方式来放飞请求。

怎么做APIManager的继承?

如果要做成离散型的API调用,那么使用继承是逃不掉的。BaseAPIManager里面负责集约化的部分,外部派生的XXXAPIManager负责离散的部分,对于BaseAPIManager来说,离散的部分有一些是必要的,比如API名字等,而我们派生的目的,也是为了提供这些数据。

我在这篇文章里面列举了种种继承的坏处,呼吁大家尽量不要使用继承。但是现在到了不得不用继承的时候,所以我得提醒一下大家别把继承用坏了。

在APIManager的情况下,我们最直觉的思路是BaseAPIManager提供一些空方法来给子类做重载,比如apiMethodName这样的函数,然而我的建议是,不要这么做。我们可以用IOP的方式来限制派生类的重载。

大概就是长这样:

BaseAPIManager的init方法里这么写:

// 注意是weak。 
@property (nonatomic, weak) id child;

(instancetype)init 
{ 
self = [super init]; 
if ([self confirmsToProtocol:@protocol(APIManager)]) { 
self.child = (id)self; 
} else { 
// 不遵守这个protocol的就让他crash,防止派生类乱来。 
NSAssert(NO, "子类必须要实现APIManager这个protocol。"); 
} 
return self; 
}

protocol这么写,把原本要重载的函数都定义在这个protocol里面,就不用在父类里面写空方法了:

@protocol APIManager

@required 
- (NSString *)apiMethodName; 
...

@end

然后在父类里面如果要使用的话,就这么写:

[self requestWithAPIName:[self.child apiMethodName] ......];

简单说就是在init的时候检查自己是否符合预先设计的子类的protocol,这就要求所有子类必须遵守这个protocol,所有针对父类的重载、覆盖也都以这个protocol为准,protocol以外的方法不允许重载、覆盖。而在父类的代码里,可以不必遵守这个protocol,保持了未来维护的灵活性。

这么做的好处就是避免了父类写空方法,同时也给子类带上了紧箍咒:要想当我的孩子,就要遵守这些规矩,不能乱来。业务方在实现子类的时候,就可以根据protocol中的方法去一一实现,然后约定就比较好做了:不允许重载父类方法,只允许选择实现或不实现protocol中的方法。

关于这个的具体的论述在这篇文章里面有,感兴趣的话可以看看。

网络层与业务层对接部分的小总结

这一节主要是讲了以下这些点:

  1. 使用delegate来做数据对接,仅在必要时采用Notification来做跨层访问
  2. 交付NSDictionary给业务层,使用Const字符串作为Key来保持可读性
  3. 提供reformer机制来处理网络层反馈的数据,这个机制很重要,好处极多
  4. 网络层上部分使用离散型设计,下部分使用集约型设计
  5. 设计合理的继承机制,让派生出来的APIManager受到限制,避免混乱
    ...

编后语

为了更好地向读者输出更优质的内容,InfoQ将精选来自国内外的优秀文章,经过整理审校后,发布到网站。本篇文章作者为田伟宇,原文链接为Casa Taloyum。本文已由原作者授权InfoQ中文站转载。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

1 by 杨 佳

1

还有下么? by 刘 德苏

还有下么?

英雄所见不同 by 易 壬俊

看完『然后我们顺便来说说为什么尽量不要用block』这里就看不下去了 ....

第一条,如果这里是个delegate回调,你也同样得step in才知道上层的具体逻辑;
第二条,如果你有一个VC实例发起某网络请求,并在请求回调block中使用到该VC实例,请使用Weak引用,这和delegate属性设置为weak是一样的,我知道你不会将delegate设置为strong,即使这种场景下没有循环引用。另外我一般会对从VC中发起的网络请求进行某种便捷的生命周期同步的封装,当该VC实例dismiss时,对应的网络请求会自动cancel。可以想象下 [[vc doLogin:^{ *** }] holdby:self] ;
第三条,使用delegate处理回调,往往会把连贯的业务逻辑形式上分散开,特别对于这种远程业务接口请求,如果通过delegate处理,假设发起业务请求的service或者controller会聚合几类远程业务(比如拉取轮转广告和拉取正文),这里你可能就要在delegate中if-else,并把这两类处理逻辑放到一起。另一层面来说,如果你要针对多个特有业务处理某些通用的逻辑,例如拉取广告和拉取正文都涉及到判断登录态,这个地方的设计好坏就不在于block or delegate了,而在分层思想,通常我认可的最佳实践是,设计interceptor机制,可以让你"拦截"某逻辑层面的block,进行额外的底层的block嵌套包装,在包装的时候加入判断登录态及相关处理的逻辑

总体而言作者对objc的闭包的理解还是不透彻的,作者的"View层的设计..."比这篇写的要好

关于"交付什么样的数据给业务层?" by 易 壬俊

耐着性子继续看,到看完"交付什么样的数据给业务层?"又得发表下意见了

作者阐述的实践相关内容,应该可以划分为两个话题,1是"是否需要强类型业务Model" 2是"类似ModelView的设计"

话题1,我认为对于上了一定规模的业务,应该引入强类型Model,作者应该考虑下前后端团队/iOS、Android、H5等多个团队在开发和使用数据接口,而这种协作问题中产生的纰漏是很容易被放大的,所以我们通过强类型Model来提供保持一致性的手段。很多这种跨平台方案,开源的也好,BAT等大公司自己山寨的也好,都会用DSL描述协议,再编译生成各个平台的Model层或者整个接口层代码。关于数据转换效率,其实这里作者有些曲解,并非是Data->Model的转换,而是JSON->Model的转换有没有必要,本身这个转换可以很轻量,因为属性其实可以不用copy,另外还可以设计lazy property,作者在后期通过KeyPath这种方式访问数据,如果频度够高反而会降低效率。再者刚才提到作者说的情况其实是Data->JSON->Model的转换,如果是使用Protobuf,就不会觉得是两次转换效率低了,反而比JSON这种要效率高。

话题2,作者设计的reformer,我感觉是综合了ModelView概念和作者在"View"一文中提过的面向接口编程概念,但reformer的定位有点奇怪,这个东西和View很靠近,这部分代码没有什么值得复用的地方(我一直反感视图层面的复用,这么简单多变的东西,复用什么鬼),另外就是实现reformer一点都不优雅,要不少用于访问的胶水代码,现在很多JSON->Model或者Data->Model的方案都喜欢使用反射把这些事情做了,模型代码干净利落

关于"集约型API调用方式和离散型API调用方式的选择" by 易 壬俊

这个基本观点一致,我赶着吃饭去就不多聊了

关于『请求的着陆点』那段,说生命周期问题的,有严重硬伤,这个叫『着陆点』的东西跟『集约/离散』方式的API调用其实没什么关系

另外这个作者叙述的『集约/离散』,其实还是分层思想,集约在下离散在上,在不存在设计过度的情况下,多一个层就多了很多想象空间,我喜欢这样

Re: 关于"交付什么样的数据给业务层?" by 易 壬俊

关于PB,虽然用了很久了,其实我是拒绝的

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

6 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT