对于MVVM,多一些思考总是没差的

写在前面

MVC,MVP,MVVM……移动端的开发可谓是在MVX的海洋中摸爬滚打!然而,V和M的概念不说,关于P,关于VM,它为什么叫Presenter,为什么叫ViewModel?我们实践中的P,VM所做的事情真的和它们的概念对得上么?

本篇即基于MVVM在移动端的应用这一话题做一些简单的讨论,希望大家可以借以回顾自己搭过的框架,码过的代码,能唤起一些有趣更有意义的思考!

第一篇:我对“MVVM”的初识

那是在2015年的时候,MVVM被炒的火热,一日,面朝我已经尽心竭力做好了概念分组的“巨型VC”,我无力地将头专向小马哥,“编辑这部分代码我还是感到很难受……”

//某VC中
[aRequester sendPostReqWithUrl:aUrl paras:paraDic response:^(id respData, NSError *error) {
    [Error check code];
    NSDictionary *tmpDic = (NSDictionary *)respData;
    NSNumber *tmpNumber = [tmpDic objectForKey:@"boolVal"] ;  
    if (YES == [tmpNumber boolValue]) {
       _contentView.titleLable.text = [tmpDic objectForKey:@"title"] ; 
    } else {
       _contentView.titleLable.text = [tmpDic objectForKey:@"title2"] ;
    }}];

如上,很常见的,网络请求后,错误检查,解析数据并用数据更新视图。
“如此清晰顺畅无比的场景,还能如何优化呢?‘感到难受’,这也没办法啦,什么都不能做了!”我们有这样自我合理化的内心os太正常不过了,但多年的经验不断地让我印证一个真理:事出反常必有鬼(感觉不爽,必可优化)。果然!……

“那是因为你把视图和逻辑耦合到一起了!”小马哥回答。

Step1 可以将我们期望在回调中做的事情进行简单的概念分组

//某VC中
[aRequester sendPostReqWithUrl:aUrl paras:paraDic response:^(id respData, NSError *error) {
    /* Error check */
    /* Handle Parser */
    /* Update View */
}];

Step2 分别抽象出解析&更新视图的具体处理

/* Handle Parser */
- (NSString *)parserTitle:(NSDictionary *respData) {
    NSString *tmpStr = nil;
    NSNumber *tmpNumber = [respData objectForKey:@"boolVal"] ;  
    if (YES == [tmpNumber boolValue]) {
        tmpStr = [respData objectForKey:@"title"] ; 
    } else {
        tmpStr = [respData objectForKey:@"title2"] ;
    }}];
    return tmpStr;
}

/* Update View */
- (void)updateViewWithTitle:(NSString *)title {
    _contentView.titleLable.text = title;
}

Step3 方法抽象封装,并新建文件(logicModel)来承载功能简单但实现复杂的逻辑代码

//某VC中
[_logicModel requestTitleInfoWithResponse:^(NSString *title) {
    /* Update View */
}];

//封装到logicModel文件中
- (void)requestUserInfoWithResponse:(void(^)(id userInfo, NSError *error))callback {
    [aRequester sendPostReqWithUrl:(NSString *)url paras:(NSDictionary *)paras response:(id respData, NSError *error) {
        /* Error check */
        /* Handle parser */
        /* Callback */
        callback(title);
    }];
}

有什么不一样么?或许不经过实践操作中对代码维护效率精益求精地追求,很难通过上面的例子直观的理解抽象的好处。甚至会有一些浮躁的逆反心理作祟,认为这么做多此一举。

抽象/封装不一定都是好的,它们的应用要权衡地考虑某个实现模块的复杂性,从而选择一个最合适的抽象层次。而抽象/封装的最基本原则参考,我想应当是:概念合理

上面的例子即是简单地将“数据的处理”“视图的更新”相互独立起来,使视图的更新更为纯粹!简言之,我们期望避免下面场景的出现

    if (YES == [tmpNumber boolValue]) {
       _contentView.titleLable.text = [tmpDic objectForKey:@"title"] ; 
    } else {
       _contentView.titleLable.text = [tmpDic objectForKey:@"title2"] ;
    }

