[关闭]
@pockry 2015-06-17T11:04:47.000000Z 字数 15597 阅读 1499

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

移动 架构 iOS


编者按: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为辅。原因如下:

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

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

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

说得具象一点就是,我们考虑这样一种情况: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里面都做了些什么事情,这时候就比较蛋疼了。

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

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的回调着陆点不统一的情况。比如这样:

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

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

  1. [AFNetworkingAPI callApiWithParam:self.param successed:^(Response *response){
  2. if ([self.delegate respondsToSelector:@selector(successWithResponse:)]) {
  3. [self.delegate successedWithResponse:response];
  4. }
  5. } failed:^(Request *request, NSError *error){
  6. if ([self.delegate respondsToSelector:@selector(failedWithResponse:)]) {
  7. [self failedWithRequest:request error:error];
  8. }
  9. }];

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

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

尽可能通过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就是莲蓬头上的不同水流挡板,需要什么模式,就拨到什么模式。

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

  1. 先定义一个protocol
  2. @protocol ReformerProtocol <NSObject>
  3. - (NSDictionary)reformDataWithManager:(APIManager *)manager;
  4. @end
  5. Controller里是这样:
  6. @property (nonatomic, strong) id<ReformerProtocol> XXXReformer;
  7. @property (nonatomic, strong) id<ReformerProtocol> YYYReformer;
  8. #pragma mark - APIManagerDelegate
  9. - (void)apiManagerDidSuccess:(APIManager *)manager
  10. {
  11. NSDictionary *reformedXXXData = [manager fetchDataWithReformer:self.XXXReformer];
  12. [self.XXXView configWithData:reformedXXXData];
  13. NSDictionary *reformedYYYData = [manager fetchDataWithReformer:self.YYYReformer];
  14. [self.YYYView configWithData:reformedYYYData];
  15. }
  16. APIManager里面,fetchDataWithReformer是这样:
  17. - (NSDictionary)fetchDataWithReformer:(id<ReformerProtocol>)reformer
  18. {
  19. if (reformer == nil) {
  20. return self.rawData;
  21. } else {
  22. return [reformer reformDataWithManager:self];
  23. }
  24. }
  1. - (void)apiManagerDidSuccess:(APIManager *)manager
  2. {
  3. // 这个回调方法有可能是来自二手房列表APIManager的回调,也有可能是租房,也有可能是新房。但是在Controller层面我们不需要对它做额外区分,只要是同一个reformer出来的数据,我们就能保证是一定能被self.XXXView使用的。这样的保证由reformer的实现者来提供。
  4. NSDictionary *reformedXXXData = [manager fetchDataWithReformer:self.XXXReformer];
  5. [self.XXXView configWithData:reformedXXXData];
  6. }

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机制能够带来以下好处:

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

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

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

  1. PropertyListReformerKeys.h
  2. extern NSString * const kPropertyListDataKeyID;
  3. extern NSString * const kPropertyListDataKeyName;
  4. extern NSString * const kPropertyListDataKeyTitle;
  5. extern NSString * const kPropertyListDataKeyImage;
  6. PropertyListReformer.h
  7. #import "PropertyListReformerKeys.h"
  8. ... ...
  9. PropertyListReformer.m
  10. NSString * const kPropertyListDataKeyID = @"kPropertyListDataKeyID";
  11. NSString * const kPropertyListDataKeyName = @"kPropertyListDataKeyName";
  12. NSString * const kPropertyListDataKeyTitle = @"kPropertyListDataKeyTitle";
  13. NSString * const kPropertyListDataKeyImage = @"kPropertyListDataKeyImage";
  14. - (NSDictionary *)reformData:(NSDictionary *)originData fromManager:(APIManager *)manager
  15. {
  16. ... ...
  17. ... ...
  18. NSDictionary *resultData = nil;
  19. if ([manager isKindOfClass:[ZuFangListAPIManager class]]) {
  20. resultData = @{
  21. kPropertyListDataKeyID:originData[@"id"],
  22. kPropertyListDataKeyName:originData[@"name"],
  23. kPropertyListDataKeyTitle:originData[@"title"],
  24. kPropertyListDataKeyImage:[UIImage imageWithUrlString:originData[@"imageUrl"]]
  25. };
  26. }
  27. if ([manager isKindOfClass:[XinFangListAPIManager class]]) {
  28. resultData = @{
  29. kPropertyListDataKeyID:originData[@"xinfang_id"],
  30. kPropertyListDataKeyName:originData[@"xinfang_name"],
  31. kPropertyListDataKeyTitle:originData[@"xinfang_title"],
  32. kPropertyListDataKeyImage:[UIImage imageWithUrlString:originData[@"xinfang_imageUrl"]]
  33. };
  34. }
  35. if ([manager isKindOfClass:[ErShouFangListAPIManager class]]) {
  36. resultData = @{
  37. kPropertyListDataKeyID:originData[@"esf_id"],
  38. kPropertyListDataKeyName:originData[@"esf_name"],
  39. kPropertyListDataKeyTitle:originData[@"esf_title"],
  40. kPropertyListDataKeyImage:[UIImage imageWithUrlString:originData[@"esf_imageUrl"]]
  41. };
  42. }
  43. return resultData;
  44. }
  45. PropertListCell.m
  46. #import "PropertyListReformerKeys.h"
  47. - (void)configWithData:(NSDictionary *)data
  48. {
  49. self.imageView.image = data[kPropertyListDataKeyImage];
  50. self.idLabel.text = data[kPropertyListDataKeyID];
  51. self.nameLabel.text = data[kPropertyListDataKeyName];
  52. self.titleLabel.text = data[kPropertyListDataKeyTitle];
  53. }

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

我们先看一下结构:

使用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;

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中文站转载。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注