实际项目中的MVVM(积木)模式第二章:model--如何高效利用数据层

<em><strong>“不为炫技而炫技,不为架构而设计架构,只为写出一个接地气、通俗易懂的使用方法”</strong></em>

(注:了解整个设计模式体系请查看我上篇文章实际项目中的MVVM(积木)模式:序章

这篇文章讲解Model和ViewModel。

本着易扩展、易理解的前提,讲解中Model和ViewModel都用最基础的方法和易理解的思维图。

为何为何会将View和ViewModel合并在一起讲解?为何我会将有些文章说的的网络层与数据层合并一起叫数据层?

很简单,我们需要明白一个道理,无论是网络请求还是本地缓存,本质上都是传递数据;因此,我们要做的就是将各种来源的数据通过数据加工(ViewModel)形成统一的格式(Model)再通过一个统一的接口传递给需要的地方。我所讲的数据层,我把这形象地叫为数据工厂。

数据工厂

首先,我们先开始说说:Model。
<strong><h4>一、Model--项目的信息承载与传递者</h4></strong>
一个多人协同开发的项目,保证数据结构的一致性和稳定是很有必要的。而Model则是很好的实现了这一需求。
首先,我们先通过三个大的方面将字典与Model作一个比较,更直观了解Model的特点。

<strong>1、字典与模型的比较</strong>

<strong>a、取值:</strong>字典会因为没有取值的这个key或者这个错误的key刚好是这个字典中其他类型的值对应的key(因输入错误等原因),通过这个key取出来的值为nil或者其他类型的值,如赋值给label之类的文字控件,可能会导致程序崩溃,而Model不会出现这样的问题;
<strong>b、数据展示:</strong>字典无法再不改变数据源的前提下,改变数据的格式,而Model则可以通过get方法实现这个需求,保证了数据的原始性和可变性;
<strong>c、后期维护:</strong>字典每个key的具体含义和有多少key要通过接口文档去了解,而Model体现在具体的属性和每个属性的备注上;
可能有同学会问建立Model一个个去复制粘贴属性好麻烦,还有像数据缓存之类还要一个个写解挡归档好麻烦呢/(ㄒoㄒ)/~~
这么多好的优点的前提下,这几个小麻烦肯定会通过方法解决噻,且看下面:

<strong>2、解决Model的一些小麻烦</strong>

<strong>a、如何快速建立Model:</strong>这里有份代码,可以将网络请求下来的字典里的key在控制台打印成Model里的属性格式哦。(打印效果在代码块下面)

- (void)writeInfoWithDict:(NSDictionary *)dict
{
    NSMutableString *strM = [NSMutableString string];
    
    [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        //  NSLog(@"%@,%@",key,[obj class]);
        
        NSString *className = NSStringFromClass([obj class]) ;
        
        if ([className isEqualToString:@"__NSCFString"] | [className isEqualToString:@"__NSCFConstantString"] | [className isEqualToString:@"NSTaggedPointerString"]) {
            [strM appendFormat:@"@property (nonatomic, copy) NSString *%@;\\\\n",key];
        }else if ([className isEqualToString:@"__NSCFArray"] |
                  [className isEqualToString:@"__NSArray0"] |
                  [className isEqualToString:@"__NSArrayI"]){
            [strM appendFormat:@"@property (nonatomic, strong) NSArray *%@;\\\\n",key];
        }else if ([className isEqualToString:@"__NSCFDictionary"]){
            [strM appendFormat:@"@property (nonatomic, strong) NSDictionary *%@;\\\\n",key];
        }else if ([className isEqualToString:@"__NSCFNumber"]){
            [strM appendFormat:@"@property (nonatomic, copy) NSNumber *%@;\\\\n",key];
        }else if ([className isEqualToString:@"__NSCFBoolean"]){
            [strM appendFormat:@"@property (nonatomic, assign) BOOL   %@;\\\\n",key];
        }else if ([className isEqualToString:@"NSDecimalNumber"]){
            [strM appendFormat:@"@property (nonatomic, copy) NSString *%@;\\\\n",[NSString stringWithFormat:@"%@",key]];
        }
        else if ([className isEqualToString:@"NSNull"]){
            [strM appendFormat:@"@property (nonatomic, copy) NSString *%@;\\\\n",[NSString stringWithFormat:@"%@",key]];
        }else if ([className isEqualToString:@"__NSArrayM"]){
            [strM appendFormat:@"@property (nonatomic, strong) NSMutableArray *%@;\\\\n",[NSString stringWithFormat:@"%@",key]];
        }
        
    }];
         NSLog(@"\\\\n\\\\n%@\\\\n",strM);
}