然后,小马哥告诉我,这就是MVVM,即当前最流行的Model-View-ViewModel模式。

我上面起名为LogicModel的文件即为小马哥所说的“VM”,它负责将网络请求、请求解析和一些数据处理逻辑进行封装,从而使VC变得“轻”一些

想象一下我们的思维走向:
1) 与视图无关的数据逻辑问题,直接往从VM入手排查
2) 与视图展示有关的逻辑问题,在VC到View的流程中一看数据对接,二看VM反馈的数据是否有误。
可谓是结点清晰,定位问题毫无压力!大赞!不愧是“MVVM”!

可是,VM就是逻辑封装自然演化的一个“代号”么?

VM = View Model,是视图的模型,“模型”一词,从概念上倾向于一种“静态”,而逻辑处理,网络请求,信号接收这些趋向于一种异步的“动态”,而且好像和“视图”的概念差别有些大。这种封装固然有它的优势所在,但MVVM的设计者干嘛对它起名为VM呢?视图的模型?叫LogicModel (逻辑模型),或者VCTool(VC工具)怎么都比VM合理吧?

只是一个名字而已嘛!然而,就我对“大牛”的理解,他们对于某种概念的名称拟定,是绝不会马马虎虎了事的

VM的本源必然就是一个VM!一个视图的模型!

第二篇:追溯MVVM的提出

或许是我对于VM的理解方向不对吧?毕竟从网上的众多文章的分析说明,从同事的实践中,我们对于MVVM又或MVP的在移动端的应用实践竟然出奇地一致!(如下图)


image.png

翻阅了几十篇相关的文章,每篇的说的颇有道理,很多文章还是分了上中下篇,并配以图示,似颇为系统的对MVC,MVP,MVVM进行介绍。不得不说,这些文章颇具指导意义,确实可以让很多限于逻辑耦合深渊的朋友找到一盏明灯,让他们的项目变得清晰而易于维护。但我还是任性地感觉,他们在打着MVVM的旗号在讲VC减负——我想要做的,是接近MVVM的本源

MVVM 最早于 2005 年被微软的 WPF 和 Silverlight 的架构师 John Gossman 提出,并且应用在微软的软件开发中。我找到了那片博文,并进行了翻译和仔细的思考探究。

《Model/View/ViewModel pattern for building WPF apps》
John Gossman
译文链接:
https://www.jianshu.com/p/b0b80163782f
原文链接: https://blogs.msdn.microsoft.com/johngossman/2005/10/08/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps/

这篇博文中,有这样这样两句有趣的话:

1)Model/View/ViewModel is a variation of Model/View/Controller (MVC) that is tailored for modern UI development platforms where the View is the responsibility of a designer rather than a classic developer.(译:MVVM是MVC模式的一个演变,针对一个视图的展示样式,比起传统的开发者,现在往往是设计师更为关注,MVVM正是为这种状况而定制的一种模式。)

2)The term means "Model of a View", and can be thought of as abstraction of the view。(译:它意为“视图的模型”,可以将它想象成一个抽像化的视图。)

那么,基于对MVVM本源的解读,我们在一个“可以被想象成视图的抽象”VM中添加大量网络,页面跳转等逻辑显然不太合适了。(它是视图,它是视图,它是视图,请这样对自己洗脑!)同时,我们也想思考下关于“让设计师去完成视图展示”这个有趣的点。

第三篇:MVVM基于WPF的应用(最初的应用场景)

WPF即Windows Presentation Foundation,是微软推出的基于Windows 的用户界面框架。

第二篇的译文中原作者举例的应用界面是Sparkle,我这边则以类似的OmniGraffle(一款原型绘制软件)的界面进行说明。(没什么特别的原因,因为我正在用OmniGraffle,更容易截图:)

image.png

