iOS使用RAC实现MVVM的正经姿势
从MVC到MVVM
前言
MVVM是微软于2005年开发出的一种软件架构设计模式,主要是为了在WPF和Silverlight中更简单的对UI实现事件驱动编程。在WPF和Silverlight中,通过MVVM成功的实现了UI布局和数据逻辑的剥离。虽然WPF和Silverlight最后都没有推广开来,但是还是让大家看到了MVVM设计模式的优秀之处。
我有幸在早年参加过Expression Blend的自动化测试工作,期间做了不少WPF和Silverlight的App,算是较早一批接触熟悉MVVM的天朝码农了。在iOS平台出现了可以优雅实现MVVM的RAC时,着实激动了一下。下面就让我们先从最早的MVC开始慢慢说起。
如果你想简单点直接看代码:Show you the code。
MVC理想设计模式
MVC是一种比较古老软件架构设计模式,主旨是将代码分为UI、数据和控制逻辑三大部分:
18-A
一个UI交互的整体过程:View接受用户操作发送给Controller,Controller根据操作对数据进行修改,Controller接受数据修改的通知,并根据通知更新对应的UI。当然Controller可能有一些自有逻辑会修改数据或者更新UI,从属关系上来说View和Model都属于Controller。
MVC实例
这是我比较喜欢的一个实例,实现一个简单的登录界面。先罗列一下简单的需求:
- 用户名有效长度为4-16位,无效时对应文本框显示为红色底色,有效时文本框显示为绿色底色,无输入时显示为白色底色。
- 密码有效长度为8-16位,对应文本框底色逻辑与用户名文本框一致。
- 登陆按钮在用户名和密码均有效时可用,否则禁用。
18-B
为了让代码看起来不那么多,我使用xib来绘制了简单的UI并完成了IBOutlet和delegate等的绑定。
然后呢需要写的代码就是大概下面这样了:
18-C
这里的username
和password
两个属性可以看作Model层,文本框和按钮的xib就是View层,VC主体代码就是Controller层。可以看到所有的Model修改逻辑和UI更新逻辑都是在Controller里一起完成的。(完整代码)
MVC解决的问题和优缺点
- 代码成功分化为UI、数据和控制逻辑三大部分。
- 易于理解使用,普及成本低。
- Controller拥有View和Model,几乎可以控制所有逻辑。
- 细节不够明确,基本上不明确归属的代码全部会放在Controller层。
- 和UI操作事件绑定较重,难以进行单元测试。
MVC实际使用状况
因为上一节中提到的3和4两点,很多代码都只能写在Controller层。还因为xib的特殊性,对多人协作十分不友好,导致大部分UI的布局和初始化代码要用代码实现,而这些代码写成单独的类也多有不便,导致本该出现在View层的代码也堆积在了Controller层。而且在iOS中,UIViewController和UIView本来就是一一对应的。这就导致了MVC从最早的Model-View-Controller最终一点点变成了Massive-View-Controller:
18-D
MVP设计模式
所谓设计模式,就是软件设计过程中为了解决普遍性问题而提出的通用解决方案。MVP的出现就是为了解决MVC的Controller越来越臃肿的问题,进一步明确代码的分工:
18-E
这个图看上去和MVC很相似,但是这里的实虚线和MVC设计模式不同。所表示的意义为View层持有Presenter层,Presenter层持有Model层,View层并不可直接访问到Model层。整体的UI交互流程和MVC类似。
这么做的意义就在于真正意义上的将UI逻辑和数据逻辑隔离,而隔离之后就可以更方便的对数据逻辑部分进行单元测试,隔离的另一个好处就是解开了一部分的耦合。
MVP实例
接着刚刚的实例,我们在它的基础上继续进行修改。
首先我们需要定义一个Presenter,头文件内把所有可接受的用户操作和更新UI需要用的回调定义好:
18-F
Presenter的内部实现:
18-G
可以看到Presenter做的事情就是把原来Controller的逻辑控制相关代码抽离出来构建成一个单独的类。接下来看一看对应的Controller现在变成什么样:
18-H
现在Controller的代码变得更加清晰了:两个更新数据的调用,三个更新UI的调用,多了一些初始化Presenter的操作。
因为现在Presenter只包含逻辑,所以我们也较容易实现一个单元测试:
18-I
从结果可以看到Controller的代码转移了一部分到Presenter,MVP也成功把逻辑和UI代码分离了。(完整代码)
MVP优缺点
- UI布局和数据逻辑代码划分界限更明确。
- 理解难度尚可,较容易推广。
- 解决了Controller的臃肿问题。
- Presenter-Model层可以进行单元测试。
- 需要额外写大量接口定义和逻辑代码(或者自己实现KVO监视)。
MVVM设计模式
随着UI交互越来越复杂,MVP本身的一些缺点还是会暴露出来。
比如虽然是可以写单元测试,但是单元测试写起来还是有很多“啰嗦”的部分,需要模拟一些假的UI处理逻辑来进行结果的验证,即使用block写法这个部分的代码量也省不了太多。
所有的用户操作和更新UI的回调需要细细定义,随着交互越来越复杂,这些定义都要有很大一坨代码。
逻辑过于复杂的情况下,Present本身也会变得臃肿难以重用,代码也会变的更加难以阅读和维护。
这时候,MVVM出现了,为了解决以上大部分问题:
18-J
首先ViewModel-Model层和之前的Present-Model层一样,没有什么大的变化。View持有ViewModel,这个和MVP也一样。变化主要在两个方面:
- ViewModel相较于Present,不仅仅是个逻辑处理机,它附带了自己的状态,所以被才可以被称为“Model”。ViewModel也因为这个变的更加独立完整,我们更容易通过ViewModel的状态去进行单元测试。Presenter在没有设置回调的时候其实一直在做空运算而已,运算得到的值没有进行存储,下次必须重新运算。
- View不直接通过传递用户操作来控制ViewModel,ViewModel也不直接通过回调来修改View。对常用的数据和UI控件的事件&属性,MVVM框架的底层均进行了封装,使得我们可以进行数据绑定操作。简单来说我们可以用类似
[viewModel.username bind:usernameTextField.text]
类似的操作使得viewModel的属性和UI控件的属性相互绑定,其中一方修改的时候另一方直接自动做对应更改。这样的话我们就不用重复的书写很多回调操作,也不用处理一大堆UI控件的delegate事件。
其实MVVM的精华小部分在ViewModel,更大部分就在数据绑定,甚至有很多人觉得应该称MVVM为MVB(Model-View-Binder)。
数据绑定引申出来的一个概念就是数据管道(转换器),这个和大家学的数字电路比较相似:
18-K
这里我们有ABC三个数据源和两个双输入的转换器,我们可以进行组合得出各种想要的结果(如上图),甚至于我们可以多次组合来完成更复杂的计算(如下图):
18-L
这里的转换器就带来了第三点改进:
- 基于数据绑定和数据管道,可以对运算逻辑进行拆分和重用,最大程度的使代码易读易维护。
MVVM实例
还是接着刚刚的工程,首先要参照Reactive Cocoa的文档把RAC添加到工程里。
ViewModel的定义
然后我们首先要把Present改造成ViewModel:
19-A
这里可以看到作为ViewModel输出值的属性设置成了readonly,剩下的username
和password
是输入值。
单元测试
值得一提的是软件工程中最好是测试驱动开发(TDD)而不是写完逻辑再补测试,所以我们先改好单元测试:
19-B
从单元测试也很容易看出来ViewModel现在足够独立并易于测试。
View层和ViewModel层的绑定
我们再看一眼现在Controller应该怎么写:
19-C
首先看到原来的一行loginButton
初始化代码没有了,因为数据绑定是自动更新的,初次绑定就会初始化状态。
对ViewModel进行输入数据的绑定,不再需要写UITextFieldDelegate然后再传递事件,一行代码完成绑定。
同样将ViewModel的输出数据绑定到UI,不需要再实现对应的回调,一样一行代码完成绑定。
这就是MVVM设计模式在最理想的情况下,Controller里需要和ViewModel交互的所有代码内容。
数据管道(转换器)
现在来说说刚刚的ConvertInputStateToColor
,它其实就是一个状态到颜色的转换器:
19-D
19-E
这里利用RACSignal的map方法做了一个映射,这就是我们的转换器。当然我们以后也可以实现别的转换器来进行方便的替换,比如实现一个仅在有效态显示绿色其他状态都显示白色的转换器。另外这个转换器如果写的更通用点,也可以被别的模块重复使用。
ViewModel的UI无关性/转换器组合的多样可能性
这里要提一下为什么ViewModel不直接提供颜色值的输出:
- ViewModel应该不关心具体的UI相关逻辑,只关心自己的逻辑正确和独立完整性。
- 易于进行单元测试,枚举当然比颜色值好检查点……
- 提供更为基础的状态,这样和不同的转换器组合会产生更多的可能性。
这里的可能性指什么呢?举个例子:出现了用户有输入内容时展示对应文本框清空按钮的新需求。这时候我们只需要完成一个新的转换器:InputStateEmpty
时返回isHidden = YES
;其余情况下返回isHidden = NO
。然后把对应输出源通过转换器绑定到清空按钮的isHidden
属性上即可。另外上一节提到的另一种颜色转换器,也是一种多样性的体现。
- 可以进行二次组合,用以计算输出值
loginEnabled
。(见下一节)
ViewModel的完整实现
19-F
需要把输出源对应的属性偷偷改成readwrite的先,不然不可写的话绑定的时候会跪。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。