MVVM架构简介

[翻译]本文翻译自objc.io官网iOS大神Ash Furrow的文章, 原文可查看Introduction to MVVM

——————————————我是分割线——————————————
2011年我在500px公司获得第一份iOS工作。虽然大学期间我一直有承包一些iOS小项目,但这份工作才堪称我第一次真正的个人show。作为唯一被聘请的iOS开发人员,我的工作是开发一款设计精美的iPad应用。在仅仅七周的时间里,我们就发布了一个1.0版本。在持续迭代的过程中我们增加了更多功能,本质上也增加了代码的复杂性。

有时我觉得不知道自己在做什么。像任何优秀的技术人员一样,我知道自己的设计模式。但由于我实在过于贴近产品端,导致自己无法正确衡量技术架构并作出客观决策。直到团队中的另一位开发人员入伙,我才意识到我们遇上麻烦了。

听说过MVC架构吗?有人也称它为巨型视图控制器模式。而这绝对就是我当年的真正感受。我不打算描述自己令人尴尬的技术细节,但假如我不得不再做一次技术决策,我一定不会选择MVC架构。

在这一工作经验后的再开发其他app时, 我都会做出一个关键的架构决策,即使用一种称为Model-View-ViewModel(MVVM)的架构来替代Model-View-Controller(MVC)。

那么MVVM究竟是什么?我们不要关注MVVM的历史背景,我们直接来看看一个典型的iOS应用是什么样子,它又是怎么派生出MVVM的:

这里我们看到一个典型的MVC架构。 Model(模型)表示数据,View(视图)表示用户界面,ViewController(控制器)负责协调这两者的交互。

虽然视图和视图控制器在技术上是不同的组件,但它们几乎总是配对着携手并进。尝试回忆一下,你什么时候见过一个视图可以与各种不同的视图控制器配对? 或者反过来? 那么我们为什么不直接确定他们两者的联系性?

上图更准确地描述了你可能已经写过的MVC代码。 但是这对于解决iOS应用中的巨型视图控制器问题并没有太大的帮助。 在典型的MVC应用程序中,很多逻辑被放置在视图控制器中。 当然,部分逻辑是属于视图控制器的,但其中很多都是被称为“表示逻辑”的东西(在MVVM术语中),比如将模型中的值转换为视图上可以呈现的东西的逻辑,比如获取一个NSDate并将其转换为一个格式化的NSString。

在上图中我们缺失了一个可以放置所有表示逻辑的东西。 我们把它称为"View Model"(视图模型),它将位于视图、控制器和模型之间:

该图精确地描述了什么是MVVM:我们把视图和控制器视为同一层,并将表示逻辑从控制器中移到视图模型这个新对象中。MVVM听起来很复杂,但它本质上就是你已熟悉的MVC架构的一个进化版。

知道MVVM是什么后,我们再来思考为什么要使用它?对我来说,使用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

现在我们假设我们有一个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];
}

这一切通过MVC实现都相当简单。 现在我们看看如何用视图模型来改良它:

@interface PersonViewModel : NSObject

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

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

@end

视图模型PersonViewModel内部的实现如下所示:

@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

如果我们没有将这个逻辑转移到视图模型中,我们将不得不实例化一个完整的视图控制器和相应的视图,并一一比较视图标签中的值。这不仅不方便测试,同时这会是一个严重脆弱的测试。现在我们可以随意修改我们的视图层次结构,而不用担心破坏我们的单元测试。即使是这个如此简单的例子,使用MVVM的测试优势也很明显,在更复杂的逻辑中它的可测试性优势会更加明显。

请注意,在这个简单的示例中模型是不可变的,所以我们可以在初始化时指定视图模型的属性值。而对于可变模型我们需要使用某种绑定机制,以便模型改变时视图模型可以及时更新其属性。此外,一旦视图模型上的模型发生变化,视图的属性也需要更新。模型变化后新数据的流动应该从模型上流动到视图模型中,再流动到视图中。

在OS X上我们可以使用Cocoa实现绑定,但在iOS上我们没有这种特权。KVO是一种不错的选择。但是如果要绑定很多属性的话,使用KVO会显得很冗赘。我个人喜欢使用ReactiveCocoa,但是我没有意思要强迫大家都去使用ReactiveCocoa。 MVVM是一个很好的架构,虽然它可以独立运行,但只有通过一个优秀的绑定框架才能实现更好的效果。

我们已经介绍了很多:MVC如何派生出MVVM并互相兼容;MVVM的可测试性;MVVM在与绑定机制配合使用时效果最佳。如果您有兴趣了解更多关于MVVM的知识,可以查看这篇博客,它更详细地解释MVVM的好处,或者关于我们如何在最近的项目中使用MVVM并取得巨大成功的文章。我还有一个全面测试过的基于MVVM的开源应用,名为C-41。如果您有任何问题,欢迎联系我

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

推荐阅读更多精彩内容