聲明:本文中的solution是我們iOS team的集體智慧結(jié)晶,并非我一個(gè)人的獨(dú)有成果,在此感謝整個(gè)團(tuán)隊(duì)的支持和幫助。轉(zhuǎn)載請(qǐng)注明出處。
前言
應(yīng)用設(shè)計(jì)模式的概念隨著iOS和Android的流行,被討論得越來(lái)越多,MVC之于iOS,已經(jīng)像當(dāng)年OO之于C++/Java一樣,隨口就被提到爛大街的程度。但是實(shí)際上MVC的概念并不是Apple最先提出的,更不是iOS專有。只不過(guò)Apple對(duì)傳統(tǒng)的MVC進(jìn)行了改進(jìn),使其更加適合iOS App的開發(fā)。同理,既然MVC是一個(gè)具有深遠(yuǎn)歷史的模型,隨著時(shí)間的推進(jìn)和各種新的需求的提出,其本身也會(huì)不斷地被改進(jìn)發(fā)展,出現(xiàn)了MVP,MVVM,MVA,MVCS,甚至還有VIPER……各種MV“X”模式。之所以會(huì)出現(xiàn)形形色色的MVX,其實(shí)最核心的還是因?yàn)镸VC中的C——職責(zé)太大太重以至于不堪重負(fù):開發(fā)的人不堪重負(fù)地寫著復(fù)雜又沒(méi)有技術(shù)含量的代碼,維護(hù)的人不堪重負(fù)地去翻閱動(dòng)輒長(zhǎng)達(dá)數(shù)千行的代碼,測(cè)試的人更是不堪重負(fù)地對(duì)著和UI及業(yè)務(wù)重度綁定難以自動(dòng)化測(cè)試的單元。也就是所謂的 ** Massive Controller **問(wèn)題, 這是推動(dòng)MVC向前進(jìn)的本源。但是就像這篇《iOS應(yīng)用架構(gòu)談 view層的組織和調(diào)用方案》中說(shuō)的,不管這些MVX怎么設(shè)計(jì),都離不開MVC這個(gè)根基,天下終歸還是MVC的天下。
那么,面對(duì)這么多的MVX,在做架構(gòu)設(shè)計(jì)時(shí)應(yīng)該如何選擇是一件即頭疼又簡(jiǎn)單的問(wèn)題:頭疼是你要做出選擇,對(duì)于我這種有選擇綜合癥的人來(lái)說(shuō),顯然是很痛苦的一件事;但是它其實(shí)又是一件很簡(jiǎn)單的事,在不知道如何選擇時(shí),總是選擇自己最熟悉的方式自然是風(fēng)險(xiǎn)最小的。當(dāng)然,熟悉的模式越來(lái)越多,可供選擇的也越來(lái)越多,這個(gè)時(shí)候,清晰的了解每種模式的優(yōu)缺點(diǎn),然后對(duì)比項(xiàng)目的實(shí)際規(guī)模和情景,去找“最合適”的而不是“最好”的模式。
各種MVX
網(wǎng)上介紹各種MVX的文章數(shù)不勝數(shù),可以參閱本文最后的參考鏈接,這里,做為筆記,把幾種常見(jiàn)的MVX一一列出來(lái),做一個(gè)簡(jiǎn)單的描述。
我都仿佛聽(tīng)到了那句熟悉的臺(tái)詞:“樓上的MVX們,出來(lái)接客了~”
-
傳統(tǒng)的MVC
MVC -
Apple的MVC
MVC-Apple -
MVP
MVP -
MVVM
MVVM MVA
Model–View–Adapter模式。這是一種比較少見(jiàn)的模式,可以參考Wiki上的解釋Model–view–adapter。其核心就是阻斷View和Modal的交互,MVA三者是線性的溝通關(guān)系,而非傳統(tǒng)MVC的三角關(guān)系。從這個(gè)概念上講和上述幾種模式非常相似,可能這就是為什么這個(gè)概念已經(jīng)很少有人提到,因?yàn)槟鼙环Q作MVA的模式,可能都在上述幾種方案中了。-
VIPER
VIPER
事實(shí)上我個(gè)人對(duì)VIPER這個(gè)結(jié)構(gòu)很感興趣,但是的確像參考文中提到的,靈活的代價(jià)就是復(fù)雜。這種架構(gòu)非常像“樂(lè)高”玩具,給您提供了最大化的自由度,但是即使為了構(gòu)建簡(jiǎn)單的App你仍然需要用一堆的小零件才能組裝起來(lái)。我自己有把本文中我們的架構(gòu)用VIPER的思想重新寫了一個(gè)測(cè)試樣例,代碼量確實(shí)要增加50%左右。但是思路上(包括代碼組織結(jié)構(gòu)上)可能比現(xiàn)有的設(shè)計(jì)模式更加清晰。
我們是怎么做那個(gè)“X”的
-
最初架構(gòu)設(shè)計(jì)
其實(shí)我并沒(méi)有一上來(lái)就奔著某個(gè)特殊的MVX去設(shè)計(jì),因?yàn)樵诔跗诩軜?gòu)階段,沒(méi)有太多可以參考的實(shí)際業(yè)務(wù)邏輯,只有一些基本的需求,所以一開始的時(shí)候,基于功能部件之間的關(guān)系,很自然的進(jìn)行了一個(gè)基本的分層設(shè)計(jì)。V和C兩層也是各司其職,但是對(duì)M層,進(jìn)行了特殊的設(shè)計(jì),封裝一層獨(dú)立的ModalLayer。由于所有熱數(shù)據(jù)均來(lái)自服務(wù)器端,必須要有一個(gè)和服務(wù)器端打直接交道的NetworkManager用以處理所有的網(wǎng)絡(luò)請(qǐng)求和響應(yīng),一部分冷數(shù)據(jù)需要進(jìn)行本地緩存用以離線展示,所以單獨(dú)設(shè)計(jì)一個(gè)CacheManager用來(lái)橋接。在這個(gè)階段DataManager的本意是將NetworkManager的接口做一定程度的封裝,將顯式的HTTP操作轉(zhuǎn)換成標(biāo)準(zhǔn)的CRUD操作接口,然后向Conroller層提供統(tǒng)一的服務(wù)。同時(shí),負(fù)責(zé)根據(jù)對(duì)應(yīng)的Cache Policy在Cache和Network之間進(jìn)行切換。說(shuō)白了,它就是Modal操作接口的一個(gè)Wrapper類。

