本文译自 :Introduction to MVVM  by Ash Furrow

2011年, 我在500px得到了第一份iOS相关的工作。 之前几年我在大学的时候就做过iOS的项目,但是,这是我第一份正式的iOS工作。我以核心开发人员的身份被招来做一个设计优美的iPad app,仅过了几个周,我们就交付了1.0 并继续迭代,加入了更多的特性,同时,代码也越来越多。

有些时候,我也不知道我在做什么,像大多数优秀的开发人员一样,我了解我的设计。但是正是因为我太了解我的作品了,导致我无法客观的判断我的设计决策到底能带来多少积极的影响,直到团队又招了个人进来,我们意识到,坏了。

听说过MVC么?有人叫做 大量的-视图-控制器。 这就是当时我的感受。细节不多说,总而言之,如果重新来过,我会选择另一条路。

那么,到底什么是MVVM?先不追究MVVM怎么来的。我们先看看一个典型的MVVM设计的iOS app 是什么样的。

ios开发MVVM和RAC ios开发 mvvm_ios开发MVVM和RAC

我们看到一个典型的MVC设计,模型负责数据,视图展示界面,控制器协调调度前两者。

稍作思考,即使视图和控制器是不同的组件,但他们几乎总是成双入对,你中有我,我中有你。上次你见过一个视图被不同控制器共用是什么时候了?或者反过来?  所以,为什么不规范化他们的联系呢?

  

ios开发MVVM和RAC ios开发 mvvm_测试_02

这个应该能更精确的描述你正在写的MVC代码。 它没办法控制越来越重的控制器。典型的MVC设计的程序里,大量的逻辑都放在控制器里,有些逻辑的确属于控制器,但还有很多并不,比如在MVVM里一种叫做展示逻辑的组件,它可以把模型映射成可以让视图直接展示的对象,也可以把NSdate 转成NSString。我们的图里漏掉了些东西,我们可以把所有的展示逻辑放在这里。我们把它叫做 ’’视图模型’’。 它位于视图和控制器之间:

ios开发MVVM和RAC ios开发 mvvm_移动开发_03

  看起来好点了!这个图精确的描述了什么是MVVM:一个增强的MVC,我们创建一个新的对象衔接视图和控制器,把展示逻辑从控制器抽出来放入其中,这个对象就是视图模型。MVVM听起来很高大上,但根本上,它还是我们熟悉的MVC的一个封装。

现在我们知道什么是MVVM了,为什么有人想用它呢?对于我来说,动机是它能降低控制器的复杂度,然后就是展示逻辑更容易测试。我们马上会用几个例子展示一下到底是怎么做到的。

有三个要点我希望大家能够知道:

。 MVVM能跟现有的MVC很好的兼容。

。 MVVM能让你的程序更容易测试。

。 MVVM在绑定机制下优势最大。

我们看到了,MVVM只是一个MVC的变种,所以很容易集成在已有的MVC设计中,先来看个简单的 Person 模型的对应的 控制器:



@interface Person : NSObject

- (instancetype)initwithSalutation:(NSString *)salutation firstName:(NSString *)firstName lastName:(NSString *)lastName birthdate:(NSDate *)birthdate;

@property (nonatomic, readonly) NSString *salutation;
@property (nonatomic, readonly) NSString *firstName;
@property (nonatomic, readonly) NSString *lastName;
@property (nonatomic, readonly) NSDate *birthdate;

@end



 

不错,现在假如我们有一个PresonViewCOntroller, 在ViewDidLoad,根据模型的属性,设置几个labels



- (void)viewDidLoad {
    [super viewDidLoad];
    
    if (self.model.salutation.length > 0) {
        self.nameLabel.text = [NSString stringWithFormat:@"%@ %@ %@", self.model.salutation, self.model.firstName, self.model.lastName];
    } else {
        self.nameLabel.text = [NSString stringWithFormat:@"%@ %@", self.model.firstName, self.model.lastName];
    }
    
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
    self.birthdateLabel.text = [dateFormatter stringFromDate:model.birthdate];
}



 

