梧桐

设计模式来啦,MVVM模式

September 09, 2014 | 0 Minute Read

接触MVVM有一段时间了,期间通过网上的例子,公司的开发平台对Model-View-ViewModel这种模式有一定的了解,遇到不少问题,也有一些自己的理解。

情景1:团队辛辛苦苦完成了一个项目,抱着激动的心情去给用户做demo,而用户给你的反馈是UI很不满意,要重新修改,否则拒绝验收。大规模修改UI,晴天霹雳!

情景2:产品在一家客户上线运行反应不错,公司准备扩大营销市场,寻求更多的客户,此时,不同客户对UI纷纷提出修改意见,众口难调,但是老总发话,客户是上帝!

那么,该是MVVM登场的时候了。

##1.MVVM模式是什么 MVVM设计模式可以说是微软为WPF量身定做的一款模式,通过Binding,借助INotifyPropertyChanged接口实现数据绑定,我们可以很容易的将UI和逻辑层分开,通过实现IComand接口来实现事件绑定。

  1. 视图(View)
  2. 视图模型(ViewModel)
  3. 模型(Model)

在MVVM模式中,View的作用仅仅是显示,它通过DataContext和ViewModel进行数据绑定,不直接与Model交互。在View的xaml后面的cs文件中,基本不需要添加额外的代码。

ViewModel是View和Model的桥梁,是Model的助手,帮助Model处理事件响应和提供给View数据显示。ViewModel应当充当后台模型的作用。

Model用于封装与应用程序业务逻辑相关的数据以及对数据的处理方法。它具有对数据直接访问的权利。Model不依赖于View和ViewModel,也就是说,Model不关心会被如何显示或是如何被操作,Model也不能包含任何用户使用的与界面相关的逻辑。

##2.MVVM模式具体实现 一个完整的C# MVVM工程包含了以下几个方面:

  1. EntityModel:使用ORM将数据库中的表、视图转化为类操作
  2. WCF:通过建立的数据模型生成WCF服务
  3. Client:引用WCF服务建立工厂,作为工程中的Model
  4. WPF:分为View,ViewModel.View专注UI,ViewModel与Client交互,获取并处理数据

在iOS项目中,我们需要引用第三方库ReactiveCocoa,ReactiveCocoa是Github开源的一款cocoa FRP 框架

Native app有很大一部分的时间是在等待事件发生,然后响应事件,比如等待网络请求完成,等待用户的操作,等待某些状态值的改变等等,等这些事件发生后,再做进一步处理。 但是这些等待和响应,并没有一个统一的处理方式。Delegate, Notification, Block, KVO, 常常会不知道该用哪个最合适。有时需要chain或者compose某几个事件,就需要多个状态变量,而状态变量一多,复杂度也就上来了。为了解决这些问题,Github的工程师们开发了ReactiveCocoa。

###几个常见的概念

在阅读ReactiveCocoa(以下简称RAC)的相关文章或代码时,经常会出现一些名词,理解它们对于理解RAC有很大的帮助,下面就简要来说说这些常见的概念。

Signal and Subscriber 这是RAC最核心的内容,这里我想用插头和插座来描述,插座是Signal,插头是Subscriber。想象某个遥远的星球,他们的电像某种物质一样被集中存储,且很珍贵。插座负责去获取电,插头负责使用电,而且一个插座可以插任意数量的插头。当一个插座(Signal)没有插头(Subscriber)时什么也不干,也就是处于冷(Cold)的状态,只有插了插头时才会去获取,这个时候就处于热(Hot)的状态。

Signal获取到数据后,会调用Subscriber的sendNext, sendComplete, sendError方法来传送数据给Subscriber,Subscriber自然也有方法来获取传过来的数据,如:[signal subscribeNext:error:completed]。这样只要没有sendComplete和sendError,新的值就会通过sendNext源源不断地传送过来,举个简单的例子:

[RACObserve(self, username) subscribeNext: ^(NSString *newName){ 
    NSLog(@"newName:%@", newName); 
}];

RACObserve使用了KVO来监听property的变化,只要username被自己或外部改变,block就会被执行。但不是所有的property都可以被RACObserve,该property必须支持KVO,比如NSURLCache的currentDiskUsage就不能被RACObserve。

Signal是很灵活的,它可以被修改(map),过滤(filter),叠加(combine),串联(chain),这有助于应对更加复杂的情况,比如:

RAC(self.logInButton, enabled) = [RACSignal 
    combineLatest:@[ 
        self.usernameTextField.rac_textSignal, 
        self.passwordTextField.rac_textSignal, 
        RACObserve(LoginManager.sharedManager, loggingIn), 
        RACObserve(self, loggedIn) 
    ] reduce:^(NSString *username, NSString *password, NSNumber *loggingIn, NSNumber *loggedIn) { 
        return @(username.length > 0 && password.length > 0 && !loggingIn.boolValue && !loggedIn.boolValue); 
}];

这段代码看起来有点复杂,来细细说一下,首先是左边的RAC(…),它的作用是将self.logInButton.enabled属性与右边的signal的sendNext值绑定。也就是如果右边的reduce的返回值为NO,那么enabled就为NO。右边的combineLatest是获取这4个signal的next值。其中可以看到self.usernameTextField.ractextSignal这么个东东,ractextSignal是RAC为UITextField添加的category,只要usernameTextField的值有变化,这个值就会被返回(sendNext)。combineLatest需要每个signal至少都有过一次sendNext。reduce的作用是根据接收到的值,再返回一个新的值,这里是@(YES)和@(NO),必须是object。

上面这段代码用到了Signal的组合,想象一下,如果是传统的方式,写起来还是挺复杂的,而且随着功能的增加,调整起来会更加麻烦。

相关文章参考ReactiveCocoa - iOS开发的新框架