iOS應用架構談(三):網絡層設計方案(上)

編者按:iOS客戶端應用架構看似簡單,但實際上要考慮的事情不少。本文作者將以系列文章的形式來討論iOS應用架構中的種種問題,本文是其中的第三篇,主要講網絡層設計以及安全機制和優化方案。

前言

網絡層在一個App中也是一個不可缺少的部分,工程師們在網絡層能夠發揮的空間也比較大。另外,蘋果對網絡請求部分已經做了很好的封裝,業界的AFNetworking也被廣泛使用。其它的ASIHttpRequest,MKNetworkKit啥的其實也都還不錯,但前者已經棄坑,后者也在棄坑的邊緣。在實際的App開發中,Afnetworking已經成為了事實上各大App的標準配置。

網絡層在一個App中承載了API調用,用戶操作日志記錄,甚至是即時通訊等任務。我接觸過一些App(開源的和不開源的)的代碼,在看到網絡層這一塊時,尤其是在看到各位架構師各顯神通展示了各種技巧,我非常為之感到興奮。但有的時候,往往也對于其中的一些缺陷感到失望。

關于網絡層的設計方案會有很多,需要權衡的地方也會有很多,甚至于爭議的地方都會有很多。但無論如何,我都不會對這些問題做出任何逃避,我會在這篇文章中給出我對它們的看法和解決方案,觀點絕不中立,不會跟大家打太極。

這篇文章就主要會講這些方面:

網絡層跟業務對接部分的設計

網絡層的安全機制實現

網絡層的優化方案

網絡層跟業務對接部分的設計

在安居客App的架構更新換代的時候,我深深地感覺到網絡層跟業務對接部分的設計有多么重要,因此我對它做的最大改變就是針對網絡層跟業務對接部分的改變。網絡層跟業務層對接部分設計的好壞,會直接影響到業務工程師實現功能時的心情。

在正式開始講設計之前,我們要先討論幾個問題:

使用哪種交互模式來跟業務層做對接?

是否有必要將API返回的數據封裝成對象然后再交付給業務層?

使用集約化調用方式還是離散型調用方式去調用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這樣的對象。這種做法是能夠提高后續操作代碼的可讀性的。在比較直覺的思路里面,是需要這部分轉化過程的,但這部分轉化過程的成本是很大的,主要成本在于:

數組內容的轉化成本較高:數組里面每項都要轉化成Item對象,如果Item對象中還有類似數組,就很頭疼。

轉化之后的數據在大部分情況是不能直接被展示的,為了能夠被展示,還需要第二次轉化。

只有在API返回的數據高度標準化時,這些對象原型(Item)的可復用程度才高,否則容易出現類型爆炸,提高維護成本。

調試時通過對象原型查看數據內容不如直接通過NSDictionary/NSArray直觀。

同一API的數據被不同View展示時,難以控制數據轉化的代碼,它們有可能會散落在任何需要的地方。

其實我們的理想情況是希望API的數據下發之后就能夠直接被View所展示。首先要說的是,這種情況非常少。另外,這種做法使得View和API聯系緊密,也是我們不希望發生的。

在設計安居客的網絡層數據交付這部分時,我添加了reformer(名字而已,叫什么都好)這個對象用于封裝數據轉化的邏輯,這個對象是一個獨立對象,事實上,它是作為Adaptor模式存在的。我們可以這么理解:想象一下我們洗澡時候使用的蓮蓬頭,水管里出來的水是API下發的原始數據。reformer就是蓮蓬頭上的不同水流擋板,需要什么模式,就撥到什么模式。

在實際使用時,代碼觀感是這樣的:

先定義一個protocol:@protocol ReformerProtocol- (NSDictionary)reformDataWithManager:(APIManager *)manager;@end在Controller里是這樣:@property (nonatomic, strong) id XXXReformer;@property (nonatomic, strong) id 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];

}

這一大段代碼看下來,我如果不說一下要點,那基本上就白寫了哈:

我們先看一下結構:

相關廠商內容

提高工程效率的各種最佳實踐和典型思路

知道創宇技術副總裁余弦將擔任QCon北京2016出品人

QCon北京2016大會,4月21-23日,與您相約北京國際會議中心,現在報名享8折優惠!

相關贊助商

QCon北京2016大會,4月21-23日,北京·國際會議中心,精彩內容邀您參與!

使用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中的方法。

關于這個的具體的論述在這篇文章里面有,感興趣的話可以看看。

網絡層與業務層對接部分的小總結

這一節主要是講了以下這些點:

使用delegate來做數據對接,僅在必要時采用Notification來做跨層訪問

交付NSDictionary給業務層,使用Const字符串作為Key來保持可讀性

提供reformer機制來處理網絡層反饋的數據,這個機制很重要,好處極多

網絡層上部分使用離散型設計,下部分使用集約型設計

設計合理的繼承機制,讓派生出來的APIManager受到限制,避免混亂

...

編后語

為了更好地向讀者輸出更優質的內容,InfoQ將精選來自國內外的優秀文章,經過整理審校后,發布到網站。本篇文章作者為田偉宇,原文鏈接為Casa Taloyum。本文已由原作者授權InfoQ中文站轉載。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容