IOS架构

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

2016-05-13  本文已影响316人  ac3

声明:本文中的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通信,子类则完全根据实际业务进行定制构造。

我们来看下具体示例:

首先看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的实现:(这里不再把所有的实现一一列举,只选出比较有代表性的几个)
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应用

上一篇下一篇

猜你喜欢

热点阅读