如果大家阅读了第二篇提供的《Model/View/ViewModel pattern for building WPF apps》,你会发现文中举例的Sparkle界面操作栏和我举例的OmniGraffle界面圈红的部分是很相似的。那么,参照文中的VM划分方式,我们可以设计A部分对应一个ViewModel A(当然OmniGraffle不一定是这样实现的),B部分对应一个ViewModel B,然后B部分的“填充”,“笔画”,“阴影”,“形状”,“线条”亦可以分别对应5个小的ViewModel……(如下图)

image.png

一个有趣的点,View的层次叠加变成了VM的层次叠加!VM真如一个View的抽象一般!

同时,第二篇摘录的另一段译文引导的另一个问题:什么叫设计师更关注UI的页面展示?不要小看我们UI同学哦,当下很多的设计师都有css、html的开发经验,同时MVVM由微软提出,记得微软有一个自己的XAML吧?它正式一种搭建UI的语言。所以,基于MVVM的模式,我们至少可以从概念上将视图完全剥离(甚至交给UI同学去渲染与实现),模型中只要有视图中需要展示的元素的具体内容数据即可,他不关心任何视图的布局,渲染效果。

同事,针对视图的布局和效果的动态改变,我们将这些改变抽象成状态,存放在VM当中。至此,一个最最简单的MVVM元组得以实现。

image.png

第四篇:MVVM基于APP的应用

回到市面上较为流行的一种类“MVVM实践模式”,它们以“MVC+VC减负+概念抽象封装”作为基本的思路参考,让VC作为VM和View沟通的主桥梁

如下图,一般是一个VC包含一个VM和一个主View,然后VM或许会处理少量“双向绑定的任务”,同时也可能将更多的比如网络答复的操作动作回调给VC去处理视图更新

image.png

这种模式易于理解也确实可以实际的提高代码的概念性和可维护性。但我们发现,视图的更新走了两条长线
1) 介由VM的绑定实现模型更新视图;
2) 借由VM的回调实现VC控制更新视图。

这总让我们感到不够清爽:当我希望将视图中的一段文字由“我的领导是个坏人”改为“我的领导是个好人”时候,没有明确的概念告诉我哪一条“线”是有决策力的“线”(可以成功进行修改的线)。

“看代码不就知道了?”
请记住:
1 有思想的代码几乎不需要透过代码来定位问题
2 维护代价的“积累”不是“叠加”而是“逻辑分支的叠乘”(每一次“选线”的犹豫,都是一层逻辑分支)

所以,看代码当然可以解决问题!甚至针对复杂的工程,你大可花费一个月将它的每个细节流程完全理透!然后心满意足的大赞自我的耐心和代码阅读能力!不想,领导已经看到了那句你还没有来得及改掉的“我的领导是个坏蛋”……

言归正传,第三篇我们基于MVVM在WPF中的应用分析貌似还蛮顺畅的,但好像应用再APP中,有什么地方有些……怪!根源在哪里?或许如下几个问题可以作为我们的参考:
1) VC的地位到底更倾向于什么?是V?是C?是VM?
2) 网络请求/视图生命周期/路由跳转这些在APP端大量出现的概念模块,它们在MVVM中有着怎样的概念归属?

4.1 MVVM基于APP的基础架构&模块分工

我们尝试一下下面的这套交互结构

image.png

首先我们明确一个点,在一个MVC结构中,即便抛开视图后,模型和控制器处理的大部分业务逻辑,都是为视图服务的。我们常说的“重VC”,很大一部分重在视图相关的逻辑或是为之服务的逻辑

所以,当我们抽象一些视图的基础模型,并通过VM将视图本身的(不需要与外界交互的)状态变迁逻辑封装在一个MVVM组的内部,对外(对VC)只暴露必要的数据更新和消息回调接口。繁琐的视图逻辑就可以被限制在一个MVVM当中(它确实也应当在那里)。这时留在VC中的逻辑,一般情况下就很少了。如果此刻的VC还让你感到“重”的话,我们大可再对其抽象一个VC-Logic,将复杂的逻辑进行封装。

各个模块所负责的主要工作可以参考下图

image.png

如图,VC中的“生命周期控制”,“网络请求”,“路由”,View中的“视图布局”,“控件效果”都很好理解,让人一眼摸不清的概念主要存在所谓的VM当中,我们来简单说明:

