[翻譯]本文翻譯自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)系我狐榔。