MVVM架構(gòu)簡介

[翻譯]本文翻譯自objc.io官網(wǎng)iOS大神Ash Furrow的文章位岔, 原文可查看Introduction to MVVM

——————————————我是分割線——————————————
2011年我在500px公司獲得第一份iOS工作。雖然大學期間我一直有承包一些iOS小項目杀狡,但這份工作才堪稱我第一次真正的個人show憔足。作為唯一被聘請的iOS開發(fā)人員,我的工作是開發(fā)一款設(shè)計精美的iPad應用酪耕。在僅僅七周的時間里导梆,我們就發(fā)布了一個1.0版本。在持續(xù)迭代的過程中我們增加了更多功能迂烁,本質(zhì)上也增加了代碼的復雜性看尼。

有時我覺得不知道自己在做什么。像任何優(yōu)秀的技術(shù)人員一樣盟步,我知道自己的設(shè)計模式藏斩。但由于我實在過于貼近產(chǎn)品端,導致自己無法正確衡量技術(shù)架構(gòu)并作出客觀決策却盘。直到團隊中的另一位開發(fā)人員入伙狰域,我才意識到我們遇上麻煩了。

聽說過MVC架構(gòu)嗎黄橘?有人也稱它為巨型視圖控制器模式兆览。而這絕對就是我當年的真正感受。我不打算描述自己令人尷尬的技術(shù)細節(jié)旬陡,但假如我不得不再做一次技術(shù)決策拓颓,我一定不會選擇MVC架構(gòu)。

在這一工作經(jīng)驗后的再開發(fā)其他app時描孟, 我都會做出一個關(guān)鍵的架構(gòu)決策驶睦,即使用一種稱為Model-View-ViewModel(MVVM)的架構(gòu)來替代Model-View-Controller(MVC)砰左。

那么MVVM究竟是什么?我們不要關(guān)注MVVM的歷史背景场航,我們直接來看看一個典型的iOS應用是什么樣子缠导,它又是怎么派生出MVVM的:

這里我們看到一個典型的MVC架構(gòu)。 Model(模型)表示數(shù)據(jù)溉痢,View(視圖)表示用戶界面僻造,ViewController(控制器)負責協(xié)調(diào)這兩者的交互。

雖然視圖和視圖控制器在技術(shù)上是不同的組件孩饼,但它們幾乎總是配對著攜手并進髓削。嘗試回憶一下,你什么時候見過一個視圖可以與各種不同的視圖控制器配對镀娶? 或者反過來立膛? 那么我們?yōu)槭裁床恢苯哟_定他們兩者的聯(lián)系性?

上圖更準確地描述了你可能已經(jīng)寫過的MVC代碼梯码。 但是這對于解決iOS應用中的巨型視圖控制器問題并沒有太大的幫助宝泵。 在典型的MVC應用程序中,很多邏輯被放置在視圖控制器中轩娶。 當然儿奶,部分邏輯是屬于視圖控制器的,但其中很多都是被稱為“表示邏輯”的東西(在MVVM術(shù)語中)鳄抒,比如將模型中的值轉(zhuǎn)換為視圖上可以呈現(xiàn)的東西的邏輯闯捎,比如獲取一個NSDate并將其轉(zhuǎn)換為一個格式化的NSString。

在上圖中我們?nèi)笔Я艘粋€可以放置所有表示邏輯的東西许溅。 我們把它稱為"View Model"(視圖模型)隙券,它將位于視圖、控制器和模型之間:

該圖精確地描述了什么是MVVM:我們把視圖和控制器視為同一層闹司,并將表示邏輯從控制器中移到視圖模型這個新對象中。MVVM聽起來很復雜沐飘,但它本質(zhì)上就是你已熟悉的MVC架構(gòu)的一個進化版游桩。

知道MVVM是什么后,我們再來思考為什么要使用它耐朴?對我來說借卧,使用MVVM好處是它能降低視圖控制器的復雜性,并使得表示邏輯更易于測試筛峭。后面我們將會通過一些實例看看它是如何實現(xiàn)這些目標的铐刘。

我希望你從這篇文章中了解三個非常重要的觀點:

  • MVVM與你現(xiàn)有的MVC架構(gòu)是兼容的。
  • MVVM能使您的應用程序更具可測試性影晓。
  • MVVM配合綁定機制的使用能更好地發(fā)揮功效镰吵。

正如前文所述檩禾,MVVM本質(zhì)上只是MVC的一個升級版,所以它能很容易被整合到使用了MVC架構(gòu)的已有應用程序中疤祭。我們來看一個簡單的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