1)什么是“处理视图状态”?
视图可能根据不同的状态有不同的展示内容,甚至展示效果。我们常见的“cur”(current)前缀就适用于说明这种场景。“当前选择的模块”,“某个按钮当前的选择状态”,这些表示视图状态的操作变量的定义应当在VM当中,相关的逻辑交互也应当在VM当中。如果说View提供了视图的所有展示元素;那么VM则可以确定某个视图模块某一时刻某一个状态下的呈现内容

2)什么是“处理视图协作”?
一个VM不一定只和一个View存在关联,它可能同时协调多个视图。
我们以同程旅行的一个筛选界面作为参考场景进行说明:

image.png

当我们将“4.5分以上”后面的对号勾上的时候,上面的“4.5分以上”会被同步勾取,同时,“评分”后面会多出个小绿点,这表示评分这页的筛选条件选择的不是默认的“不限”。很显然,关键词模块、筛选分类模块、筛选详情模块正常人都会分成3部分视图绘制。这三个视图间显然是有交互关系的(即“筛选详情模块”的勾选触发了“关键词模块”的高亮和“筛选分类模块”的加点),而VM即是处理这种交互关系理想场所。

3)什么是“数据绑定”?
这边特指将一个模型数据和视图中的一个展示内容进行关联绑定;
单向绑定一般指模型数据变化触发对应的视图数据变化
双向绑定指模型数据,视图数据任意一方变化,都会触发另一方的同步变化。

4)什么是“数据转换”?
我们不能企望所有的模型数据都能直接被视图使用,比如模型中是一个BOOL(0/1)值,而对应的视图展示期望为“是”/“否”,类似这样的数据转化工作,交给VM吧!

4.2 MVVM基于APP的抽象讨论

我们再来讨论一下几个观点的理解:
1)VC是特殊的VM
很常见的,VC中除了主要的展示视图外,还有一个导航条(NavigationBar),而我们又很常见导航条要根据主视图的滚动而改变展示效果(比如随着视图滚动变得透明),这种视图的交互显然只能在VC中处理。这很正常,VC可以理解为特殊的VM,即它会负责一些类似VM的协调工作(协调本身也是C的职责),亦会负责VC的其他本职工作(如控制视图生命周期等)

2)VM的是可以存在类似View的层次的
写视图,Subview(子视图)的概念是逃不掉的,而参照“将VM理解为视图”的思路,复杂视图中,VM的层次也是逃不掉的,像图中一样。

image.png

大家会发现,我在VC下面标明了“mainVM”,在一些MVVM下面标明了“mini”,mainVM好理解,因为前面我们已经引入了“VC是特殊的VM这一思路”,但是mini呢?

大多数场景,我们一个页面的视图交互不会特别复杂,所以,一般的多层视图,只用一个VM管理就够了。但有时我们会希望对视图层中的小模块进行MVVM封装,因为它是“通用”的(希望被复用的),通用的小视图模块往往是“简单”的,这是mini的第一层含义。

同时,当我们没有引入VM概念的时候,View就单纯地是View么?想想UIButton吧,可以设定选择状态不说,它还可以随时获取当前按钮的选择状态(selected),这不就是说UIButton保存了视图状态么!如果把UIButton进行细致的概念拆分,不就变成了我们的MVVM组么!所以,我们很多系统的视图控件,本来就可以理解为mini的MVVM

VM从某种角度上讲,就是一个视图!

第五篇:RAC对iOS实践MVVM的价值

很显然,上面的讲述中,我们只字未提到RAC(ReactiveCocoa),所以,RAC本身是和MVVM没有本质上的关联的。但无可反驳的是,使用RAC确实能让MVVM的实践上显得更加精巧。

5.1 快速绑定

我们在应用中运用的视图更新接口,block回调,代理,通知,KVO,目标动作对……都可以理解为广义“绑定”所依赖的技巧RAC则将上述机制统一成“消息”,可以让我们以更简单的方式处理绑定动作。请看下面的例子。