其中,CacheManager這一環(huán)在這個(gè)初期設(shè)計(jì)中我們使用的是Core Data,DataManager負(fù)責(zé)管理Core Data的Modal Entity,于是順理成章的接管了Modal這一層的操作。因此這里的設(shè)計(jì)其實(shí)還是一個(gè)標(biāo)準(zhǔn)的MVC模式,只是在M層增加了一個(gè)Network Helper(ACNetworkManager)和一個(gè)Wrapper(ACCoreDataManager)來(lái)使得網(wǎng)絡(luò)操作更加方便。

-
最終實(shí)現(xiàn)方案
在實(shí)際的實(shí)現(xiàn)過(guò)程中,隨著業(yè)務(wù)邏輯的不斷提煉和解耦,上述架構(gòu)設(shè)計(jì)慢慢演變成這樣:

可以看到最明顯的區(qū)別在ModalLayer這一層:
- 原來(lái)的DataManager分裂成了一個(gè)BaseModalManager基類和一系列ModalManager子類;
- 每一個(gè)ModalManager子類對(duì)應(yīng)于自己的Modal Entity,
- 每一個(gè)ModalManager子類對(duì)應(yīng)于一組View和Controller來(lái)完成一組特定的業(yè)務(wù)邏輯(多數(shù)是以頁(yè)面為單位,同一個(gè)頁(yè)面的邏輯會(huì)使用一個(gè)或多個(gè)ModalManager)。
- 這些ModalManager全部通過(guò)父類(基類)BaseModalManager和CacheManager以及NetworkManager通信,子類則完全根據(jù)實(shí)際業(yè)務(wù)進(jìn)行定制構(gòu)造。
我們來(lái)看下具體示例:
-
NetworkManager
@interface HTTPNetWorkManage + (instancetype)sharedManage; - (HTTPRequestTask *)HTTPRequest:(HTTPMethodType)HTTPMethod URLString:(NSString *)URLString parameters:(id)parameters data:(NSData*)partFormData success:(void (^)(id task, id responseObject))success failure:(void (^)(id task, NSError *error))failure; @end -
BaseModal
在這里,我們?cè)诘诙娴膶?shí)現(xiàn)中Modal層的Entity和Cache都拋棄了Core Data,而讓BaseModal Entity直接繼承自Mantle,這樣直接核心就是modalOfClass接口,用于將JSON數(shù)據(jù)自動(dòng)轉(zhuǎn)換成Modal類:@interface ACBaseModel : MTLModel<MTLJSONSerializing> + (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error; @end
關(guān)于為什么放棄Core Data而使用Mantle,可以參考我的另一篇文章《從Core Data到Mantle》。后來(lái)我才發(fā)現(xiàn)我們并不是唯一的一個(gè)有過(guò)這樣的經(jīng)歷的團(tuán)隊(duì),這里有一篇文章《為什么唱吧iOS 6.0選擇了Mantle》,可以給大家另一個(gè)直觀的感受。
- BaseModalManager
首先看ModalManager的基類方法設(shè)定:
/* BaseModalManager Delegate Protocol
*/
@protocol BaseModelManageDelegateProtcol <NSObject>
@optional
...
- (void)manager:(BaseModelManage *)manager api:(NSString*)api identifier:(NSString*)identifier didSendWithData:(id)data;
- (void)manager:(BaseModelManage *)manager api:(NSString*)api identifier:(NSString*)identifier didFailRequestWithError:(NSError *)error;
@end
@interface BaseModelManage : NSObject
- (instancetype)initWithDelegate:(id<BaseModelManageDelegateProtcol>)delegate;
//Common Request API, use fetchData/fetchMoreData for GET, uploadData for attechment POST
- (void)sendData:(HTTPMethodType)HTTPMethod api:(NSString*)api parameters:(NSDictionary*)parameters needFreezable:(BOOL)needFreezable identifier:(NSString*)identifier;
- (void)uploadData:(NSString*)api attachmentData:(NSData*)attachmentData parameters:(NSDictionary*)parameters identifier:(NSString*)identifier;
...
// Cache
- (NSArray*)loadCacheData:(NSString*)api identifier:(NSString*)identifier;
// Abstract Class, must be override by sub-class
- (DataCachePolicy)cachePolicy:(NSString*)api;
- (Class)modelClass:(NSString*)api;
// Web Service Helper API
...
@end
在這里,核心是一套Common Request API 以及 BaseModalManagerProtocol 協(xié)議接口。前者負(fù)責(zé)通過(guò)Network Wrapper向服務(wù)器獲取數(shù)據(jù)并自動(dòng)轉(zhuǎn)換成Modal Entity,后者則負(fù)責(zé)異步的向Delegate通知數(shù)據(jù)更新。Cache層則直接選擇 TMCache 對(duì)Mantle轉(zhuǎn)換后的Modal Entity Dictionary做最簡(jiǎn)單的存儲(chǔ)。
一般而言,ModalManager的Delegate即是Controller層,因?yàn)槊恳粋€(gè)ModalManager會(huì)對(duì)應(yīng)自己的VC,所以只要VC在Delegate中實(shí)現(xiàn)具體的更新UI的操作,就能夠?qū)⒉煌腗odal操作和不同的View操作進(jìn)行連接對(duì)應(yīng)。因此DelegateProtocol的地位非常重要,它和NetworkManager的block機(jī)制一起,共同建立了一個(gè)從Modal層到VC層的橋梁,也就是說(shuō)從某種意義上,這種方式建立了M到C的單向綁定:
- (void)sendData:(HTTPMethodType)HTTPMethod api:(NSString*)api parameters:(NSDictionary*)parameters needFreezable:(BOOL)needFreezable identifier:(NSString*)identifier
{
// initialization
...
NSString* apiIdentifier = ... // Construct the identifier as you wish
// Call Network Manager to handle the network operation
[[HTTPNetWorkManage sharedManage] HTTPRequest:HTTPMethod URLString:api parameters:parameters data:nil needFreezable:needFreezable
// Success Block
success:^(id *task, id responseObject) {
// Parse the JSON response
Class entityClass = [self modalClass:api];
if(entityClass){
cotentDic = // parse the JSON data to entity modal via entityClass ... ;
}
// error handling if possible ...
...
// Success Delegate
if([self.delegate respondsToSelector:@selector(manager:api:identifier:didSendWithData:)]){
[self.delegate manager:self api:api identifier:identifier didSendWithData:responseObject];
}
if(DataCachePolicyLocalCache == [self cachePolicy:api]){
// Update Local Cache ...
}
}
// Failure Delegate
failure:^(id *task, NSError *error) {
if([self.delegate respondsToSelector:@selector(manager:api:identifier:didFailRequestWithError:)]){
[self.delegate manager:self api:api identifier:identifier didFailRequestWithError:error];
}
}
];
}
特別強(qiáng)調(diào)一下在這些方法中隨處可見(jiàn)的identifier。這是一個(gè)非常關(guān)鍵的參數(shù)。它的作用,是使得M能夠向C提供“多通道”通信的能力。什么意思呢?就是說(shuō),一個(gè)具有復(fù)雜業(yè)務(wù)邏輯的頁(yè)面,其Controller一定會(huì)向Modal層做出多個(gè)不同的請(qǐng)求操作,有了identifier給每一個(gè)請(qǐng)求進(jìn)行標(biāo)示,作為Delegate的C可以就可以根據(jù)這些identifier區(qū)分出不同的請(qǐng)求的回調(diào),從而對(duì)UI做出對(duì)應(yīng)的操作。
接下來(lái)舉例看下,一個(gè)具體的ModalManager的子類該做哪些事。假設(shè)我們有一個(gè)頁(yè)面,是關(guān)于個(gè)人的地址信息欄,需要從服務(wù)器端獲取所有的可用地址信息,并且可以修改這些信息,而這些地址信息中,省份信息也是需要提前拉取以便供用戶選擇的。我們就對(duì)Address這個(gè)業(yè)務(wù)提供一個(gè)獨(dú)立的AddressModalManager:
-
樣例 - AddressModalManager/AddressManageViewController
#import "BaseModelManage.h" #define kAddressModelManagehModifyAddress @"modifyAddress" #define kAddressModelManageAddAddress @"addAddress" #define kAddressModelManageFetchAllProvince @"fetchAllProvince" #define kAddressModelManageFetchAllAddress @"fetchAllAddress" #define kAddressModelManageDeleteAddress @"deleteAddress" @interface AddressModelManage : BaseModelManage // Address: CRUD operations - (void)fetchAllAddress; - (void)deleteAddress:(NSNumber*)rid; - (void)addAddress:(NSString*)consignee phoneNum:(NSString*)phoneNum provinceId:(NSNumber *)provinceId cityId:(NSNumber*)cityId streetString:(NSString*)streetString isDefault:(BOOL)isDefault; - (void)modifyAddress:(NSNumber *)rid consignee:(NSString*)consignee phoneNum:(NSString*)phoneNum provinceId:(NSNumber *)provinceId cityId:(NSNumber*)cityId streetString:(NSString*)streetString isDefault:(BOOL)isDefault; - (void)modifyDefaultAddress:(NSNumber *)rid isDefault:(BOOL)isDefault; // Province: READ-only - (void)fetchAllProvince; // Cache - (NSArray*)loadAddressCache; @end
來(lái)看AddressModalManager的實(shí)現(xiàn):(這里不再把所有的實(shí)現(xiàn)一一列舉,只選出比較有代表性的幾個(gè))
ModalManager子類為特定的業(yè)務(wù)提供CRUD操作的Wrapper,封裝基類的fetch接口:
-(void)fetchAllAddress
{
[self fetchData:kApiV1Address parameters:nil identifier:kACAddressModelManageFetchAllAddress];
}
-(void)addAddress:(NSString*)consignee phoneNum:(NSString*)phoneNum provinceId:(NSNumber *)provinceId cityId:(NSNumber*)cityId streetString:(NSString*)streetString isDefault:(BOOL)isDefault
{
NSMutableDictionary *parameter = [[NSMutableDictionary alloc] init];
[parameter setObject:provinceId forKey:@"province"];
[parameter setObject:cityId forKey:@"city"];
// other parameters ...
[self sendData:HTTPMethodTypePOST api:kApiV1Address parameters:parameter identifier:kAddressModelManageAddAddress];
}
// Other implementations
...
- (void)fetchAllProvince
{
[self fetchData:kApiV1Province parameters:nil identifier:kAddressModelManageFetchAllProvince];
}
同樣為VC層封裝Cache實(shí)現(xiàn):
- (NSArray*)loadAddressCache
{
return [self loadCacheData:kApiV1Address identifier:kAddressModelManageFetchAllAddress];
}
下面這部分是關(guān)鍵,只有對(duì)應(yīng)具體的業(yè)務(wù)(也就是具體的VC層),ModalManager才能知道VC需要的具體Modal Entity是哪些,才能讓基類BaseModalManager去自動(dòng)完成底層NetworkManager提供的JSON數(shù)據(jù)的解析,所以必須重寫modalClass類。同樣,不同的業(yè)務(wù)請(qǐng)求也決定了不同的Cache策略:
- (Class)modelClass:(NSString *)api
{
if([api isEqualToString:kApiV1Province]){
return [Province class];
}
return [PersonalAddress class];
}
- (DataCachePolicy)cachePolicy:(NSString *)api
{
if([api isEqualToString:kApiV1Province]){
return DataCachePolicyLocalCache;
}else if ([api isEqualToString:kApiV1Address]){
return DataCachePolicyLocalCache;
}
return DataCachePolicyMemoryCache;
}
這里,api即對(duì)應(yīng)了具體的業(yè)務(wù)請(qǐng)求。
最后,就是Address業(yè)務(wù)對(duì)應(yīng)的VC層,它主要負(fù)責(zé)實(shí)現(xiàn)BaseModelManageDelegateProtcol 方法去更新UI:
@implementation AddressManageViewController
// Other implementation
...
-(void)manager:(BaseModelManage *)manager api:(NSString *)api identifier:(NSString *)identifier didSendWithData:(id)data
{
if([identifier isEqualToString:kAddressModelManagehModifyAddress]){
[self.addressModelManage fetchAllAddress];
[self.tableView.header beginRefreshing];
}else if([identifier isEqualToString:kAddressModelManageDeleteAddress]){
[self.addressModelManage fetchAllAddress];
[self.tableView.header beginRefreshing];
}else{
NSLog(@"api is %@",api);
}
}
- (void)manager:(BaseModelManage *)manager api:(NSString*)api identifier:(NSString*)identifier didFailRequestWithError:(NSError *)error
{
if([self.tableView.header isRefreshing]){
[self.tableView.header endRefreshing];
}
[self.view showNetWorkError:error];
}
@end
很難說(shuō),我們的模式是MVX里的哪一種,如果按照常規(guī)的定義的話,應(yīng)該是MVVM和MVP模式的結(jié)合:
1)從基類BaseModalManager的職責(zé)上說(shuō),它實(shí)現(xiàn)了“半個(gè)”View Modal的功能,之所以說(shuō)半個(gè),是因?yàn)殡m然用delegate和block結(jié)合的方式,在某種程度上實(shí)現(xiàn)了Modal到Controller的綁定,但是并沒(méi)有做到完整意義上的View和ViewModal之間的雙向綁定;
2)從ModalManager子類的定制化來(lái)看,其和具體的View Controller掛鉤,則在某種程度上提現(xiàn)了Presenter的特點(diǎn),ModalManager的子類承擔(dān)了一部分原本Controller的業(yè)務(wù)邏輯的操作,為UI的展示提供基本的接口。
但是就像我之前說(shuō)的,最好的設(shè)計(jì)模式就是最適合自己的模式,這套架構(gòu),能夠很好的應(yīng)付我們的項(xiàng)目,不管是在可擴(kuò)展性上,還是在可維護(hù)性上,目前都表現(xiàn)的相當(dāng)優(yōu)秀。稍微有些不足的地方,可能是由于ModalManager的子類是針對(duì)具體的UI Page的,在少數(shù)情況下的一些派生子類重復(fù)性功能代碼比較多。但是對(duì)于這一點(diǎn),我們只需要針對(duì)這些相似的業(yè)務(wù)邏輯,將通用的ModalManager給提煉出來(lái),就能夠在很大程度上提高復(fù)用度的問(wèn)題。
總結(jié)
不管是MVC還是MVVM還是什么MVX,設(shè)計(jì)模式總是為解決具體的問(wèn)題服務(wù)的。沒(méi)有最好的設(shè)計(jì)模式,只有在特殊場(chǎng)景下最憂的設(shè)計(jì)模式。我們?cè)谧黾軜?gòu)設(shè)計(jì)時(shí),應(yīng)當(dāng)不斷地依據(jù)實(shí)際項(xiàng)目經(jīng)驗(yàn)的累積和總結(jié),在多種不同的模式中找到他們想解決的實(shí)際問(wèn)題的關(guān)鍵點(diǎn)的思路,然后用這些思路去設(shè)計(jì)項(xiàng)目,而不是被具體的“X”給束縛了手腳。
2016.5.12 完稿于南京
參考文獻(xiàn)
iOS 框架模式(簡(jiǎn)述 MVC,MVP,MVVM 和 VIPER)
界面之下:還原真實(shí)的 MVC、MVP、MVVM 模式
多方位全面解析:如何正確地寫好一個(gè)界面
MVC,MVP 和 MVVM 的圖示
iOS應(yīng)用架構(gòu)談 網(wǎng)絡(luò)層設(shè)計(jì)方案
使用VIPER構(gòu)建iOS應(yīng)用




