iOS_Model层到底怎么用?

3,790 阅读8分钟

最近在读App架构方面的书。对这个感兴趣是因为我意识到:

  • 如果只停留在一些简单页面开发,架构肯定作用不大,只需要关注这个页面需要什么技术细节来实现就可以;
  • 但如果是涉及到一个功能多样或者业务复杂的App,那么有一个良好规范的架构绝对是有帮助的。

然后onevcat的 关于 MVC 的一个常见的误用一文也启发了我,解决了一直以来我对Model的困惑,所以想用自己的例子再记录一下,正好实现一个OC的版本。


一、标准的Model使用

在onevcat的文章中,大神贴出了一个标准的MVC结构图。这个图源自斯坦福CS193p的iOS应用开发课程。我在自学入门iOS的时候也学习了这个课程,不过是很老的版本,还用的Objective-C作为教学语言。

当时我还是个超级新手,看到这个图的时候,最先懂的是View和Controller的交互,毕竟iOS开发最先接触学习的肯定是视图的创建和交互。而看到Model和Controller的交互,只知道用通知来和Controller通信,至于具体怎么实现则是我一直的困惑。

别看这只是一个单纯的类之间的通信问题,我相信很多缺乏和周围交流的新手开发者们对模型的使用很容易停留在“瘦Model”上。即便想实现上图的标准用法,但一时半会儿还真不好找学习资料,反正不用实现图片里的标准,App一样能开发,更没动力找了。

二、现状

因此很容易出现的情况:Massive ViewController,把逻辑都堆在ViewController这个视图容器里,形成了庞大的、难以维护的单个类。

这也是onevcat大神在他的文章中提出的两个问题,Massive ViewController:

  1. 本质是Model 层“寄生”在ViewController 中
  2. 违反数据流动规则和单一职责规则

用我的例子举例说明这两个问题。现在我们实现了下图的一个服务列表。

这个我的需求和我的服务这两个列表共用一个模型:

@interface MyReleaseModel : JSONModel

// 公用
@property(nonatomic, copy) NSString<Optional> *type;
@property(nonatomic, copy) NSString<Optional> *ID;
@property(nonatomic, copy) NSString<Optional> *addtime;
@property(nonatomic, copy) NSString<Optional> *views;


// 我的需求
@property(nonatomic, copy) NSArray<Optional> *dem_img;
@property(nonatomic, copy) NSString<Optional> *dem_desc;
@property(nonatomic, copy) NSString<Optional> *dem_price;


// 我的服务
@property(nonatomic, copy) NSArray<Optional> *s_img;
@property(nonatomic, copy) NSString<Optional> *s_desc;
@property(nonatomic, copy) NSString<Optional> *s_price;


然后在ViewController里,有两个当前列表的数组,之后的删除逻辑就需要操作它:

// 需求列表array
@property(nonatomic, strong) NSMutableArray *demandMutaArray;

// 服务列表array
@property(nonatomic, strong) NSMutableArray *serviceMutaArray;

点击删除按钮的逻辑,需求和服务的删除逻辑一样,所以这里列举需求的删除代码(OC的代码真的很不适合展示……):

// 点击了我的需求 删除按钮

// 由于在cell里,所以获取到当前cell
ReleasedServiceAndDemandTableViewCell *myDemandCell = (ReleasedServiceAndDemandTableViewCell *)[[[sender view] superview] superview];

// 再获取当前行数
NSIndexPath *myDemandIndexPath = [weakSelf.demandTableView indexPathForCell:myDemandCell];

// 使用了JSONModel,所以数组里的每一项都是一个JSONModel类型的数据
MyReleaseModel *myDemandModel = weakSelf.demandMutaArray[myDemandIndexPath.row];

// 网络请求写在Model类里了,所以从Model发出删除的网络请求(隐去具体的参数)
[myDemandModel deleteItemNetworkWithxxx:myDemandModel.xxx withxxx:myDemandModel.xxx];

// 在viewController类里对数组操作
[weakSelf.demandMutaArray removeObjectAtIndex:myDemandIndexPath.row];

// 调用系统框架里列表的删除API
[weakSelf.demandTableView deleteRowsAtIndexPaths:@[myDemandIndexPath] withRowAnimation:UITableViewRowAnimationLeft];


三、阐述问题

现在就是这么一个通过列表展示数据,然后能进行删除操作的情况。那么这有什么问题呢?

首先,就是Model 层“寄生”在ViewController 中

