iOS App架构:实践中体会形形色色的MV“X”

声明:本文中的solution是我们iOS team的集体智慧结晶,并非我一个人的独有成果,在此感谢整个团队的支持和帮助。转载请注明出处。

前言

应用设计模式的概念随着iOS和Android的流行,被讨论得越来越多,MVC之于iOS,已经像当年OO之于C++/Java一样,随口就被提到烂大街的程度。但是实际上MVC的概念并不是Apple最先提出的,更不是iOS专有。只不过Apple对传统的MVC进行了改进,使其更加适合iOS App的开发。同理,既然MVC是一个具有深远历史的模型,随着时间的推进和各种新的需求的提出,其本身也会不断地被改进发展,出现了MVP,MVVM,MVA,MVCS,甚至还有VIPER……各种MV“X”模式。之所以会出现形形色色的MVX,其实最核心的还是因为MVC中的C——职责太大太重以至于不堪重负:开发的人不堪重负地写着复杂又没有技术含量的代码,维护的人不堪重负地去翻阅动辄长达数千行的代码,测试的人更是不堪重负地对着和UI及业务重度绑定难以自动化测试的单元。也就是所谓的 ** Massive Controller **问题, 这是推动MVC向前进的本源。但是就像这篇《iOS应用架构谈 view层的组织和调用方案》中说的,不管这些MVX怎么设计,都离不开MVC这个根基,天下终归还是MVC的天下。

那么,面对这么多的MVX,在做架构设计时应该如何选择是一件即头疼又简单的问题:头疼是你要做出选择,对于我这种有选择综合症的人来说,显然是很痛苦的一件事;但是它其实又是一件很简单的事,在不知道如何选择时,总是选择自己最熟悉的方式自然是风险最小的。当然,熟悉的模式越来越多,可供选择的也越来越多,这个时候,清晰的了解每种模式的优缺点,然后对比项目的实际规模和情景,去找“最合适”的而不是“最好”的模式。

各种MVX

网上介绍各种MVX的文章数不胜数,可以参阅本文最后的参考链接,这里,做为笔记,把几种常见的MVX一一列出来,做一个简单的描述。

我都仿佛听到了那句熟悉的台词:“楼上的MVX们,出来接客了~”

  1. 传统的MVC


    MVC
  2. Apple的MVC


    MVC-Apple
  3. MVP


    MVP
  4. MVVM


    MVVM
  5. MVA
    Model–View–Adapter模式。这是一种比较少见的模式,可以参考Wiki上的解释Model–view–adapter。其核心就是阻断View和Modal的交互,MVA三者是线性的沟通关系,而非传统MVC的三角关系。从这个概念上讲和上述几种模式非常相似,可能这就是为什么这个概念已经很少有人提到,因为能被称作MVA的模式,可能都在上述几种方案中了。

  6. VIPER


    VIPER

事实上我个人对VIPER这个结构很感兴趣,但是的确像参考文中提到的,灵活的代价就是复杂。这种架构非常像“乐高”玩具,给您提供了最大化的自由度,但是即使为了构建简单的App你仍然需要用一堆的小零件才能组装起来。我自己有把本文中我们的架构用VIPER的思想重新写了一个测试样例,代码量确实要增加50%左右。但是思路上(包括代码组织结构上)可能比现有的设计模式更加清晰。

我们是怎么做那个“X”的

  • 最初架构设计

其实我并没有一上来就奔着某个特殊的MVX去设计,因为在初期架构阶段,没有太多可以参考的实际业务逻辑,只有一些基本的需求,所以一开始的时候,基于功能部件之间的关系,很自然的进行了一个基本的分层设计。V和C两层也是各司其职,但是对M层,进行了特殊的设计,封装一层独立的ModalLayer。由于所有热数据均来自服务器端,必须要有一个和服务器端打直接交道的NetworkManager用以处理所有的网络请求和响应,一部分冷数据需要进行本地缓存用以离线展示,所以单独设计一个CacheManager用来桥接。在这个阶段DataManager的本意是将NetworkManager的接口做一定程度的封装,将显式的HTTP操作转换成标准的CRUD操作接口,然后向Conroller层提供统一的服务。同时,负责根据对应的Cache Policy在Cache和Network之间进行切换。说白了,它就是Modal操作接口的一个Wrapper类。

OriginArc.png

其中,CacheManager这一环在这个初期设计中我们使用的是Core Data,DataManager负责管理Core Data的Modal Entity,于是顺理成章的接管了Modal这一层的操作。因此这里的设计其实还是一个标准的MVC模式,只是在M层增加了一个Network Helper(ACNetworkManager)和一个Wrapper(ACCoreDataManager)来使得网络操作更加方便。

MVC1.png
  • 最终实现方案

在实际的实现过程中,随着业务逻辑的不断提炼和解耦,上述架构设计慢慢演变成这样:

NewArc.png

可以看到最明显的区别在ModalLayer这一层:

  1. 原来的DataManager分裂成了一个BaseModalManager基类和一系列ModalManager子类;
  2. 每一个ModalManager子类对应于自己的Modal Entity,
  3. 每一个ModalManager子类对应于一组View和Controller来完成一组特定的业务逻辑(多数是以页面为单位,同一个页面的逻辑会使用一个或多个ModalManager)。
  4. 这些ModalManager全部通过父类(基类)BaseModalManager和CacheManager以及NetworkManager通信,子类则完全根据实际业务进行定制构造。