看下图打印效果,那么打印出来的效果大家知道了吧,直接复制粘贴就OK啦:

打印效果

<strong>b、怎么解决繁琐的解挡归档呢</strong>

在BaseModel(Model的基类)中写一个统一的解挡、归档,这里就要用到runtime中非常有用的两个方法:

class_copyIvarList(Class cls, unsigned int *outCount) //遍历该类成员变量列表
ivar_getName(Ivar v) //获取该类某个成员变量的名字

那么具体怎么在基类写一个,所有适用呢,且看下面:

- (void)encodeWithCoder:(NSCoder *)encoder
{
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    
    for (int i = 0; i<count; i++) {
        Ivar ivar = ivars[i];
        const char *name = ivar_getName(ivar);
        // 归档
        NSString *key = [NSString stringWithUTF8String:name];
        id value = [self valueForKey:key];
        [encoder encodeObject:value forKey:key];
    }
    
    free(ivars);
}

- (id)initWithCoder:(NSCoder *)decoder
{
    if (self = [super init]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([self class], &count);
        for (int i = 0; i<count; i++) {
            Ivar ivar = ivars[i];
            const char *name = ivar_getName(ivar);
            // 解档
            NSString *key = [NSString stringWithUTF8String:name];
            id value = [decoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        free(ivars);
    } 
    return self; 
}

至此,所有继承于这个Model基类的Model都自动实现了解档归档的方法。
既然解决了建立Model的一些小麻烦,我们就来构建一个项目中标准的BaseModel(基类模型)。

<strong>3、何为基类Model建立要求?</strong>

<strong>a、数据格式读取统一与写入统一;</strong>
<strong>b、模型属性值可批量修改;</strong>

这样才能保证在“千奇百怪、朝令夕改”的数据源中,进入到这个项目体系后,面向业务工程师的时候,是统一整齐的标准模型,然后业务工程师才会在这个基础之上扩展其他子Model。
其中读写统一则是通过上面的解档归档解决,而模型属性值批量修改则是通过李明杰大神的MJExtension(这个三方库可以在不用继承其他Model前提下使用,保证了Model独立性),具体代码在BaseModel如下:

- (id)mj_newValueFromOldValue:(id)oldValue property:(MJProperty *)property
{
    if ([property.name isEqualToString:@"buy_price"]|| [property.name isEqualToString:@"buy_sale_price"]||[property.name isEqualToString:@"info_price"]||[property.name isEqualToString:@"sale_price"] ) {
        if (oldValue == nil || [oldValue isKindOfClass:[NSNull class]])
            return @"价格面议";
    }//可将所有模型在生成时将涉及相应字段的值变成“价格面议”
     else if (property.type.typeClass ==[NSString class]) {
        if (oldValue == nil || [oldValue isKindOfClass:[NSNull class]])
            
            return @"--";//可将所有模型在生成时将string类型从nil或Null变成“--”
    } else if (property.type.typeClass == [NSArray class]) {
        if (oldValue == nil)
            return [[NSArray alloc] init];//可将所有模型在生成时将array类型从nil变成[[NSArray alloc] init]
    }
    
    return oldValue;
}

以上代码举了部分替换的例子,目的是告诉大家通过这方法可以在数据源传给客户端是非友好数据的时候,我们客户端能够进行处理,给业务工程师一个友好的数据。这对数据工厂这个模式来说是非常有必要的。
<strong>至此,BaseModel就只有写两个方法:解档与归档 以及 属性值批量修改。因为我们始终要明白:Model是来保证整个项目架构数据结构的一致性和稳定性。那么继承这个BaseModel的子Model就都具备了面向业务工程师友好数据的特点。</strong>

那么如何正确使用子Model呢?
我举两个例子:

<strong>1、如何将模型源数据的时间 20161010 变成 2016-10-10(正确使用get方法)</strong>

- (NSString *)pub_date{
    
    return [_pub_date formatDateString];//formatDateString是NSString的Category
    
}

这样的好处有两方面:一方面,业务工程师不会修改源数据,保证了源数据的安全性;另一方面,类似formatDateString的方法,是通过Category(也就是我后面要说的工具类)使用的,保证了代码的低耦合性。

<strong>2、使用子Model分离数据的一个例子(这个例子感谢我的同事 张尔柏 同学提供,展示这个例子主要目的是为了表达子Model其中一个在cell样式数据分离的作用,可能会因为没有demo,大家不太好理解。所以,大家可以在后期demo上传后再次详细了解)</strong>

这里我们举个tableView中Cell显示(整个tableView的demo将会在View篇结束后放出),其中Model要做的事情,我们先看在Model中的代码:

- (void)setupInfo {
    self.xibName = @"SaleInfoTableViewCell";/指定Cell的Cell样式
    self.cellHeight = @(76);//指定Cell的高度
    self.ideltifier = @"cell";//指定Cell的ideltifier
}

通过在Model生成时执行这个方法,实现了Cell样式与样式数据分离,做到了每个Cell的View样式与Model的绑定。

<strong>在讲解Model的结尾,总结起来就是:Model存在的目的是为了给业务工程师一个友好稳定的数据,让业务工程师在相应的模块内独立地做相应的数据操作。</strong>

<strong><h4>二、ViewModel--做一个优秀的数据工厂</h4></strong>
如文章开始的图就知道,ViewModel更像一个食品工厂一样,将不同的原料通过不同的制作工艺产出为统一的产品。
那么作为工厂的框架BaseViewModel应该是怎样的呢?
首先我们应该先想到,我们的原料(数据)来自哪里?
基本都是来自网络了噻!
既然来自网络,那么就明确了BaseViewModel应该实现三个事情:网络通信、上传、下载。(都用af第三方库实现)
废话不多说,直接上代码:

<strong>1、BaseViewModel实现的三个方法</strong>

<strong>网络请求:</strong>

- (void)serviceNetWorkWithUrlStr:(NSString *)urlStr//请求网络地址
                          Params:(NSMutableDictionary *)params//请求参数
                         Success:(void(^)(id result))successBlock
                         Failure:(void(^)(NSError *error))failBlock {
    AFHTTPRequestOperationManager * manager = [AFHTTPRequestOperationManager manager];
    manager.requestSerializer.timeoutInterval = 10;//请求时长
    manager.requestSerializer = [AFHTTPRequestSerializer serializer];
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
//如果需要加解密,可引入加解密的工具类将params加密实现
    [manager POST:urlStr parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
        successBlock(responseObject);
        
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"%@", [error localizedDescription]);
        failBlock(error);
    }];
    
}

