【翻譯】MVVM介紹

鏈接:https://www.objc.io/issues/13-architecture/mvvm/

MVVM介紹

2011年,我在500px得到我的第一份工作。雖然在大學(xué)中我做了幾年的iOS外包工作,但是這是我第一次真正的iOS開發(fā)工作。我是唯一的iOS開發(fā)者去制作漂亮設(shè)計的iOS應(yīng)用。在短短的7周內(nèi),我們發(fā)布了1.0版本并持續(xù)迭代,添加了許多功能。從本質(zhì)上來說,代碼庫也變得更復(fù)雜了。

有時候就像我不知道我做了什么。我清楚我的設(shè)計模式,就像其他優(yōu)秀的開發(fā)一樣,但我離我的產(chǎn)品太近了,導(dǎo)致我無法客觀的衡量我的架構(gòu)決策的功效。當(dāng)團隊中又來了一個一位開發(fā)者時,我才意識到我們遇到問題了。

大家是否聽過MVC?有些人稱之為Massive View Controller(巨型視圖控制器)。這就是我當(dāng)時的感受。我不想提起太多尷尬的細節(jié),但這足以說明,如果不得不重新來一次,我會做出不同的決策。

我會修改其中一個關(guān)鍵的架構(gòu),并將其帶入我從那時起開發(fā)的應(yīng)用,也就是將Model-View-Controller改為Model-View-ViewModel。

所以到底什么是MVVM?先不管MVVM的來歷,我們先看一個典型的iOS應(yīng)用是什么樣,并從這里開始了解MVVM:

Typical Model-View-Controller setup

一個典型的MVC架構(gòu)是這樣的:Models呈現(xiàn)數(shù)據(jù),Views呈現(xiàn)用戶交互,View Controller則調(diào)節(jié)前兩者之間的交互。很棒的架構(gòu)!

再仔細想想,盡管views和view controllers從技術(shù)上來說是完全不同的組件,但他們幾乎總是成對出現(xiàn)。你什么時候能看到一個view可以與不同的view controller成對出現(xiàn)?或者反過來亦然?所以我們?yōu)槭裁床话阉麄冞B接在一起:

Intermediate

上圖跟準確的描述了我們實際寫出的MVC代碼。但這對于解決日益增長的massive view controllers沒有太大的幫助。在典型的的MVC應(yīng)用中,大量的邏輯放在了view controllers中。其中有部分確實屬于view controller,但是更多的所謂“表現(xiàn)邏輯”,在MVVM術(shù)語講,就是將模型中的一些值,轉(zhuǎn)換為可以在view中顯示的內(nèi)容,比如NSDate日期類型轉(zhuǎn)換成格式化后的NSString類型。

在上面的圖中缺失了一部分用來存放表現(xiàn)邏輯的代碼。我們將之稱為view model(視圖模型),它在view/controller和model之間:

Model-View-ViewModel

這樣看起來好多了。上圖準確描述了MVVM:一個升級版的MVC模式,其中將view和controller連接到一起,然后將表現(xiàn)邏輯從controller中移動到了一個新的對象中,也就是view model。MVVM模型聽起來很復(fù)雜,但本質(zhì)上就是一個對你所熟知的MVC架構(gòu)進行了一個包裝。

所以現(xiàn)在你知道了什么是MVVM,但是為什么我們要使用它呢?對于我來說,在iOS上使用MVVM的動機就是它可以減少視圖控制器的復(fù)雜度,同時提高了表現(xiàn)了邏輯的可測試性。接下來用一些例子來看下如何完成上述的目標(biāo)。

以下3點希望看完本文后可以學(xué)到的:

  • MVVM可以兼容現(xiàn)有的MVC架構(gòu)
  • MVVM讓應(yīng)用更容易測試
  • MVVM和綁定技術(shù)一起時最佳

正如前邊所說,MVVM基本山是一個MVC的改進版本,所以我們很容易看到如何將MVVM整合到現(xiàn)有的使用典型MVC架構(gòu)實現(xiàn)的應(yīng)用。我們先創(chuàng)建一個簡單的Person模型和關(guān)聯(lián)的視圖控制器。

@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

很好!現(xiàn)在我們有一個PersonViewController,在它的viewDidLoad中使用model屬性設(shè)置一些label。

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

這一切都非常簡單,標(biāo)準的MVC?,F(xiàn)在我們看看如何使用view model改進下:

@interface PersonViewModel : NSObject

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

@property (nonatomic, readonly) Person *person;

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

@end

view model中的實現(xiàn)如下:

@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移動到了view model中。此時viewDidLoad方法就變得非常輕量:

- (void)viewDidLoad {
    [super viewDidLoad];

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

可以看到,并沒有對MVC架構(gòu)做太多的改變。都是相同的代碼,只是放到了不同的地方。MVVM兼容MVC,可以簡化視圖控制器,同時提高可測試性。

可測試性,嗯?是怎樣的?眾所周知,視圖控制器是出了名的難以測試。在MVVM中,我們將盡可能多的代碼遷移到view models中。測試視圖控制器就變得容易多了,因為視圖控制器不需要再做那么多工作,而且view models是非常容易測試的。我們看一下:

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

如果我們沒有將表示邏輯移到view model中,就需要初始化整個視圖控制器和相關(guān)的視圖,并且比較view中l(wèi)abel的值。這不僅僅變成了一個不便的間接層,而且會成為一個嚴重脆弱的測試。而使用MVVM后,我們可以自由的根據(jù)意愿修改視圖的層級而不用擔(dān)心打破我們的單元測試。使用MVVM對于測試的優(yōu)點是顯而易見的,哪怕僅僅是一個簡單的例子,而且當(dāng)表示邏輯越來越復(fù)雜時會更明顯。

在上面的簡單例子中,模型是不可變的,所以我們可以在初始化階段將設(shè)置view model的屬性。對于可變的model來說,我們需要使用一種綁定機制讓view model在背后的model改變時,更新自己的屬性。此外,當(dāng)view model中的模型改變時,視圖中的屬性也需要更新。模型的更新需要級聯(lián)向下通過view model進入view。

在OS X系統(tǒng)中,人們可以使用Cocoa綁定,但是在iOS上沒有這么好的配置使用。此時就會想到鍵值綁定(KVO),KVO做了很多偉大的工作。然而,對于一個簡單的綁定都需要許多樣板,更不用說如果有許多屬性需要綁定的時候了。相比KVO,我更喜歡使用ReactiveCocoa,但MVVM沒有強制我們使用ReactiveCocoa。MVVM是一個偉大的典范,它本身完全獨立,只是在有一個良好的綁定框架時會做的更好。

本文我們覆蓋了不少內(nèi)容:MVVM起源于MVC,看他們是如何相兼容的范式,從可測試性的角度看MVVM,并且知道了MVVM在有綁定技術(shù)下工作的更好。如果你對MVVM學(xué)習(xí)有興趣,可以看下這篇博客,從更多的細節(jié)上解釋了MVVM的好處,或者這篇文章,關(guān)于我們在最近的項目中如何使用MVVM來獲取巨大的成功。我也有一個開源的基于MVVM的完整的可測試的應(yīng)用C-41。去看看吧,如果有問題可聯(lián)系我的twitter,讓我知道。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容