前言
随着移动开发的浪潮逐渐退去,现在对于移动开发工程师的要求越来越高,不在仅仅满足于能完成项目所需的功能于界面的搭建,对于底层和架构的理解对于现在的开发越来越重要,在此有一点架构的设计经验可以与大家分享和探讨
传统移动MVC设计模式
在长久以来的开发过程中,我们有使用着用途最广,使用者众多的MVC设计模式。但是,随着开发的进行,MVC产生的问题越来越多,控制器层的代码无法控制,代码越来越难以维护和迭代。每当产品来提出新的需求,程序猴子们的大刀早已饥渴难耐了,但是又不得不陷入继续打补丁版本更新无尽的循环中···
是MVC已经被时代淘汰了吗?
可能我们没有使用正确的姿势
深入理解MVC中的M
Model不只是一个数据模型,更是一个处理数据产生、获取、逻辑处理中心,不能只单单理解为一个数据模型,我们在开发中使用的Model只是这个数据中心中很小很小的一个部分。而在这个数据中心中,我们又应该将这个中心分层,比如可以分为Service层用来处理业务逻辑、DAO层处理数据库逻辑、Protocol层来封装一些某些功能接口等等。在此,我使用JAVAWeb中的一个图来描述设计模式是再好不过了,如下:
移动开发过程中不涉及Web服务器层,但是后面两部分的设计思想可以很好的借鉴和使用到移动开发中,特别是Service层的设计,由于在Java中除了类能封装某一些特定的功能之外,还提供了给我们interface来封装某一些特定的功能,一是用来避免使用类的重量级剔除三大特性,二是能解决不相干的类中包含一些共同的方法的问题。在iOS中,我们就可以大肆使用protocol来解决分层中的许多问题,现在越来越火的POP面向协议编程也就是利用的这一思想。欣慰的是,在Swift版本不断迭代过程中,苹果也越来越重视和推崇这一思想,在Swift中也不断优化,方便开发者使用,已经支持在协议中添加默认的方法实现,使用起来更加方便。
群众:逼逼了这么多,不来点代码
不多逼逼,直接上项目代码:
项目中有这样一个需求:有几个页面内容大致相同,这个页面的数据源不同,不同数据源展示的页面Cell也不完全同,数据需要缓存,没有网络的时候需要展示缓存数据,有网络时显示网络请求下来的最新数据
思路1:每个不同页面分别建立一个控制器,分别写请求方法,控制器中独立编写每个页面的逻辑处理与控制
思路2:抽取这几个页面的共同部分为基类,然后建立子类分别写子类中独立的方法
思路3:只写一个公共通用类,针对不同页面的数据,切换不同的数据源,并且在直接在通用类中判断来展示不同的cell
对比一下三种思路,第一种思路最容易想到,也是程序员能马上开工的思路,但是也是代码最多最累的方式,且不利于后期增加和维护;第二种比第一种多了继承的思想,但是也增加了代码量和文件量;在项目中我是采取了第三种方式,最少代码来能实现需求功能,且还要利于后期维护
抽取一个protocol文件并提取几个页面公用数据借口
@protocol LYIPAggregationBusinessProtocol
///////////////////////////////////////筛选条件///////////////////////////////////////
- (LYSignal<NSArray<LYAggregationFilterModel *> *> *)allSearchConditions;
///////////////////////////////////////搜索结果///////////////////////////////////////
- (LYSignal<NSArray<LYAggregationCommonDetail *> *> *)allSearchedCommonItemsPage:(NSInteger)page rows:(NSInteger)rows keywords:(NSString *)keywords;
@end
不同的页面新建文件并在.h中添加协议继承公用protocol并增加特有方法,由于项目代码设计业务故名称用Page代替
@protocol LYIPAggregationPage1DatasProtocol<LYIPAggregationBusinessProtocol>
///////////////////////////////////////获得页面一列表数据///////////////////////////////////////
- (LYSignal<NSArray<LYAggregationPage1Detail *> *> *)commonItemsWithPage:(NSInteger)page rows:(NSInteger)rows catId:(NSString *)catId;
...省略其他方法
@end
///////////////////////////////////////获得页面二列表数据///////////////////////////////////////
@protocol LYIPAggregationPage2DatasProtocol<LYIPAggregationBusinessProtocol>
- (LYSignal<NSArray<LYAggregationPage2Detail *> *> *)commonItemsWithPage:(NSInteger)page rows:(NSInteger)rows catId:(NSString *)catId;
...省略其他方法
@end
///////////////////////////////////////获得页面三列表数据///////////////////////////////////////
@protocol LYIPAggregationPage3DatasProtocol<LYIPAggregationBusinessProtocol>
- (LYSignal<NSArray<LYAggregationPage3Detail *> *> *)commonItemsWithPage:(NSInteger)page rows:(NSInteger)rows catId:(NSString *)catId;
...省略其他方法
@end
protocol抽取完毕,接下来继续看实现,实现写在.m文件中,由于几个不同页面的逻辑大致相同,故只展示一个页面代码:
- (LYSignal<NSArray<LYAggregationFilterModel *> *> *)allSearchConditions {
// 如果没网通过服务类获取本地数据
if(LYReachability.currentState == LYNetStateError) return GET_SERVICE(Page1AllSearchConditions);
// 有网则请求获取网络最新数据
return [[@"/v1/guest/childrenCategoryList".http_get parameters:@{
@"cateCode" : @"V11"
}]
resultMap:^id(id value) {
return [LYAggregationFilterModel ly_objectArrayWithKeyValuesArray:value];
}].signal;
}
- (LYSignal<NSArray<LYAggregationPage1Detail *> *> *)allSearchedPage1ItemsPage:(NSInteger)page rows:(NSInteger)rows keywords:(NSString *)keywords {
// 如果没网通过服务类获取本地数据
if(LYReachability.currentState == LYNetStateError) return GET_SERVICE(AllSearchedPage1ItemsPage);
// 有网则请求获取网络最新数据
return [[@"/v1/search/goods".http_get parameters:@{
@"page" : @(page),
@"rows" : @(rows),
@"cateCode" : @"V11",
@"keyword" : NotNil(keywords)
}]
resultMap:^id(id value) {
id keyvalues = [value objectForKey:@"list"];
NSArray *array = [LYAggregationPage1Detail ly_objectArrayWithKeyValuesArray:keyvalues];
BOOL hasNextPage = [[value objectForKey:@"hasNextPage"] boolValue];
[array enumerateObjectsUsingBlock:^(LYAggregationPage1Detail *obj, NSUInteger idx, BOOL * _Nonnull stop) {
obj.hasNextPage = hasNextPage;
obj.page = page;
}];
return array;
}].signal;
}
- (LYSignal<NSArray<LYAggregationPage1Detail *> *> *)page1ItemsWithPage:(NSInteger)page rows:(NSInteger)rows catId:(NSString *)catId {
// 如果没网通过服务类获取本地数据
if(LYReachability.currentState == LYNetStateError) return GET_SERVICE(Page1ItemsWithPage);
// 有网则请求获取网络最新数据
return [[@"/v1/guest/goodsList".http_get parameters:@{
@"page" : @(page),
@"rows" : @(rows),
@"cateCode" : catId? catId: NotNil(@"V11"),
}]
resultMap:^id(id value) {
id keyvalues = [value objectForKey:@"list"];
NSArray *array = [LYAggregationPage1Detail ly_objectArrayWithKeyValuesArray:keyvalues];
BOOL hasNextPage = [[value objectForKey:@"hasNextPage"] boolValue];
[array enumerateObjectsUsingBlock:^(LYAggregationPage1Detail *obj, NSUInteger idx, BOOL * _Nonnull stop) {
obj.hasNextPage = hasNextPage;
obj.page = page;
}];
return array;
}].signal;
}
在页面数据获取的实现中,我添加了数据获取的实现先判断用户当前的网络状态。如果断网状态我先通过一个服务类宏去获取本地数据,本地数据的获取也是一个协议,通过协议的方法拿到数据的实现,再看控制器代码:
- (void)viewDidLoad {
[super viewDidLoad];
// 添加数据绑定
[self setupData];
...
}
- (void)setupData {
if ([self.cateCode isEqualToString:@"V11"]) {
self.dataGetter = GET_SERVICE(LYIPAggregationPage1DatasProtocol);
}else if([self.cateCode isEqualToString:@"V18"]) {
self.dataGetter = GET_SERVICE(LYIPAggregationPage2DatasProtocol);
}else {
self.dataGetter = GET_SERVICE(LYIPAggregationPage3DatasProtocol);
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if ([_cateCode isEqualToString:@"V11"]) {
LYIPAggregationPage1Cell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(LYIPAggregationPage1Cell.class)];
cell.dataSource = self.commonItemsArray[indexPath.row];
return cell;
}else if([_cateCode isEqualToString:@"V18"]){
LYIPAggregationPage2Cell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(LYIPAggregationPage2Cell.class)];
cell.ticketDataSource = self.commonItemsArray[indexPath.row];
return cell;
}else {
LYIPAggregationPage3Cell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(LYIPAggregationPage3Cell.class)];
cell.dataSource = self.commonItemsArray[indexPath.row];
return cell;
}
}
上面的代码片段只展示了控制器中的部分代码,数据请求可以完全从控制器代码中分离从而减轻控制器负担,当然,分离这个并不是重点,重点是我们抽出一个数据服务类之后,我们可以在服务类中添加更多逻辑和方法,数据来源只是其中的一种。数据来源可以从本地缓存的数据库获取,也可以从网络请求获取。本地的缓存服务也可以抽出一套接口方法,让具体的实现类去实现。抽取服务类还有一个好处,就是我们在服务器接口未开发完毕或者自己想用假数据来测试时,能手动添加一个类实现协议方法,添加本地假数据方便调试程序。切换数据源时十分简单,控制器中的代码只需动一行,也就是只用切换数据源获取的方法就能切换为测试数据。
服务类的功能还有很多,分离代码是服务类的一个很重要的功能,降低代码内部的耦合,更加便于维护。如果服务类中涉及到数据库的操作时,比如登陆注册成功之后的用户信息入库等操作时,这个时候就应该用到DAO层了。DAO也可以用相同的设计思想来设计,Service层中逻辑可以添加与DAO层的交互,而这所有的一切控制器根本就不用关心。
第一篇只是简单介绍了Service层中的一个应用,关于Service的实现,将会在第二篇中介绍。一个好的设计思想和模式确实十分重要,越是大的项目越是要从开始就应该注重设计,设计完成之后再开始写代码。