現(xiàn)在我們假設(shè)我們有一個PersonViewController盼产,它在viewDidLoad中根據(jù)模型屬性設(shè)置一些標簽:

- (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實現(xiàn)都相當簡單。 現(xiàn)在我們看看如何用視圖模型來改良它:

@interface PersonViewModel : NSObject

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

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

@end

視圖模型PersonViewModel內(nèi)部的實現(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

我們已經(jīng)將viewDidLoad中的表示邏輯轉(zhuǎn)移到視圖模型中勺馆,新的viewDidLoad方法現(xiàn)在非常輕巧:

- (void)viewDidLoad {
    [super viewDidLoad];

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

正如你所看到的戏售,原來的MVC架構(gòu)并沒有太多變化。 代碼是相同的草穆,它們只是被移動了灌灾。 它與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

如果我們沒有將這個邏輯轉(zhuǎn)移到視圖模型中盆佣,我們將不得不實例化一個完整的視圖控制器和相應的視圖,并一一比較視圖標簽中的值械荷。這不僅不方便測試共耍,同時這會是一個嚴重脆弱的測試。現(xiàn)在我們可以隨意修改我們的視圖層次結(jié)構(gòu)吨瞎,而不用擔心破壞我們的單元測試痹兜。即使是這個如此簡單的例子,使用MVVM的測試優(yōu)勢也很明顯颤诀,在更復雜的邏輯中它的可測試性優(yōu)勢會更加明顯字旭。

請注意,在這個簡單的示例中模型是不可變的崖叫,所以我們可以在初始化時指定視圖模型的屬性值遗淳。而對于可變模型我們需要使用某種綁定機制,以便模型改變時視圖模型可以及時更新其屬性心傀。此外屈暗,一旦視圖模型上的模型發(fā)生變化,視圖的屬性也需要更新。模型變化后新數(shù)據(jù)的流動應該從模型上流動到視圖模型中养叛,再流動到視圖中种呐。

在OS X上我們可以使用Cocoa實現(xiàn)綁定,但在iOS上我們沒有這種特權(quán)一铅。KVO是一種不錯的選擇陕贮。但是如果要綁定很多屬性的話,使用KVO會顯得很冗贅潘飘。我個人喜歡使用ReactiveCocoa肮之,但是我沒有意思要強迫大家都去使用ReactiveCocoa。 MVVM是一個很好的架構(gòu)卜录,雖然它可以獨立運行戈擒,但只有通過一個優(yōu)秀的綁定框架才能實現(xiàn)更好的效果。

我們已經(jīng)介紹了很多:MVC如何派生出MVVM并互相兼容艰毒;MVVM的可測試性筐高;MVVM在與綁定機制配合使用時效果最佳。如果您有興趣了解更多關(guān)于MVVM的知識丑瞧,可以查看這篇博客柑土,它更詳細地解釋MVVM的好處,或者關(guān)于我們?nèi)绾卧谧罱捻椖恐惺褂肕VVM并取得巨大成功的文章绊汹。我還有一個全面測試過的基于MVVM的開源應用稽屏,名為C-41。如果您有任何問題西乖,歡迎聯(lián)系我狐榔。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市获雕,隨后出現(xiàn)的幾起案子薄腻,更是在濱河造成了極大的恐慌,老刑警劉巖届案,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件庵楷,死亡現(xiàn)場離奇詭異,居然都是意外死亡楣颠,警方通過查閱死者的電腦和手機嫁乘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來球碉,“玉大人,你說我怎么就攤上這事仓蛆≌龆” “怎么了?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長豆拨。 經(jīng)常有香客問我直奋,道長,這世上最難降的妖魔是什么施禾? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任脚线,我火速辦了婚禮,結(jié)果婚禮上弥搞,老公的妹妹穿的比我還像新娘邮绿。我一直安慰自己,他們只是感情好攀例,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布船逮。 她就那樣靜靜地躺著,像睡著了一般粤铭。 火紅的嫁衣襯著肌膚如雪挖胃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天梆惯,我揣著相機與錄音酱鸭,去河邊找鬼。 笑死垛吗,一個胖子當著我的面吹牛凹髓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播职烧,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼扁誓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蚀之?” 一聲冷哼從身側(cè)響起蝗敢,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎足删,沒想到半個月后寿谴,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡失受,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年讶泰,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拂到。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡痪署,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出兄旬,到底是詐尸還是另有隱情狼犯,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站悯森,受9級特大地震影響宋舷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瓢姻,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一祝蝠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧幻碱,春花似錦绎狭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至摔桦,卻和暖如春社付,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背邻耕。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工鸥咖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人兄世。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓啼辣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親御滩。 傳聞我的和親對象是個殘疾皇子鸥拧,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容