編者按:iOS客戶端應(yīng)用架構(gòu)看似簡單,但實際上要考慮的事情不少吹榴。本文作者將以系列文章的形式來討論iOS應(yīng)用架構(gòu)中的種種問題坏晦,本文是其中的第三篇倚喂,主要講網(wǎng)絡(luò)層設(shè)計以及安全機制和優(yōu)化方案。
前言
網(wǎng)絡(luò)層在一個App中也是一個不可缺少的部分杯瞻,工程師們在網(wǎng)絡(luò)層能夠發(fā)揮的空間也比較大镐牺。另外,蘋果對網(wǎng)絡(luò)請求部分已經(jīng)做了很好的封裝魁莉,業(yè)界的AFNetworking也被廣泛使用睬涧。其它的ASIHttpRequest卒废,MKNetworkKit啥的其實也都還不錯,但前者已經(jīng)棄坑宙地,后者也在棄坑的邊緣摔认。在實際的App開發(fā)中,Afnetworking已經(jīng)成為了事實上各大App的標(biāo)準(zhǔn)配置宅粥。
網(wǎng)絡(luò)層在一個App中承載了API調(diào)用参袱,用戶操作日志記錄,甚至是即時通訊等任務(wù)秽梅。我接觸過一些App(開源的和不開源的)的代碼抹蚀,在看到網(wǎng)絡(luò)層這一塊時,尤其是在看到各位架構(gòu)師各顯神通展示了各種技巧企垦,我非常為之感到興奮环壤。但有的時候,往往也對于其中的一些缺陷感到失望钞诡。
關(guān)于網(wǎng)絡(luò)層的設(shè)計方案會有很多郑现,需要權(quán)衡的地方也會有很多,甚至于爭議的地方都會有很多荧降。但無論如何接箫,我都不會對這些問題做出任何逃避,我會在這篇文章中給出我對它們的看法和解決方案朵诫,觀點絕不中立辛友,不會跟大家打太極。
這篇文章就主要會講這些方面:
網(wǎng)絡(luò)層跟業(yè)務(wù)對接部分的設(shè)計
網(wǎng)絡(luò)層的安全機制實現(xiàn)
網(wǎng)絡(luò)層的優(yōu)化方案
網(wǎng)絡(luò)層跟業(yè)務(wù)對接部分的設(shè)計
在安居客App的架構(gòu)更新?lián)Q代的時候剪返,我深深地感覺到網(wǎng)絡(luò)層跟業(yè)務(wù)對接部分的設(shè)計有多么重要废累,因此我對它做的最大改變就是針對網(wǎng)絡(luò)層跟業(yè)務(wù)對接部分的改變。網(wǎng)絡(luò)層跟業(yè)務(wù)層對接部分設(shè)計的好壞脱盲,會直接影響到業(yè)務(wù)工程師實現(xiàn)功能時的心情邑滨。
在正式開始講設(shè)計之前,我們要先討論幾個問題:
使用哪種交互模式來跟業(yè)務(wù)層做對接宾毒?
是否有必要將API返回的數(shù)據(jù)封裝成對象然后再交付給業(yè)務(wù)層驼修?
使用集約化調(diào)用方式還是離散型調(diào)用方式去調(diào)用API?
這些問題討論完畢之后诈铛,我會給出一個完整的設(shè)計方案來給大家做參考乙各,設(shè)計方案是魚,討論的這些問題是漁幢竹,我什么都授了耳峦,大家各取所需。
使用哪種交互模式來跟業(yè)務(wù)層做對接焕毫?
這里其實有兩個問題:一蹲坷,以什么方式將數(shù)據(jù)交付給業(yè)務(wù)層驶乾?二,交付什么樣的數(shù)據(jù)給業(yè)務(wù)層循签?
以什么方式將數(shù)據(jù)交付給業(yè)務(wù)層级乐?
iOS開發(fā)領(lǐng)域有很多對象間數(shù)據(jù)的傳遞方式,我看到的大多數(shù)App在網(wǎng)絡(luò)層所采用的方案主要集中于這三種:Delegate县匠,Notification风科,Block。KVO和Target-Action我目前還沒有看到有使用的乞旦。
目前我知道邊鋒主要是采用的block贼穆,大智慧主要采用的是Notification,安居客早期以Block為主兰粉,后面改成了以Delegate為主故痊,阿里沒發(fā)現(xiàn)有通過Notification來做數(shù)據(jù)傳遞的地方(可能有),Delegate玖姑、Block以及target-action都有愕秫,阿里iOS App網(wǎng)絡(luò)層的作者說這是為了方便業(yè)務(wù)層選擇自己合適的方法去使用。這里大家都是各顯神通客峭,每次我看到這部分的時候豫领,我都喜歡問作者為什么采用這種交互方案,但很少有作者能夠說出個條條框框來舔琅。
然而在我這邊,我的意見是以Delegate為主洲劣,Notification為輔备蚓。原因如下:
盡可能減少跨層數(shù)據(jù)交流的可能,限制耦合
統(tǒng)一回調(diào)方法囱稽,便于調(diào)試和維護
在跟業(yè)務(wù)層對接的部分只采用一種對接手段(在我這兒就是只采用delegate這一個手段)限制靈活性郊尝,以此來交換應(yīng)用的可維護性
盡可能減少跨層數(shù)據(jù)交流的可能,限制耦合
什么叫跨層數(shù)據(jù)交流战惊?就是某一層(或模塊)跟另外的與之沒有直接對接關(guān)系的層(或模塊)產(chǎn)生了數(shù)據(jù)交換流昏。為什么這種情況不好?嚴(yán)格來說應(yīng)該是大部分情況都不好吞获,有的時候跨層數(shù)據(jù)交流確實也是一種需求况凉。之所以說不好的地方在于,它會導(dǎo)致代碼混亂各拷,破壞模塊的封裝性刁绒。我們在做分層架構(gòu)的目的其中之一就在于下層對上層有一次抽象,讓上層可以不必關(guān)心下層細(xì)節(jié)而執(zhí)行自己的業(yè)務(wù)烤黍。
所以知市,如果下層細(xì)節(jié)被跨層暴露傻盟,一方面你很容易因此失去鄰層對這個暴露細(xì)節(jié)的保護;另一方面嫂丙,你又不可能不去處理這個細(xì)節(jié)娘赴,所以處理細(xì)節(jié)的相關(guān)代碼就會散落各地,最終難以維護跟啤。
說得具象一點就是筝闹,我們考慮這樣一種情況:A<-B<-C。當(dāng)C有什么事件腥光,通過某種方式告知B关顷,然后B執(zhí)行相應(yīng)的邏輯。一旦告知方式不合理武福,讓A有了跨層知道C的事件的可能议双,你 就很難保證A層業(yè)務(wù)工程師在將來不會對這個細(xì)節(jié)作處理。一旦業(yè)務(wù)工程師在A層產(chǎn)生處理操作捉片,有可能是補充邏輯平痰,也有可能是執(zhí)行業(yè)務(wù),那么這個細(xì)節(jié)的相關(guān)處理代碼就會有一部分散落在A層伍纫。然而前者是不應(yīng)該散落在A層的宗雇,后者有可能是需求。另外莹规,因為B層是對A層抽象的赔蒲,執(zhí)行補充邏輯的時候,有可能和B層針對這個事件的處理邏輯產(chǎn)生沖突良漱,這是我們很不希望看到的舞虱。
那么什么情況跨層數(shù)據(jù)交流會成為需求?在網(wǎng)絡(luò)層這邊母市,信號從2G變成3G變成4G變成Wi-Fi矾兜,這個是跨層數(shù)據(jù)交流的其中一個需求。不過其他的跨層數(shù)據(jù)交流需求我暫時也想不到了患久,哈哈椅寺,應(yīng)該也就這一個吧。
嚴(yán)格來說蒋失,使用Notification來進行網(wǎng)絡(luò)層和業(yè)務(wù)層之間數(shù)據(jù)的交換返帕,并不代表這一定就是跨層數(shù)據(jù)交流,但是使用Notification給跨層數(shù)據(jù)交流開了一道口子高镐,因為Notification的影響面不可控制溉旋,只要存在實例就存在被影響的可能。另外嫉髓,這也會導(dǎo)致誰都不能保證相關(guān)處理代碼就在唯一的那個地方观腊,進而帶來維護災(zāi)難邑闲。作為架構(gòu)師,在這里給業(yè)務(wù)工程師限制其操作的靈活性是必要的梧油。另外苫耸,Notification也支持一對多的情況,這也給代碼散落提供了條件儡陨。同時褪子,Notification所對應(yīng)的響應(yīng)方法很難在編譯層面作限制,不同的業(yè)務(wù)工程師會給他取不同的名字骗村,這也會給代碼的可維護性帶來災(zāi)難嫌褪。
手機淘寶架構(gòu)組的俠武同學(xué)曾經(jīng)給我分享過一個問題,在這里我也分享給大家:曾經(jīng)有一個工程師在監(jiān)聽Notification之后胚股,沒有寫釋放監(jiān)聽的代碼笼痛,當(dāng)然,找到這個原因又是很漫長的一段故事琅拌,現(xiàn)在找到原因了缨伊,然而監(jiān)聽這個Notification的對象有那么多,不知道具體是哪個Notificaiton进宝,也不知道那個沒釋放監(jiān)聽的對象是誰刻坊。后來折騰了很久大家都沒辦法的時候,有一個經(jīng)驗豐富的工程師提出用hook(Method Swizzling)的方式党晋,最終找到了那個沒釋放監(jiān)聽的對象谭胚,bug修復(fù)了。
我分享這個問題的目的并不是想強調(diào)Notification多么多么不好隶校,Notification本身就是一種設(shè)計模式漏益,在屬于它的問題領(lǐng)域內(nèi),Notification是非常好的一種解決方案深胳。但我想強調(diào)的是,對于網(wǎng)絡(luò)層這個問題領(lǐng)域內(nèi)來看铜犬,架構(gòu)師首先一定要限制代碼的影響范圍舞终,在能用影響范圍小的方案的時候就盡量采用這種小的方案,否則將來要是有什么奇怪需求或者出了什么小問題癣猾,維護起來就非常麻煩敛劝。因此Notification這個方案不能作為首選方案,只能作為備選纷宇。
那么Notification也不是完全不能使用夸盟,當(dāng)需求要求跨層時,我們就可以使用Notification像捶,比如前面提到的網(wǎng)絡(luò)條件切換上陕,而且這個需求也是需要滿足一對多的桩砰。
所以,為了符合前面所說的這些要求释簿,使用Delegate能夠很好地避免跨層訪問亚隅,同時限制了響應(yīng)代碼的形式,相比Notification而言有更好的可維護性庶溶。
然后我們順便來說說為什么盡量不要用block
1. block很難追蹤煮纵,難以維護
我們在調(diào)試的時候經(jīng)常會單步追蹤到某一個地方之后,發(fā)現(xiàn)尼瑪這里有個block偏螺,如果想知道這個block里面都做了些什么事情行疏,這時候就比較蛋疼了。
- (void)someFunctionWithBlock:(SomeBlock *)block
{
... ...
-> block();? //當(dāng)你單步走到這兒的時候套像,要想知道block里面都做了哪些事情的話酿联,就很麻煩。
... ...
}
2. block會延長相關(guān)對象的生命周期
block會給內(nèi)部所有的對象引用計數(shù)加一凉夯,這一方面會帶來潛在的retain cycle货葬,不過我們可以通過Weak Self的手段解決。另一方面比較重要就是劲够,它會延長對象的生命周期震桶。
在網(wǎng)絡(luò)回調(diào)中使用block,是block導(dǎo)致對象生命周期被延長的其中一個場合征绎,當(dāng)ViewController從window中卸下時蹲姐,如果尚有請求帶著block在外面飛,然后block里面引用了ViewController(這種場合非常常見)人柿,那么ViewController是不能被及時回收的柴墩,即便你已經(jīng)取消了請求,那也還是必須得等到請求著陸之后才能被回收凫岖。
然而使用delegate就不會有這樣的問題江咳,delegate是弱引用,哪怕請求仍然在外面飛哥放,歼指,ViewController還是能夠及時被回收的,回收之后指針自動被置為了nil甥雕,無傷大雅踩身。
所以平時盡量不要濫用block,尤其是在網(wǎng)絡(luò)層這里社露。
3. 統(tǒng)一回調(diào)方法挟阻,便于調(diào)試和維護
前面講的是跨層問題,區(qū)分了Delegate和Notification,順帶談了一下Block附鸽。然后現(xiàn)在談到的這個情況脱拼,就是另一個采用Block方案不是很合適的情況。首先拒炎,Block本身無好壞對錯之分挪拟,只有合適不合適。在這一節(jié)要講的情況里击你,Block無法做到回調(diào)方法的統(tǒng)一玉组,調(diào)試和維護的時候也很難在調(diào)用棧上顯示出來,找的時候會很蛋疼丁侄。
在網(wǎng)絡(luò)請求和網(wǎng)絡(luò)層接受請求的地方時惯雳,使用Block沒問題。但是在獲得數(shù)據(jù)交給業(yè)務(wù)方時鸿摇,最好還是通過Delegate去通知到業(yè)務(wù)方石景。因為Block所包含的回調(diào)代碼跟調(diào)用邏輯放在同一個地方,會導(dǎo)致那部分代碼變得很長拙吉,因為這里面包括了調(diào)用前和調(diào)用后的邏輯潮孽。從另一個角度說,這在一定程度上違背了single function筷黔,single task的原則往史,在需要調(diào)用API的地方,就只要寫API調(diào)用相關(guān)的代碼佛舱,在回調(diào)的地方椎例,寫回調(diào)的代碼。
然后我看到大部分App里请祖,當(dāng)業(yè)務(wù)工程師寫代碼寫到這邊的時候订歪,也意識到了這個問題。因此他們會在block里面寫個一句話的方法接收參數(shù)肆捕,然后做轉(zhuǎn)發(fā)刷晋,然后就可以把這個方法放在其他地方了,繞過了Block的回調(diào)著陸點不統(tǒng)一的情況慎陵。比如這樣:
[API callApiWithParam:param successed:^(Response *response){
[self successedWithResponse:response];
} failed:^(Request *request, NSError *error){
[self failedWithRequest:request error:error];
}];
這實質(zhì)上跟使用Delegate的手段沒有什么區(qū)別掏秩,只是繞了一下,不過還是沒有解決統(tǒng)一回調(diào)方法的問題荆姆,因為block里面寫的方法名字可能在不同的ViewController對象中都會不一樣,畢竟業(yè)務(wù)工程師也是很多人映凳,各人有各人的想法胆筒。所以架構(gòu)師在這邊不要貪圖方便,還是使用delegate的手段吧,業(yè)務(wù)工程師那邊就能不用那么繞了仆救。Block是目前大部分第三方網(wǎng)絡(luò)庫都采用的方式抒和,因為在發(fā)送請求的那一部分,使用Block能夠比較簡潔彤蔽,因此在請求那一層是沒有問題的摧莽,只是在交換數(shù)據(jù)之后,還是轉(zhuǎn)變成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];
}
}];
這樣在業(yè)務(wù)方這邊回調(diào)函數(shù)就能夠比較統(tǒng)一镊辕,便于維護。
綜上蚁袭,對于以什么方式將數(shù)據(jù)交付給業(yè)務(wù)層征懈?這個問題的回答是這樣:
盡可能通過Delegate的回調(diào)方式交付數(shù)據(jù),這樣可以避免不必要的跨層訪問揩悄。當(dāng)出現(xiàn)跨層訪問的需求時(比如信號類型切換)卖哎,通過Notification的方式交付數(shù)據(jù)。正常情況下應(yīng)該是避免使用Block的删性。
交付什么樣的數(shù)據(jù)給業(yè)務(wù)層亏娜?
我見過非常多的App的網(wǎng)絡(luò)層在拿到JSON數(shù)據(jù)之后,會將數(shù)據(jù)轉(zhuǎn)變成對應(yīng)的對象原型蹬挺。注意维贺,我這里指的不是NSDictionary,而是類似Item這樣的對象汗侵。這種做法是能夠提高后續(xù)操作代碼的可讀性的幸缕。在比較直覺的思路里面,是需要這部分轉(zhuǎn)化過程的晰韵,但這部分轉(zhuǎn)化過程的成本是很大的发乔,主要成本在于:
數(shù)組內(nèi)容的轉(zhuǎn)化成本較高:數(shù)組里面每項都要轉(zhuǎn)化成Item對象,如果Item對象中還有類似數(shù)組雪猪,就很頭疼栏尚。
轉(zhuǎn)化之后的數(shù)據(jù)在大部分情況是不能直接被展示的,為了能夠被展示只恨,還需要第二次轉(zhuǎn)化译仗。
只有在API返回的數(shù)據(jù)高度標(biāo)準(zhǔn)化時,這些對象原型(Item)的可復(fù)用程度才高官觅,否則容易出現(xiàn)類型爆炸纵菌,提高維護成本。
調(diào)試時通過對象原型查看數(shù)據(jù)內(nèi)容不如直接通過NSDictionary/NSArray直觀休涤。
同一API的數(shù)據(jù)被不同View展示時咱圆,難以控制數(shù)據(jù)轉(zhuǎn)化的代碼笛辟,它們有可能會散落在任何需要的地方。
其實我們的理想情況是希望API的數(shù)據(jù)下發(fā)之后就能夠直接被View所展示序苏。首先要說的是手幢,這種情況非常少。另外忱详,這種做法使得View和API聯(lián)系緊密围来,也是我們不希望發(fā)生的。
在設(shè)計安居客的網(wǎng)絡(luò)層數(shù)據(jù)交付這部分時匈睁,我添加了reformer(名字而已监透,叫什么都好)這個對象用于封裝數(shù)據(jù)轉(zhuǎn)化的邏輯,這個對象是一個獨立對象软舌,事實上才漆,它是作為Adaptor模式存在的。我們可以這么理解:想象一下我們洗澡時候使用的蓮蓬頭佛点,水管里出來的水是API下發(fā)的原始數(shù)據(jù)醇滥。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的原始數(shù)據(jù)(JSON對象)由Manager實例保管米碰,reformer方法里面取Manager的原始數(shù)據(jù)(manager.rawData)做轉(zhuǎn)換窝革,然后交付出去。蓮蓬頭的水管部分是Manager吕座,負(fù)責(zé)提供原始水流(數(shù)據(jù)流)虐译,reformer就是不同的模式,換什么reformer就能出來什么水流吴趴。
要點3:例子中舉的場景是一個API數(shù)據(jù)被多個View使用的情況漆诽,體現(xiàn)了reformer的一個特點:可以根據(jù)需要改變同一數(shù)據(jù)來源的展示方式。比如API數(shù)據(jù)展示的是“附近的小區(qū)”锣枝,那么這個數(shù)據(jù)可以被列表(XXXView)和地圖(YYYView)共用厢拭,不同的view使用的數(shù)據(jù)的轉(zhuǎn)化方式不一樣,這就通過不同的reformer解決了撇叁。
要點4:在一個view用來同一展示不同API數(shù)據(jù)的情況供鸠,reformer是絕佳利器。比如安居客的列表view的數(shù)據(jù)來源可能有三個:二手房列表API陨闹,租房列表API回季,新房列表API家制。這些API返回來的數(shù)據(jù)的value可能一致,但是key都是不一致的泡一。這時候就可以通過同一個reformer來做數(shù)據(jù)的標(biāo)準(zhǔn)化輸出,這樣就使得view代碼復(fù)用成為可能觅廓。這體現(xiàn)了reformer另外一個特點:同一個reformer出來的數(shù)據(jù)是高度標(biāo)準(zhǔn)化的鼻忠。形象點說就是:只要蓮蓬頭不換撑教,哪怕水管的水變成海水或者污水了沙郭,也依舊能夠輸出符合洗澡要求的淡水水流黎泣。舉個例子:
- (void)apiManagerDidSuccess:(APIManager *)manager
{
// 這個回調(diào)方法有可能是來自二手房列表APIManager的回調(diào)力细,也有可能是
租房孵稽,也有可能是新房锋玲。但是在Controller層面我們不需要對它做額外區(qū)分翁涤,
只要是同一個reformer出來的數(shù)據(jù)形庭,我們就能保證是一定能被self.XXXView使
用的劫侧。這樣的保證由reformer的實現(xiàn)者來提供埋酬。
NSDictionary *reformedXXXData = [manager
fetchDataWithReformer:self.XXXReformer];
[self.XXXView configWithData:reformedXXXData];
}
要點5:有沒有發(fā)現(xiàn),使用reformer之后烧栋,Controller的代碼簡潔了很多写妥?而且,數(shù)據(jù)原型在這種情況下就沒有必要存在了审姓,隨之而來的成本也就被我們繞過了珍特。
reformer本質(zhì)上就是一個符合某個protocol的對象,在controller需要從api manager中獲得數(shù)據(jù)的時候魔吐,順便把reformer傳進去扎筒,于是就能獲得經(jīng)過reformer重新洗過的數(shù)據(jù),然后就可以直接使用了酬姆。
更抽象地說嗜桌,reformer其實是對數(shù)據(jù)轉(zhuǎn)化邏輯的一個封裝。在controller從manager中取數(shù)據(jù)之后轴踱,并且把數(shù)據(jù)交給view之前症脂,這期間或多或少都是要做一次數(shù)據(jù)轉(zhuǎn)化的,有的時候不同的view淫僻,對應(yīng)的轉(zhuǎn)化邏輯還不一樣诱篷,但是展示的數(shù)據(jù)是一樣的。而且往往這一部分代碼都非常復(fù)雜雳灵,且跟業(yè)務(wù)強相關(guān)棕所,直接上代碼,將來就會很難維護悯辙。所以我們可以考慮采用不同的reformer封裝不同的轉(zhuǎn)化邏輯琳省,然后讓controller根據(jù)需要選擇一個合適的reformer裝上迎吵,就像洗澡的蓮蓬頭,需要什么樣的水流(數(shù)據(jù)的表現(xiàn)形式)就換什么樣的頭针贬,然而水(數(shù)據(jù))都是一樣的击费。這種做法能夠大大提高代碼的可維護性,以及減少ViewController的體積桦他。
總結(jié)一下蔫巩,reformer事實上是把轉(zhuǎn)化的代碼封裝之后再從主體業(yè)務(wù)中拆分了出來,拆分出來之后不光降低了原有業(yè)務(wù)的復(fù)雜度快压,更重要的是圆仔,它提高了數(shù)據(jù)交付的靈活性。另外蔫劣,由于Controller負(fù)責(zé)調(diào)度Manager和View坪郭,因此它是知道Manager和View之間的關(guān)系的,Controller知道了這個關(guān)系之后脉幢,就有了充要條件來為不同的View選擇不同的Reformer歪沃,并用這個Reformer去改造Mananger的數(shù)據(jù),然后ViewController獲得了經(jīng)過reformer處理過的數(shù)據(jù)之后鸵隧,就可以直接交付給view去使用绸罗。Controller因此得到瘦身,負(fù)責(zé)業(yè)務(wù)數(shù)據(jù)轉(zhuǎn)化的這部分代碼也不用寫在Controller里面豆瘫,提高了可維護性珊蟀。
所以reformer機制能夠帶來以下好處:
好處1:繞開了API數(shù)據(jù)原型的轉(zhuǎn)換,避免了相關(guān)成本外驱。
好處2:在處理單View對多API育灸,以及在單API對多View的情況時,reformer提供了非常優(yōu)雅的手段來響應(yīng)這種需求昵宇,隔離了轉(zhuǎn)化邏輯和主體業(yè)務(wù)邏輯磅崭,避免了維護災(zāi)難。
好處3:轉(zhuǎn)化邏輯集中瓦哎,且將轉(zhuǎn)化次數(shù)轉(zhuǎn)為只有一次砸喻。使用數(shù)據(jù)原型的轉(zhuǎn)化邏輯至少有兩次,第一次是把JSON映射成對應(yīng)的原型蒋譬,第二次是把原型轉(zhuǎn)變成能被View處理的數(shù)據(jù)割岛。reformer一步到位。另外犯助,轉(zhuǎn)化邏輯在reformer里面癣漆,將來如果API數(shù)據(jù)有變,就只要去找到對應(yīng)reformer然后改掉就好了剂买。
好處4:Controller因此可以省去非常多的代碼惠爽,降低了代碼復(fù)雜度癌蓖,同時提高了靈活性,任何時候切換reformer而不必切換業(yè)務(wù)邏輯就可以應(yīng)對不同View對數(shù)據(jù)的需要婚肆。
好處5:業(yè)務(wù)數(shù)據(jù)和業(yè)務(wù)有了適當(dāng)?shù)母綦x租副。這么做的話,將來如果業(yè)務(wù)邏輯有修改旬痹,換一個reformer就好了附井。如果其他業(yè)務(wù)也有相同的數(shù)據(jù)轉(zhuǎn)化邏輯,其他業(yè)務(wù)直接拿這個reformer就可以用了两残,不用重寫。另外把跨,如果controller有修改(比如UI交互方式改變)人弓,可以放心換controller,完全不用擔(dān)心業(yè)務(wù)數(shù)據(jù)的處理着逐。
在不使用特定對象表征數(shù)據(jù)的情況下崔赌,如何保持?jǐn)?shù)據(jù)可讀性?
不使用對象來表征數(shù)據(jù)的時候耸别,事實上就是使用NSDictionary的時候健芭。事實上,這個問題就是秀姐,如何在NSDictionary表征數(shù)據(jù)的情況下保持良好的可讀性慈迈?
蘋果已經(jīng)給出了非常好的做法,用固定字符串做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];
}
這一大段代碼看下來,我如果不說一下要點舷蟀,那基本上就白寫了哈:
我們先看一下結(jié)構(gòu):
相關(guān)廠商內(nèi)容
知道創(chuàng)宇技術(shù)副總裁余弦將擔(dān)任QCon北京2016出品人
QCon北京2016大會恤磷,4月21-23日,與您相約北京國際會議中心野宜,現(xiàn)在報名享8折優(yōu)惠扫步!
相關(guān)贊助商
QCon北京2016大會,4月21-23日速缨,北京·國際會議中心锌妻,精彩內(nèi)容邀您參與!
使用Const字符串來表征Key旬牲,字符串的定義跟著reformer的實現(xiàn)文件走仿粹,字符串的extern聲明放在獨立的頭文件內(nèi)搁吓。
這樣reformer生成的數(shù)據(jù)的key都使用Const字符串來表示,然后每次別的地方需要使用相關(guān)數(shù)據(jù)的時候吭历,把PropertyListReformerKeys.h這個頭文件import進去就好了堕仔。
另外要注意的一點是,如果一個OriginData可能會被多個Reformer去處理的話晌区,Key的命名規(guī)范需要能夠表征出其對應(yīng)的reformer名字摩骨。如果reformer是PropertyListReformer,那么Key的名字就是PropertyListKeyXXXX朗若。
這么做的好處就是恼五,將來遷移的時候相當(dāng)方便,只要扔頭文件就可以了哭懈,只扔頭文件是不會導(dǎo)致拔出蘿卜帶出泥的情況的灾馒。而且也避免了自定義對象帶來的額外代碼體積。
另外遣总,關(guān)于交付的NSDictionary睬罗,其實具體還是看view的需求,reformer的設(shè)計初衷是:通過reformer轉(zhuǎn)化出來的可以直接是View旭斥,或者是view直接可以使用的對象(包括NSDictionary)容达。比如地圖標(biāo)點列表API的數(shù)據(jù),通過reformer轉(zhuǎn)化之后就可以直接變成MKAnnotation垂券,然后MKMapView就可以直接使用了花盐。這里說的只是當(dāng)你的需求是交付NSDictionary時,如何保證可讀性的情況圆米,再強調(diào)一下哈卒暂,reformer交付的是view直接可以使用的對象,交付出去的可以是NSDictionary娄帖,也可以是UIView也祠,跟DataSource結(jié)合之后交付的甚至可以是UITableViewCell/UICollectionViewCell。不要被NSDictionary或所謂的轉(zhuǎn)化成model再交付的思想局限近速。
綜上诈嘿,我對交付什么樣的數(shù)據(jù)給業(yè)務(wù)層?這個問題的回答就是這樣:
對于業(yè)務(wù)層而言削葱,由Controller根據(jù)View和APIManager之間的關(guān)系奖亚,選擇合適的reformer將View可以直接使用的數(shù)據(jù)(甚至reformer可以用來直接生成view)轉(zhuǎn)化好之后交付給View。對于網(wǎng)絡(luò)層而言析砸,只需要保持住原始數(shù)據(jù)即可昔字,不需要主動轉(zhuǎn)化成數(shù)據(jù)原型。然后數(shù)據(jù)采用NSDictionary加Const字符串key來表征,避免了使用對象來表征帶來的遷移困難作郭,同時不失去可讀性陨囊。
集約型API調(diào)用方式和離散型API調(diào)用方式的選擇?
集約型API調(diào)用其實就是所有API的調(diào)用只有一個類夹攒,然后這個類接收API名字蜘醋,API參數(shù),以及回調(diào)著陸點(可以是target-action咏尝,或者block压语,或者delegate等各種模式的著陸點)作為參數(shù)。然后執(zhí)行類似startRequest這樣的方法编检,它就會去根據(jù)這些參數(shù)起飛去調(diào)用API了胎食,然后獲得API數(shù)據(jù)之后再根據(jù)指定的著陸點去著陸。比如這樣:
集約型API調(diào)用方式:
[APIRequest startRequestWithApiName:@"itemList.v1" params:params
success:@selector(success:) fail:@selector(fail:) target:self];
離散型API調(diào)用是這樣的允懂,一個API對應(yīng)于一個APIManager斥季,然后這個APIManager只需要提供參數(shù)就能起飛,API名字累驮、著陸方式都已經(jīng)集成入APIManager中。比如這樣:
離散型API調(diào)用方式:
@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調(diào)用和離散型API調(diào)用這兩者實現(xiàn)方案不是互斥的舵揭,單看下層谤专,大家都是集約型。因為發(fā)起一個API請求之后午绳,除去業(yè)務(wù)相關(guān)的部分(比如參數(shù)和API名字等)置侍,剩下的都是要統(tǒng)一處理的:加密,URL拼接拦焚,API請求的起飛和著陸蜡坊,這些處理如果不用集約化的方式來實現(xiàn),作者非癲即癡赎败。然而對于整個網(wǎng)絡(luò)層來說秕衙,尤其是業(yè)務(wù)方使用的那部分,我傾向于提供離散型的API調(diào)用方式僵刮,并不建議在業(yè)務(wù)層的代碼直接使用集約型的API調(diào)用方式据忘。原因如下:
原因1:當(dāng)前請求正在外面飛著的時候,根據(jù)不同的業(yè)務(wù)需求存在兩種不同的請求起飛策略:一個是取消新發(fā)起的請求搞糕,等待外面飛著的請求著陸勇吊。另一個是取消外面飛著的請求,讓新發(fā)起的請求起飛窍仰。集約化的API調(diào)用方式如果要滿足這樣的需求汉规,那么每次要調(diào)用的時候都要多寫一部分判斷和取消的代碼,手段就做不到很干凈驹吮。
前者的業(yè)務(wù)場景舉個例子就是刷新頁面的請求针史,刷新詳情晶伦,刷新列表等。后者的業(yè)務(wù)場景舉個例子是列表多維度篩選悟民,比如你先篩選了商品類型坝辫,然后篩選了價格區(qū)間。當(dāng)然射亏,后者的情況不一定每次篩選都要調(diào)用API近忙,我們先假設(shè)這種篩選每次都必須要通過調(diào)用API才能獲得數(shù)據(jù)。
如果是離散型的API調(diào)用智润,在編寫不同的APIManager時候就可以針對不同的API設(shè)置不同的起飛策略及舍,在實際使用的時候,就可以不必關(guān)心起飛策略了窟绷,因為APIMananger里面已經(jīng)寫好了锯玛。
原因2:便于針對某個API請求來進行AOP。在集約型的API調(diào)用方式下兼蜈,如果要針對某個API請求的起飛和著陸過程進行AOP攘残,這代碼得寫成什么樣。为狸。歼郭。噢,尼瑪這畫面太美別說看了辐棒,我都不敢想病曾。
原因3:當(dāng)API請求的著陸點消失時,離散型的API調(diào)用方式能夠更加透明地處理這種情況漾根。
當(dāng)一個頁面的請求正在天上飛的時候泰涂,用戶等了好久不耐煩了,小手點了個back辐怕,然后ViewController被pop被回收逼蒙。此時請求的著陸點就沒了。這是很危險的情況秘蛇,著陸點要是沒了其做,就很容易crash的。一般來說處理這個情況都是在dealloc的時候取消當(dāng)前頁面所有的請求赁还。如果是集約型的API調(diào)用妖泄,這個代碼就要寫到ViewController的dealloc里面,但如果是離散型的API調(diào)用艘策,這個代碼寫到APIManager里面就可以了蹈胡,然后隨著ViewController的回收進程,APIManager也會被跟著回收,這部分代碼就得到了調(diào)用的機會罚渐。這樣業(yè)務(wù)方在使用的時候就可以不必關(guān)心著陸點消失的情況了却汉,從而更加關(guān)注業(yè)務(wù)。
原因4:離散型的API調(diào)用方式能夠最大程度地給業(yè)務(wù)方提供靈活性荷并,比如reformer機制就是基于離散型的API調(diào)用方式的合砂。另外,如果是針對提供翻頁機制的API源织,APIManager就能簡單地提供loadNextPage方法去加載下一頁翩伪,頁碼的管理就不用業(yè)務(wù)方去管理了。還有就是谈息,如果要針對業(yè)務(wù)請求參數(shù)進行驗證缘屹,比如用戶填寫注冊信息,在離散型的APIManager里面實現(xiàn)就會非常輕松侠仇。
綜上轻姿,關(guān)于集約型的API調(diào)用和離散型的API調(diào)用,我傾向于這樣:對外提供一個BaseAPIManager來給業(yè)務(wù)方做派生逻炊,在BaseManager里面采用集約化的手段組裝請求互亮,放飛請求,然而業(yè)務(wù)方調(diào)用API的時候余素,則是以離散的API調(diào)用方式來調(diào)用胳挎。如果你的App只提供了集約化的方式,而沒有離散方式的通道溺森,那么我建議你再封裝一層,便于業(yè)務(wù)方使用離散的API調(diào)用方式來放飛請求窑眯。
怎么做APIManager的繼承屏积?
如果要做成離散型的API調(diào)用,那么使用繼承是逃不掉的磅甩。BaseAPIManager里面負(fù)責(zé)集約化的部分炊林,外部派生的XXXAPIManager負(fù)責(zé)離散的部分,對于BaseAPIManager來說卷要,離散的部分有一些是必要的渣聚,比如API名字等,而我們派生的目的僧叉,也是為了提供這些數(shù)據(jù)奕枝。
我在這篇文章里面列舉了種種繼承的壞處,呼吁大家盡量不要使用繼承瓶堕。但是現(xiàn)在到了不得不用繼承的時候隘道,所以我得提醒一下大家別把繼承用壞了。
在APIManager的情況下,我們最直覺的思路是BaseAPIManager提供一些空方法來給子類做重載谭梗,比如apiMethodName這樣的函數(shù)忘晤,然而我的建議是,不要這么做激捏。我們可以用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, "子類必須要實現(xiàn)APIManager這個protocol。");
}
return self;
}
protocol這么寫表谊,把原本要重載的函數(shù)都定義在這個protocol里面钞护,就不用在父類里面寫空方法了:
@protocol APIManager
@required
- (NSString *)apiMethodName;
...
@end
然后在父類里面如果要使用的話,就這么寫:
[self requestWithAPIName:[self.child apiMethodName] ......];
簡單說就是在init的時候檢查自己是否符合預(yù)先設(shè)計的子類的protocol爆办,這就要求所有子類必須遵守這個protocol难咕,所有針對父類的重載、覆蓋也都以這個protocol為準(zhǔn)距辆,protocol以外的方法不允許重載余佃、覆蓋。而在父類的代碼里跨算,可以不必遵守這個protocol爆土,保持了未來維護的靈活性。
這么做的好處就是避免了父類寫空方法诸蚕,同時也給子類帶上了緊箍咒:要想當(dāng)我的孩子步势,就要遵守這些規(guī)矩,不能亂來背犯。業(yè)務(wù)方在實現(xiàn)子類的時候坏瘩,就可以根據(jù)protocol中的方法去一一實現(xiàn),然后約定就比較好做了:不允許重載父類方法漠魏,只允許選擇實現(xiàn)或不實現(xiàn)protocol中的方法倔矾。
關(guān)于這個的具體的論述在這篇文章里面有,感興趣的話可以看看柱锹。
網(wǎng)絡(luò)層與業(yè)務(wù)層對接部分的小總結(jié)
這一節(jié)主要是講了以下這些點:
使用delegate來做數(shù)據(jù)對接哪自,僅在必要時采用Notification來做跨層訪問
交付NSDictionary給業(yè)務(wù)層,使用Const字符串作為Key來保持可讀性
提供reformer機制來處理網(wǎng)絡(luò)層反饋的數(shù)據(jù)禁熏,這個機制很重要壤巷,好處極多
網(wǎng)絡(luò)層上部分使用離散型設(shè)計,下部分使用集約型設(shè)計
設(shè)計合理的繼承機制瞧毙,讓派生出來的APIManager受到限制隙笆,避免混亂
...
編后語
為了更好地向讀者輸出更優(yōu)質(zhì)的內(nèi)容锌蓄,InfoQ將精選來自國內(nèi)外的優(yōu)秀文章,經(jīng)過整理審校后撑柔,發(fā)布到網(wǎng)站瘸爽。本篇文章作者為田偉宇,原文鏈接為Casa Taloyum铅忿。本文已由原作者授權(quán)InfoQ中文站轉(zhuǎn)載剪决。