声明:本文中的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们,出来接客了~”
-
传统的MVC
MVC -
Apple的MVC
MVC-Apple -
MVP
MVP -
MVVM
MVVM -
MVA
Model–View–Adapter模式。这是一种比较少见的模式,可以参考Wiki上的解释Model–view–adapter。其核心就是阻断View和Modal的交互,MVA三者是线性的沟通关系,而非传统MVC的三角关系。从这个概念上讲和上述几种模式非常相似,可能这就是为什么这个概念已经很少有人提到,因为能被称作MVA的模式,可能都在上述几种方案中了。 -
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这一层:
- 原来的DataManager分裂成了一个BaseModalManager基类和一系列ModalManager子类;
- 每一个ModalManager子类对应于自己的Modal Entity,
- 每一个ModalManager子类对应于一组View和Controller来完成一组特定的业务逻辑(多数是以页面为单位,同一个页面的逻辑会使用一个或多个ModalManager)。
- 这些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应用
网友评论