我们来看下具体示例:

  • 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
    在这里,我们在第二版的实现中Modal层的Entity和Cache都抛弃了Core Data,而让BaseModal Entity直接继承自Mantle,这样直接核心就是modalOfClass接口,用于将JSON数据自动转换成Modal类:

      @interface ACBaseModel : MTLModel<MTLJSONSerializing>
      + (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error;
      @end
    

关于为什么放弃Core Data而使用Mantle,可以参考我的另一篇文章《从Core Data到Mantle》。后来我才发现我们并不是唯一的一个有过这样的经历的团队,这里有一篇文章《为什么唱吧iOS 6.0选择了Mantle》,可以给大家另一个直观的感受。

  • BaseModalManager

首先看ModalManager的基类方法设定:

    /* 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 协议接口。前者负责通过Network Wrapper向服务器获取数据并自动转换成Modal Entity,后者则负责异步的向Delegate通知数据更新。Cache层则直接选择 TMCache 对Mantle转换后的Modal Entity Dictionary做最简单的存储。

一般而言,ModalManager的Delegate即是Controller层,因为每一个ModalManager会对应自己的VC,所以只要VC在Delegate中实现具体的更新UI的操作,就能够将不同的Modal操作和不同的View操作进行连接对应。因此DelegateProtocol的地位非常重要,它和NetworkManager的block机制一起,共同建立了一个从Modal层到VC层的桥梁,也就是说从某种意义上,这种方式建立了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];
              }
           }
        ];
    }

特别强调一下在这些方法中随处可见的identifier。这是一个非常关键的参数。它的作用,是使得M能够向C提供“多通道”通信的能力。什么意思呢?就是说,一个具有复杂业务逻辑的页面,其Controller一定会向Modal层做出多个不同的请求操作,有了identifier给每一个请求进行标示,作为Delegate的C可以就可以根据这些identifier区分出不同的请求的回调,从而对UI做出对应的操作。

接下来举例看下,一个具体的ModalManager的子类该做哪些事。假设我们有一个页面,是关于个人的地址信息栏,需要从服务器端获取所有的可用地址信息,并且可以修改这些信息,而这些地址信息中,省份信息也是需要提前拉取以便供用户选择的。我们就对Address这个业务提供一个独立的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
    

来看AddressModalManager的实现:(这里不再把所有的实现一一列举,只选出比较有代表性的几个)
ModalManager子类为特定的业务提供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实现:

    - (NSArray*)loadAddressCache
    {
        return [self loadCacheData:kApiV1Address identifier:kAddressModelManageFetchAllAddress];
    }

下面这部分是关键,只有对应具体的业务(也就是具体的VC层),ModalManager才能知道VC需要的具体Modal Entity是哪些,才能让基类BaseModalManager去自动完成底层NetworkManager提供的JSON数据的解析,所以必须重写modalClass类。同样,不同的业务请求也决定了不同的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即对应了具体的业务请求。
最后,就是Address业务对应的VC层,它主要负责实现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

很难说,我们的模式是MVX里的哪一种,如果按照常规的定义的话,应该是MVVM和MVP模式的结合:
1)从基类BaseModalManager的职责上说,它实现了“半个”View Modal的功能,之所以说半个,是因为虽然用delegate和block结合的方式,在某种程度上实现了Modal到Controller的绑定,但是并没有做到完整意义上的View和ViewModal之间的双向绑定;
2)从ModalManager子类的定制化来看,其和具体的View Controller挂钩,则在某种程度上提现了Presenter的特点,ModalManager的子类承担了一部分原本Controller的业务逻辑的操作,为UI的展示提供基本的接口。

但是就像我之前说的,最好的设计模式就是最适合自己的模式,这套架构,能够很好的应付我们的项目,不管是在可扩展性上,还是在可维护性上,目前都表现的相当优秀。稍微有些不足的地方,可能是由于ModalManager的子类是针对具体的UI Page的,在少数情况下的一些派生子类重复性功能代码比较多。但是对于这一点,我们只需要针对这些相似的业务逻辑,将通用的ModalManager给提炼出来,就能够在很大程度上提高复用度的问题。

总结

不管是MVC还是MVVM还是什么MVX,设计模式总是为解决具体的问题服务的。没有最好的设计模式,只有在特殊场景下最忧的设计模式。我们在做架构设计时,应当不断地依据实际项目经验的累积和总结,在多种不同的模式中找到他们想解决的实际问题的关键点的思路,然后用这些思路去设计项目,而不是被具体的“X”给束缚了手脚。

2016.5.12 完稿于南京

参考文献

iOS 框架模式(简述 MVC,MVP,MVVM 和 VIPER)
界面之下:还原真实的 MVC、MVP、MVVM 模式
多方位全面解析:如何正确地写好一个界面
MVC,MVP 和 MVVM 的图示
iOS应用架构谈 网络层设计方案
使用VIPER构建iOS应用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容