<strong>上传:</strong>
该上传方法需要求能同时多传,且传不同类型的文件,以适应不用的场景需要

- (void)uploadFileWithfileList:(NSMutableArray *)params//存放上传文件的数组
                      Option:(NSDictionary *)optiondic//请求参数
                       Url:(NSString *)requestURL//上传网络地址
                         Success:(void(^)(id responseObject))successBlock
                         Failure:(void(^)(NSError *error))failBlock
                         progress:(void (^)(float progress))progress{
    AFHTTPRequestOperationManager *manager=[AFHTTPRequestOperationManager manager];
    //设置返回的数据解析格式
    manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"];
    manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"application/json"];
    AFHTTPRequestOperation *operation = [manager POST:requestURL parameters:optiondic constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
       //下方举了一个极端例子:当数组里存放了不同类型的文件如何上传
        for (int i = 0; i<params.count; i++) {
            if ([params[i] isKindOfClass:[UIImage class]]) {
                
                UIImage *image = [params[i] imageCompressForWidth:params[i] targetWidth:375];
                NSData *imageData =UIImagePNGRepresentation(image);
                [formData appendPartWithFileData:imageData name:@"file_content" fileName:[NSString stringWithFormat:@"anyImage_%d.png",i] mimeType:@"image/png"];
            }else if ([params[i] isKindOfClass:[NSString class]]) {
                NSURL *url = [NSURL fileURLWithPath:params[i]];
                NSData *data = [NSData dataWithContentsOfURL:url];
                [formData appendPartWithFileData:data name:@"file_content" fileName:@"11.aac" mimeType:@"audio/x-mei-aac"];
                
            }else if ([params[i] isKindOfClass:[NSURL class]]) {
                NSData *data = [NSData dataWithContentsOfURL:params[i]];
                [formData appendPartWithFileData:data name:@"file_content" fileName:@"11.aac" mimeType:@"audio/x-mei-aac"];
            }
            
        }
    } success:^(AFHTTPRequestOperation *operation, id responseObject) {

        successBlock(responseObject);
        
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        failBlock(error);
    }];
    //获得上传进度
    [operation setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
        NSLog(@"百分比:%f",totalBytesWritten*1.0/totalBytesExpectedToWrite);
        progress(totalBytesWritten*1.0/totalBytesExpectedToWrite);
        
        
    }];
}