1)常规方式:双向绑定一个字符串和一个textField的text值

/* 1. 使textField中的text改变时,字符串textStr可以同步变化 */
[_textField addTarget:self action:@selector(valueChanged:) forControlEvents:UIControlEventEditingChanged];

- (void)valueChanged:(UITextField *)textField {
    _textStr = _textField.text;
}

/* 2. 使textStr改变时,textField中的text可以同步变化 */
[self addObserver:self
       forKeyPath:@"textStr"
          options:NSKeyValueObservingOptionNew
          context:nil];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    if (object == self && [keyPath isEqualToString:@"textStr"]) {
        _textField.text = _textStr;
    }
}

2)RAC方式:双向绑定一个字符串和一个textField的text值

/* 1. 使textField中的text改变时,字符串textStr可以同步变化 */
RAC(self, textStr) = _textField.rac_textSignal;
/* 2. 使textStr改变时,textField中的text可以同步变化 */
RAC(self.textField, text) = RACObserve(self, textStr);

代码的简化是显而易见的。

5.2 多元监听

iOS中对于代理的应用场景还是很多的。而基本的代理模式中,某个模块的代理者只能有一个。为了让多个对象同时接收代理消息,我们不得不修改模块结构,又或者自定制一个自以为很简单完美的代理队列,又或将代理改用通知?!(不想玩死自己的话,放弃在局部使用这种思路吧!)甚至,还有更奇葩的设计
然而,在RAC中很简单。

下面的代码即实现了textStr和textStr2同时监听textField的text的变化
(处理代理一样的简单,因为RAC全部将其抽象成为了“消息”)

    RAC(self, textStr) = _textField.rac_textSignal;
    RAC(self, textStr2) = _textField.rac_textSignal;

但是,应用中的意义呢?
将我们封装的VM可以理解为一个模块,对一个模块而言,没什么比输入输出接口的设计更加重要了。而互联网时代,神奇的需求变动在很多场景下让我们不得不对模块进行更新,甚至更新模块的对外接口。多元监听的支持可以大大降低模块对外接口更新的复杂性。(接口的更新很容易牵连整个模块的基础框架,更细节的分析在此不再赘述)

5.3 元组的引入

元组,并不是一个让人感到陌生的概念,它即代表一组约定的有序数据,该组数据中每个数据的数据结构不需要统一。
在MVVM中(其实普通的视图设计中也是),为了方便视图的展示,我们常常要约定一些轻量级的纯视图数据结构。这时候Tuple或许会是最契合我们场景的概念。

为什么?
1)Tuple的概念定位不同于Array,tuple的长度一般是确定的,tuple组内每个元素的类型不要求一致
2)Tuple的概念亦不同于Dictionary,tuple无须将一个数组分为key,value两个部分(繁琐,麻烦~),同时,tuple是有序的(字典是无序的)。

当然,介于tuple的灵活性特征,使用场景一定要控制在小范围,需要定义对象的时候,还是要定义的!万万不可将tuple在不可控的大范围使用。(一样是玩死自己的行为)

5.4 没有RAC不能应用MVVM?

我不这么认为:
1)如我们之前说过的,MVVM与RAC没有本质的关联
2)如5.1~5.3,RAC可以使我们应用MVVM的一些场景变得更为简单优雅,RAC针对MVVM优化的问题在我们不使用MVVM时依然也存在(你用MVC是有些场景一样要用KVO),而且,这些场景不算是决定性的(双向绑定的实践应用场景其实很少)

结语

这篇文章的准备在一个月前,中间因为主工作项目原因有各种间断和搁置,但也庆幸有这样的项目需求,可以让我将其中部分的思路得以实践和印证。相信这篇从MVVM的提出为出发点,经过了反复思考印证,将模块的概念分工多次推倒重组的文章,可以真正为大家对MVVM理解上提供有价值的思路参考,为实践中遇到的一些让人感到不舒服的代码的优化方向上提供有价值的思路参考,为MVVM在移动端的应用实践提供有价值的思路参考!

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

推荐阅读更多精彩内容