iOS架构设计与优化(一)

前言

随着移动开发的浪潮逐渐退去,现在对于移动开发工程师的要求越来越高,不在仅仅满足于能完成项目所需的功能于界面的搭建,对于底层和架构的理解对于现在的开发越来越重要,在此有一点架构的设计经验可以与大家分享和探讨

传统移动MVC设计模式

在长久以来的开发过程中,我们有使用着用途最广,使用者众多的MVC设计模式。但是,随着开发的进行,MVC产生的问题越来越多,控制器层的代码无法控制,代码越来越难以维护和迭代。每当产品来提出新的需求,程序猴子们的大刀早已饥渴难耐了,但是又不得不陷入继续打补丁版本更新无尽的循环中···

是MVC已经被时代淘汰了吗?
可能我们没有使用正确的姿势

深入理解MVC中的M

Model不只是一个数据模型,更是一个处理数据产生、获取、逻辑处理中心,不能只单单理解为一个数据模型,我们在开发中使用的Model只是这个数据中心中很小很小的一个部分。而在这个数据中心中,我们又应该将这个中心分层,比如可以分为Service层用来处理业务逻辑、DAO层处理数据库逻辑、Protocol层来封装一些某些功能接口等等。在此,我使用JAVAWeb中的一个图来描述设计模式是再好不过了,如下:

image

移动开发过程中不涉及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的实现,将会在第二篇中介绍。一个好的设计思想和模式确实十分重要,越是大的项目越是要从开始就应该注重设计,设计完成之后再开始写代码。