美文网首页IOS架构
iOS App架构:实践中体会形形色色的MV“X”

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

作者: ac3 | 来源:发表于2016-05-13 17:04 被阅读316次

    声明:本文中的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应用

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/nfzwlttx.html