文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏
参与翻译: Enix (10), Stella (2)

Introduction to 介绍

我们展现了在构建Swift项目中所发现问题及其解决方法。收看改版后每周一集的《Swift Talk》,挤满了现场编码和对我们决策利弊的讨论。

2011年,我在500px得到了我的第一个iOS工作。我已经在大学做了几年的iOS承包,但这是我第一次,真正的iOS工作。我被聘为唯一的iOS开发者并将设计精美的iPad应用程序。在七个星期内,我们发布了系统1.0,并且继续更新,增加更多的功能,从本质上讲,是更复杂的代码库。

第 1 段(可获 2 积分)

有时感觉就像我不知道我在做什么。我知道我的设计模式,就像任何一个好的程序员一样,但我太了解我所做的产品,可以客观地衡量了我的建筑决策的有效性。所以当另一个开发人员在船上使我意识到我们的队伍即将产生矛盾。 

听说过MVC吗?有人称之为,混乱的视图控制器。当时觉得这个称谓是对的。我不会让自己处于尴尬的细节,但可以这么说,如果我不得不重新再经历一次,我会做出不同的决定。

第 2 段(可获 2 积分)

我将要做的其中一个关键的架构改变,就是使用一种叫做数据模型-视图-视图数据模型(Model-View-ViewModel)的模式替代数据模型-视图-控制器(Model-View-Controller)。

那么什么叫MVVM呢?我们不去考量MVVM产生的历史背景,而是去从基本的iOS app入手,并深入研究MVVM到底是什么:

下面我们看到一个标准的MVC设置。数据模型表示数据,视图表示用户接口,还有视图控制器协调它们直接的交互。很酷吧。

但我们来思量一下,虽然视图和视图控制器是两个不同的组件,但是它们往往一起使用,成对出现。什么时候视图会和不同的视图控制器配对?或者反过来?所以,为什么不把它们的关系形式化呢?

第 3 段(可获 2 积分)

以上更准确的描述了你写过的MVC代码。但是它并没有讨论到在iOS app中日益臃肿的视图控制器。在标准的MVC应用中,大量的业务逻辑都放在视图控制器中,某些代码确实是属于视图控制器的,但是大部分在MVVM术语中被称作“表达逻辑”,这些逻辑是用于从数据模型转换至视图层,就例如获取一个NSDate值,并把它格式化为NSString。

我们在图表中缺失了一些模块。那些模块可以包含所有的表示逻辑。我们把这些模块称之为“视图数据模型”(view model)。它将介于视图/控制器和数据模型之间。

第 4 段(可获 2 积分)

看起来好多了!这幅图准确的描述了什么是MVVM:我们得到了一个增强版的MVC模式,视图和控制器的连接正式建立,并把表达逻辑从控制器移动到了一个新的对象,也就是视图数据模型。MVVM听起来挺复杂,但是其实它是你熟悉的MVC架构的一种封装。

那么,我们已经知道什么是MVVM,那我们为什么需要使用它?动机其实就隐藏在iOS的MVVM背后,对于我而言,它可以帮助我减少视图控制器的复杂度,还有让表达逻辑更容易测试。我们来通过一些例子,看看如何通过它实现我们的目标。

第 5 段(可获 2 积分)

以下几点是我需要在本文强调的:

  • MVVM是兼容当前的MVC架构的
  • MVVM可以让你的应用更容易测试
  • MVVM通过绑定机制实现,会工作的更好

就如我们之前看到的,MVVM基本就是MVC模式的美化,所以我们容易发现,它是如何与现有的机遇经典的MVC架构的app融合在一起。让我们来看看一个简单的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
第 6 段(可获 2 积分)

很酷吧。假如我们现在有一个PersonViewController的类在viewDidLoad里面被定义, 我们只需基于数据模型的属性创建一些标签:

- (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];
}

 

第 7 段(可获 2 积分)

代码非常直观,这就是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

 

第 8 段(可获 2 积分)

很酷吧。 我们把viewDidLoad里面的表达逻辑移动到了视图数据模型( view model)里面。我们的新的viewDidLoad方法现在非常轻盈: 

- (void)viewDidLoad {
    [super viewDidLoad];

    self.nameLabel.text = self.viewModel.nameText;
    self.birthdateLabel.text = self.viewModel.birthdateText;
}

那么,你会发现,并没有涉及很多的对现有MVC架构的代码的修改。一样的代码,只是把代码移动一下。它完全兼容MVC,实现轻盈的视图控制器,并且更容易测试。

代码可以测试?如何实现呢?视图控制器本来就难以测试,因为它们做的东西太多了。但是在MVVM中,我们尝试移动视图控制器中的代码到视图数据模型中。测试视图控制器变得更容易一点,因为它做的更少,同时视图数据模型也更容易的测试。让我们一起来看看:

第 9 段(可获 2 积分)
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
第 10 段(可获 2 积分)

如果我们没有移动代码到视图数据模型,我们需要初始化一个完整的视图控制器,还有它对应的视图,并用数据模型中的属性和视图中标签值做对比。这样不仅对比的不直接,而且会出现很多碎片化的测试用例。现在我们得益于MVVM,可以随意修改视图,从这个简单的例子中可以看到。这个优点在更复杂的逻辑中,会变得更明显。

我们发现,在这个简单的例子中,数据模型是不可变的,所以我们在视图数据模型初始化的时候,就给它的属性赋值。对于可变的数据模型,我们需要一种绑定机制,好让视图数据模型可以随着数据模型的变化而变化。而且,如果视图数据模型背后的数据模型改变了,视图的属性也需要更新。数据模型的改变需要通过视图数据模型传播到视图。

第 11 段(可获 2 积分)

在OS X系统, 我们可以使用Cocoa绑定,但是在iOS上我们可无法尝到这甜头。键值观察模式(Key-value observation)立刻进入我们的脑海,并且它非常的棒,尤其当我们有好多属性需要绑定。我喜欢使用ReactiveCocoa,但是我们并强制一定要在MVVM中使用它。MVVM是一个非常伟大的设计模式,并且它只有在绑定模式中才会发挥的更好。

我们已经讨论了好多:从MVC中推导出MVVM,并探讨它们是如何兼容的,从测试的角度考察MVVM,并探讨在绑定模式中,它如何工作的更好。如果你对MVVM感兴趣,你可以看看这篇博文,它更详细的介绍MVVM的好处,或者阅读这篇文章,它是关于如何把MVVM应用在我的一个成功的项目当中。我还有一个测试完整的,基于MVVM的开源项目,名为 C-41。在阅读我的代码的过程中,如果有问题, 欢迎和我联系

第 12 段(可获 2 积分)

文章评论