表面上看似有一个MyReleaseModel类,但它其实是“瘦Model”,只提供需要的属性字段,真正起到Model作用的则是上面的demandMutaArrayserviceMutaArray两个数组。

onevcat在他的文章中提出:

我们难以从外界维护或者同步 items(注:这里是demandMutaArrayserviceMutaArray两个数组) 的状态,添加和删除操作被“绑定”在了这个 View Controller 里,如果你还想通过其他 View Controller 维护待办列表的话,就不得不考虑数据同步的问题 (我们会在稍后看到几个具体的这方面的例子);另外,这样的设置导致 items 难以测试。你几乎无法为添加/删除/修改待办列表进行 Model 层的测试。

其次,是违反数据流动规则和单一职责规则

如果点击删除按钮的话,会是这样一个流程:

  1. 改变Model(demandMutaArrayserviceMutaArray两个数组)
  2. 改变tableView的Cell

这实质是操作UI,然后变更Model,但同时也变更了UI。但之前那个标准的MVC图所倡导的数据流动应该是:

  • UI 操作 -> 经由 View Controller 进行模型更新 -> 新的模型经由 View Controller 更新 UI -> 等待新的 UI 操作

而上面的例子则在经由 View Controller 进行模型更新这一步变成经由 View Controller 进行模型更新以及 UI 操作。onevcat大神的观点是:“虽然看起来这是很不起眼的变更,但是会在项目复杂后带来麻烦。”

在onevcat大神的文章里,他列举了两个场景证明他的观点,可以去看一下。

四、到底怎么改进,更好地使用Model

创建真正的Model层

整个改进过程就是把ViewController里操作数据的那部分逻辑迁移到Model层,然后Model层使用通知Notification把必要的信息回传给ViewController,后者根据信息做相应动作。

Model层是app的内容,它不依赖于(像UIKit那样的)任何app框架。也就是说,程序员对model层有完全的控制。Model层通常包括model对象(在录音app中的例子是文件夹和录音对象)和协调对象(比如我们的app例子中的负责在磁盘上存储数据的Store类型)。被存储在磁盘上的那部分model我们称之为文档model(documentation model)。

如果model层能做到和应用框架分离,我们就可以完全在app的范围之外使用它。我们可以很容易地在另外的测试套件中运行它,或者用一个完全不同的应用框架重写新的view层。这个model层将能够用于Android,macOS或者Windows版本的app中。

——《App架构——使用Swift进行iOS架构》

Model主要使用观察者模式:

观察者模式是在MVC中维持model和view分离的关键。

这种方式的优点在于,不论变更源自哪里(比如,view事件、后台任务或者网络),我们都可以确信UI是和model数据同步的。

而且在遇到变更请求时,model将有机会拒绝或者修改这个请求

——《App架构——使用Swift进行iOS架构》

把数据相关的属性放到Model里

@interface MyReleaseModel ()

@property(nonatomic, strong) NSMutableArray *demandMutaArray;
@property(nonatomic, strong) NSMutableArray *serviceMutaArray;

@end

然后我们需要监视这两个列表数组的变化,Swift有值类型的数组,有监视属性,可以非常方便地监视属性的变化。在OC里我就先用KVO代替了。

#pragma mark - KVO method
// 观察回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    NSMutableArray *oldArray = (NSMutableArray *)[change valueForKey:@"old"];
    NSMutableArray *newArray = (NSMutableArray *)[change valueForKey:@"new"];
    
    if ([keyPath isEqualToString:@"demandMutaArray"]) {
        // demandMutaArray
    } else {
        // serviceMutaArray
        
    }
}

// 添加KVO
- (void)observePropertyChange {
    [self.demandMutaArray addObserver:self forKeyPath:@"demandMutaArray" options:NSKeyValueObservingOptionNew context:nil];
    [self.serviceMutaArray addObserver:self forKeyPath:@"serviceMutaArray" options:NSKeyValueObservingOptionNew context:nil];
}

// 移除KVO
- (void)removeObserverFromProperty {
    [self.demandMutaArray removeObserver:self forKeyPath:@"demandMutaArray"];
    [self.serviceMutaArray removeObserver:self forKeyPath:@"serviceMutaArray"];
}

在适当的地方调用添加KVO和移除KVO的方法。然后在观察回调方法,也就是每次属性变化的时候,我们做一个新值和旧值的对比,再定义一个enum,根据对比结果返回enum的状态。


typedef enum : NSUInteger {
    addItem,
    removeItem,
    reload,
} ChangeBehavior;


