iOS_Model层到底怎么用?

最近在读App架构方面的书。对这个感兴趣是由于我意识到:程序员

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

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


1、标准的Model使用

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

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

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

2、现状

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

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

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

用个人例子举例说明这两个问题。如今咱们实现了下图的一个服务列表。框架

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

@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];


复制代码

3、阐述问题

如今就是这么一个经过列表展现数据,而后能进行删除操做的状况。那么这有什么问题呢?

首先,就是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大神的文章里,他列举了两个场景证实他的观点,能够去看一下。

4、到底怎么改进,更好地使用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 操做

5、总结

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

相关文章
相关标签/搜索