内容字号:默认大号超大号

段落设置:段首缩进取消段首缩进

字体设置:切换到微软雅黑切换到宋体

MVVM架构简介

2018-05-15 16:44 出处:清屏网 人气: 评论(0

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 。如果您有任何问题,欢迎联系我。

分享给小伙伴们:
本文标签: MVVM架构

相关文章

发表评论愿您的每句评论,都能给大家的生活添色彩,带来共鸣,带来思索,带来快乐。

CopyRight © 2015-2016 QingPingShan.com , All Rights Reserved.

清屏网 版权所有 豫ICP备15026204号