iOS App架構(gòu):實(shí)踐中體會(huì)形形色色的MV“X”

聲明:本文中的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)接客了~”

  1. 傳統(tǒng)的MVC


    MVC
  2. Apple的MVC


    MVC-Apple
  3. MVP


    MVP
  4. MVVM


    MVVM
  5. 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的模式,可能都在上述幾種方案中了。

  6. 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類。

OriginArc.png

其中,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ò)操作更加方便。

MVC1.png
  • 最終實(shí)現(xiàn)方案

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

NewArc.png

可以看到最明顯的區(qū)別在ModalLayer這一層:

  1. 原來(lái)的DataManager分裂成了一個(gè)BaseModalManager基類和一系列ModalManager子類;
  2. 每一個(gè)ModalManager子類對(duì)應(yīng)于自己的Modal Entity,
  3. 每一個(gè)ModalManager子類對(duì)應(yīng)于一組View和Controller來(lái)完成一組特定的業(yè)務(wù)邏輯(多數(shù)是以頁(yè)面為單位,同一個(gè)頁(yè)面的邏輯會(huì)使用一個(gè)或多個(gè)ModalManager)。
  4. 這些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)用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容