+ (ChangeBehavior)differenceBetweenOld:(NSMutableArray *)old andNew:(NSMutableArray *)new {
    NSSet *oldSet = [NSSet setWithArray:old];
    NSSet *newSet = [NSSet setWithArray:new];
    
    if ([oldSet isSubsetOfSet:newSet]) {
        // 添加
        // ...
        return addItem;
    } else if ([newSet isSubsetOfSet:oldSet]) {
        // 删除
        // ...
        return removeItem;
    } else {
        // 既添加 也删除
        // ...
        return reload;
    }
}


// 观察回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    NSMutableArray *oldArray = (NSMutableArray *)[change valueForKey:@"old"];
    NSMutableArray *newArray = (NSMutableArray *)[change valueForKey:@"new"];
    
    if ([keyPath isEqualToString:@"demandMutaArray"]) {
        
        // demandMutaArray
        ChangeBehavior behavior = [self.class differenceBetweenOld:oldArray andNew:newArray];
        [[NSNotificationCenter defaultCenter] postNotificationName:@"MyReleaseModelDemandDidChangedNotification" object:self userInfo:@{@"MyReleaseModelDemandDidChangedNotification": @(behavior)}];
        
    } else {
        // serviceMutaArray
        // 同上
    }
}


在Model里给外界开放“添加”“删除”等操作数据的方法和一些数据相关的属性

@property(nonatomic, assign) NSInteger demandPage;
@property(nonatomic, assign) NSInteger servicePage;
@property(nonatomic, assign) NSInteger demandCount;
@property(nonatomic, assign) NSInteger serviceCount;
- (void)addItem:(NSMutableArray *)itemArray;
- (void)removeAtIndex:(NSIndexPath *)indexPath;
- (MyReleaseModel *)itemAtIndex:(NSIndexPath *)indexPath;

贴上接口定义,实现代码就不在此贴上了,在实现里会改变demandMutaArrayserviceMutaArray,从而触发KVO回调,再通过通知Notification把相应的Enum状态返回给订阅通知的ViewController类。

在相应的ViewController类里订阅通知,视图更新时,调用Model方法操作数据

先在相应的ViewController里实例化Model,懒加载方式:

- (MyReleaseModel *)demandModel {
    if (_demandModel == nil) {
        _demandModel = [MyReleaseModel sharedInstance];
    }
    return _demandModel;
}

- (MyReleaseModel *)serviceModel {
    if (_serviceModel == nil) {
        _serviceModel = [MyReleaseModel sharedInstance];
    }
    return _serviceModel;
}

订阅通知,以及当Model改变时,Model通知ViewController来改变View:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(demandOrServiceDidChange:) name:@"MyReleaseModelDidChangedNotification" object:nil];
}

- (void)demandOrServiceDidChange:(NSNotification *)notification {
    if  ([notification.name isEqualToString:@"MyReleaseModelDemandDidChangedNotification"]) {
        // 需求列表
        ChangeBehavior behaivor = (ChangeBehavior)notification.userInfo[@"MyReleaseModelDemandDidChangedNotification"];
        switch (behaivor) {
            case addItem:
                // 给table添加相应的cell
                break;
            case removeItem:
                // 删除table相应的cell
                break;
            case reload:
                // 刷新tableView
                break;
            default:
                break;
        }
    } else {
        // 服务列表
        // ...
    }
}



或者当View改变时,View通过ViewController改变Model:


- (void)tapGestureAction:(UITapGestureRecognizer *)sender {
    NSInteger index = sender.view.tag;
    
    if (index == 1) {
        NSLog(@"点击了我的需求 删除按钮");
        ReleasedServiceAndDemandTableViewCell *myDemandCell = (ReleasedServiceAndDemandTableViewCell *)[[[sender view] superview] superview];
            NSIndexPath *myDemandIndexPath = [weakSelf.demandTableView indexPathForCell:myDemandCell];
            
            // 重点:改变Model
            [self.demandModel removeAtIndex:myDemandIndexPath];
            // ....
    // ....
}

这样,我们就实现了MVC图所倡导的这种单向数据流动:

  • UI 操作 -> 经由 View Controller 进行模型更新 -> 新的模型经由 View Controller 更新 UI -> 等待新的 UI 操作

五、总结

这样的方式写Model,真正的把Model从ViewController独立了出来,也实现了单一职责原则——Model全权负责数据,也达成了单向数据流,使整个流程不杂乱。