<strong>下载:</strong>

- (void)downloadFileWithOption:(NSDictionary *)paramDic//请求参数
                 withUrl:(NSString*)requestURL//下载地址
                     savedPath:(NSString*)savedPath//保存路径
               downloadSuccess:(void (^)(id responseObject))success
               downloadFailure:(void (^)(NSError *error))failure
                      progress:(void (^)(float progress))progress

{
    AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
    NSMutableURLRequest *request =[serializer requestWithMethod:@"POST" URLString:requestURL parameters:paramDic error:nil];
    AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc]initWithRequest:request];
    [operation setOutputStream:[NSOutputStream outputStreamToFileAtPath:savedPath append:NO]];
    [operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
        float p = (float)totalBytesRead / totalBytesExpectedToRead;
        progress(p);//下载进度
        NSLog(@"download:%f", (float)totalBytesRead / totalBytesExpectedToRead);
        
    }];
    
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
        success(responseObject);
        NSLog(@"下载成功");
        
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        failure(error);
        
        NSLog(@"下载失败");
        
    }];
    
    [operation start];
    
    
}

<strong>2.如何写好一个子ViewModel</strong>

不知道大家注意到没有,其实我们真实的项目中,网络请求返回来的状态其实可能是会存在三种状态的:<strong>请求成功;请求失败,服务器返回错误信息;请求失败,网络不通。</strong>同时,<strong>我们会在某些地方做缓存读写</strong>。那既然如此,继承BaseViewModel的子ViewModel的对外接口(<strong>唯一对外接口</strong>)应是如下所写:

- (void)serviceNetWorkMessageListWith
                             Success:(void(^)(id responseObject))successBlock//请求成功回调
                         ReceiveFail:(void (^)(id responseObject))ReceiveFailBolck//请求失败,服务器返回错误信息
                             Failure:(void(^)(NSError *error))failBlock//请求失败,网络不通 {
    NSMutableDictionary *tempDic = [@{@"service_code":Message_Service_Code} mutableCopy];
    [tempDic  addEntriesFromDictionary:[self getPrivateParameters]]//所有接口的公共访问参数;
    [tempDic addEntriesFromDictionary:@{"":""}//对应接口的对应参数];
//这里举个Archive做数据缓存的读取与存储的例子,方便大家理解在某些场景下需要某个ViewModel作缓存时,如何只对外提供一个数据接口,里面作多级缓存的原理,具体实现大家可以结合自身优化,比如数据库的缓存,比如在这个类单独归一个方法。
if ([[NSUserDefaults standardUserDefaults] objectForKey:Message_Service_Code]) {
                NSArray *priarr = [[NSUserDefaults standardUserDefaults] objectForKey:Message_Service_Code];
                NSMutableArray *priMuarr = [NSMutableArray arrayWithCapacity:0];
                for (NSData *data in priarr) {
                    
                    MessageInfoModel *message =  [NSKeyedUnarchiver unarchiveObjectWithData:data];//解档
                    [priMuarr addObject:message];
                }
                successBlock(priMuarr);//返回缓存数据
            }
    [self serviceNetWorkWithUrlStr:[UrlGenerator queryNewMessageUrl] Params:tempDic Success:^(id responseObject) {

 if ([responseObject[@"code"] isEqualToString:@"0000"]) {
            
            
            NSArray *array = [MessageInfoModel mj_objectArrayWithKeyValuesArray:responseObject[@"data"]];
            NSMutableArray *enArr = [NSMutableArray arrayWithCapacity:0];
            for (MessageInfoModel *messageModel in array) {
                
                
                NSData *data = [NSKeyedArchiver archivedDataWithRootObject:messageModel];
                [enArr addObject:data];//归档
                
                
            }
            [[NSUserDefaults standardUserDefaults] setObject:enArr forKey:Message_Service_Code];
            successBlock(array);//返回网络请求数据
            
            
        }else {
            
            ReceiveFailBolck(responseObject[@"msg"]);
        }
        
        
    } Failure:^(NSError *error) {
        failBlock(error);
    }];
}

<strong>3、到底ViewModel放在哪里合适?创建多少个ViewModel合适?</strong>

开门见山直说,个人认为只要涉及数据的View的模块,最理想的情况应该一个View模块一个ViewModel。
因为,只有这样,在后期项目越来越庞大,维护的人员越来越多的时候,才能保证模块之间的绝对独立。
(如果是一两个人开发的中小型项目,也没必要一个模块对一个ViewModel,本身功能不多,没有必要。这时可以一个Controller对一个ViewModel,或者几个同需求功能的Controller对一个ViewModel。项目死的,人是灵活的,具体情况具体分析。)

我举两个例子:

a.某个模块因为维护人员频次多,需求改动频繁,到最后需要大改某个模块的时候,因为之前的至上到下(数据到界面)的绝对独立,可以完全抽出来,重新制作一个新的模块,重新替换进去;
b.某些模块需要数据缓存,某些模块又需要网络状态判断(含大图片展示的模块),完全可以针对不同情况针对这些相应模块对应的ViewModel数据工厂做相应的事情。
以上两个例子,我想做过项目的,尤其是遇到频繁更改需求的,应该是深有体会(哎,我本人就特别有体会/(ㄒoㄒ)/~~)。


借用一位大神的话结束Model篇:

<em><strong>“你必须得清楚你要做什么,业务方希望要什么。而不是为了架构而架构,也不是为了体验新技术而改架构方案。以前是MVC,最近流行MVVM,如果过去的MVC是个好架构,没什么特别大的缺陷,就不要推倒然后搞成MVVM。”</em></strong>

后面几天我在各位大神的建议下会不断优化文章各个细节,欢迎多多关注,共同学习。几天后会发出<strong> view--业务与界面结合的模块开发 </strong>请多关注。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容