这个很直观,典型的MVC,现在看看我们怎样用加入一个视图模型:



@interface PersonViewModel : NSObject

- (instancetype)initWithPerson:(Person *)person;

@property (nonatomic, readonly) Person *person;

@property (nonatomic, readonly) NSString *nameText;
@property (nonatomic, readonly) NSString *birthdateText;

@end



 

我们的视图模型实现如下:



@implementation PersonViewModel

- (instancetype)initWithPerson:(Person *)person {
    self = [super init];
    if (!self) return nil;
    
    _person = person;
    if (person.salutation.length > 0) {
        _nameText = [NSString stringWithFormat:@"%@ %@ %@", self.person.salutation, self.person.firstName, self.person.lastName];
    } else {
        _nameText = [NSString stringWithFormat:@"%@ %@", self.person.firstName, self.person.lastName];
    }
    
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
    _birthdateText = [dateFormatter stringFromDate:person.birthdate];
    
    return self;
}

@end



 

不错,我们把展示逻辑从viewDidLoad挪到我们的视图模型里,新的viewDidLoad方法现在轻多了:



- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.nameLabel.text = self.viewModel.nameText;
    self.birthdateLabel.text = self.viewModel.birthdateText;
}



 

所以,你也看到了,与MVC比,变化不大,代码相同,只是挪了挪位置,与MVC兼容,但控制器确更轻了,也更容易测试了。

容易测试?怎么做到的?好吧,过重的控制器难于测试早就臭名昭著了,在MVVM里,我们试图把尽可能多的代码挪到视图模型里,测试控制器就更容易了,因为它没那么多逻辑了,并且视图模型也很容易测试,看下面:



SpecBegin(Person)
    NSString *salutation = @"Dr.";
    NSString *firstName = @"first";
    NSString *lastName = @"last";
    NSDate *birthdate = [NSDate dateWithTimeIntervalSince1970:0];

    it (@"should use the salutation available. ", ^{
        Person *person = [[Person alloc] initWithSalutation:salutation firstName:firstName lastName:lastName birthdate:birthdate];
        PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
        expect(viewModel.nameText).to.equal(@"Dr. first last");
    });

    it (@"should not use an unavailable salutation. ", ^{
        Person *person = [[Person alloc] initWithSalutation:nil firstName:firstName lastName:lastName birthdate:birthdate];
        PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
        expect(viewModel.nameText).to.equal(@"first last");
    });

    it (@"should use the correct date format. ", ^{
        Person *person = [[Person alloc] initWithSalutation:nil firstName:firstName lastName:lastName birthdate:birthdate];
        PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
        expect(viewModel.birthdateText).to.equal(@"Thursday January 1, 1970");
    });
SpecEnd



如果我们不把这些逻辑挪到视图模型里,那我们得实例化一个控制器和对应的视图,在视图里比较label的值,这样不仅不直接,而且会导致测试代码更脆弱。现在我们可以随便修改视图,不用担心破坏单元测试,即使上面这个简单的栗子,MVVM的优势也是不言而喻的。而且随着逻辑越来越复杂,优势更明显。

注意,上面这个简单的栗子,模型是不变的,所以我们可以在初始化的时候用视图模型赋值,对于可变的模型,我们得是用绑定机制,这样在模型更改值的时候视图模型能同时修改。进一步,一旦视图的模型变化,视图的属性也能跟着变。模型的改变能够直接传达到视图上。

在OS X上,可以用Cocoa bindings,但是iOS还没有这么吊的机制,这时候就想到了KVO,它也很棒。然而,即使对于一个简单属性的绑定,KVO也过于啰嗦了,更别说更多属性的情况了。实际上,我喜欢用ReactiveCocoa。没有限制说用MVVM必须用ReactiveCocoa。MVVM本身就是个很吊的设计模式,只不过跟很吊的绑定框架一块只会更好而已。