我于 2011 年在 500px 找到自己的第一份 iOS 開(kāi)發(fā)工作。雖然我已經(jīng)在大學(xué)里做了好幾年 iOS 外包開(kāi)發(fā),但這才是我的一個(gè)真正的 iOS 開(kāi)發(fā)工作。我被作為唯一的 iOS 開(kāi)發(fā)者被招聘去實(shí)現(xiàn)擁有漂亮設(shè)計(jì)的 iPad 應(yīng)用。在短短七周里,我們就發(fā)布了 1.0 并持續(xù)迭代,添加了更多特性,但從本質(zhì)上,代碼庫(kù)也變得更加復(fù)雜了。
有時(shí)我感覺(jué)就像我不知道在做什么。雖然我知道自己的設(shè)計(jì)模式——就像任何好的編程人員那樣 —— 但我太接近我在做的產(chǎn)品以至于不能客觀地衡量我的架構(gòu)決策的有效性。當(dāng)隊(duì)伍中來(lái)了另外一位開(kāi)發(fā)者時(shí),我意識(shí)到我們陷入困境了。
從沒(méi)聽(tīng)過(guò) MVC ?有人稱之為 Massive View Controller(重量級(jí)視圖控制器),這就是我們那時(shí)候的感覺(jué)。我不打算介紹令人汗顏的細(xì)節(jié),但說(shuō)實(shí)在的,如果我不得不再次重來(lái)一次,我絕對(duì)會(huì)做出不同的決策。
我會(huì)修改一個(gè)關(guān)鍵架構(gòu),并將其帶入我從那時(shí)起就在開(kāi)發(fā)的各種應(yīng)用,即使用一種叫做 Model-View-ViewModel 的架構(gòu)替換 Model-View-Controller。
所以,MVVM 到底是什么?與其專注于說(shuō)明 MVVM 的來(lái)歷,不如讓我們看一個(gè)典型的 iOS 是如何構(gòu)建的,并從那里了解 MVVM:
http://wiki.jikexueyuan.com/project/objc/images/13-1.png" alt="" />
我們看到的是一個(gè)典型的 MVC 設(shè)置。Model 呈現(xiàn)數(shù)據(jù),View 呈現(xiàn)用戶界面,而 View Controller 調(diào)節(jié)它兩者之間的交互。Cool!
稍微考慮一下,雖然 View 和 View Controller 是技術(shù)上不同的組件,但它們幾乎總是手牽手在一起,成對(duì)的。你什么時(shí)候看到一個(gè) View 能夠與不同 View Controller 配對(duì)?或者反過(guò)來(lái)?所以,為什么不正規(guī)化它們的連接呢?
http://wiki.jikexueyuan.com/project/objc/images/13-2.png" alt="" />
這更準(zhǔn)確地描述了你可能已經(jīng)編寫(xiě)的 MVC 代碼。但它并沒(méi)有做太多事情來(lái)解決 iOS 應(yīng)用中日益增長(zhǎng)的重量級(jí)視圖控制器的問(wèn)題。在典型的 MVC 應(yīng)用里,許多邏輯被放在 View Controller 里。它們中的一些確實(shí)屬于 View Controller,但更多的是所謂的“表示邏輯(presentation logic)”,以 MVVM 屬術(shù)語(yǔ)來(lái)說(shuō),就是那些將 Model 數(shù)據(jù)轉(zhuǎn)換為 View 可以呈現(xiàn)的東西的事情,例如將一個(gè) NSDate
轉(zhuǎn)換為一個(gè)格式化過(guò)的 NSString
。
我們的圖解里缺少某些東西,那些使我們可以把所有表示邏輯放進(jìn)去的東西。我們打算將其稱為 “View Model” —— 它位于 View/Controller 與 Model 之間:
http://wiki.jikexueyuan.com/project/objc/images/13-3.png" alt="" />
看起好多了!這個(gè)圖解準(zhǔn)確地描述了什么是 MVVM:一個(gè) MVC 的增強(qiáng)版,我們正式連接了視圖和控制器,并將表示邏輯從 Controller 移出放到一個(gè)新的對(duì)象里,即 View Model。MVVM 聽(tīng)起來(lái)很復(fù)雜,但它本質(zhì)上就是一個(gè)精心優(yōu)化的 MVC 架構(gòu),而 MVC 你早已熟悉。
現(xiàn)在我們知道了什么是 MVVM,但為什么我們會(huì)想要去使用它呢?在 iOS 上使用 MVVM 的動(dòng)機(jī),對(duì)我來(lái)說(shuō),無(wú)論如何,就是它能減少 View Controller 的復(fù)雜性并使得表示邏輯更易于測(cè)試。通過(guò)一些例子,我們將看到它如何達(dá)到這些目標(biāo)。
此處有三個(gè)重點(diǎn)是我希望你看完本文能帶走的:
如我們之前所見(jiàn),MVVM 基本上就是 MVC 的改進(jìn)版,所以很容易就能看到它如何被整合到現(xiàn)有使用典型 MVC 架構(gòu)的應(yīng)用中。讓我們看一個(gè)簡(jiǎn)單的 Person
Model 以及相應(yīng)的 View Controller:
@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
Cool!現(xiàn)在我們假設(shè)我們有一個(gè) 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];
}
這全都直截了當(dāng),標(biāo)準(zhǔn)的 MVC。現(xiàn)在來(lái)看看我們?nèi)绾斡靡粋€(gè) View Model 來(lái)增強(qiáng)它。
@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 的實(shí)現(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
Cool!我們已經(jīng)將 viewDidLoad
中的表示邏輯放入我們的 View Model 里了。此時(shí),我們新的 viewDidLoad
就會(huì)非常輕量:
- (void)viewDidLoad {
[super viewDidLoad];
self.nameLabel.text = self.viewModel.nameText;
self.birthdateLabel.text = self.viewModel.birthdateText;
}
所以,如你所見(jiàn),并沒(méi)有對(duì)我們的 MVC 架構(gòu)做太多改變。還是同樣的代碼,只不過(guò)移動(dòng)了位置。它與 MVC 兼容,帶來(lái)更輕量的 View Controllers。
可測(cè)試,嗯?是怎樣?好吧,View Controller 是出了名的難以測(cè)試,因?yàn)樗鼈冏隽颂嗍虑?。?MVVM 里,我們?cè)囍M可能多的將代碼移入 View Model 里。測(cè)試 View Controller 就變得容易多了,因?yàn)樗鼈儾辉僮鲆淮蠖咽虑椋⑶?View Model 也非常易于測(cè)試。讓我們來(lái)看看:
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
如果我們沒(méi)有將這個(gè)邏輯移入 View Model,我們將不得不實(shí)例化一個(gè)完整的 View Controller 以及伴隨的 View,然后去比較我們 View 中 Lable 的值。這樣做不只是會(huì)變成一個(gè)麻煩的間接層,而且它只代表了一個(gè)十分脆弱的測(cè)試?,F(xiàn)在,我們可以按意愿自由地修改視圖層級(jí)而不必?fù)?dān)心破壞我們的單元測(cè)試。使用 MVVM 帶來(lái)的對(duì)于測(cè)試的好處非常清晰,甚至從這個(gè)簡(jiǎn)單的例子來(lái)看也可見(jiàn)一斑,而在有更復(fù)雜的表示邏輯的情況下,這個(gè)好處會(huì)更加明顯。
注意到在這個(gè)簡(jiǎn)單的例子中, Model 是不可變的,所以我們可以只在初始化的時(shí)候指定我們 View Model 的屬性。對(duì)于可變 Model,我們還需要使用一些綁定機(jī)制,這樣 View Model 就能在背后的 Model 改變時(shí)更新自身的屬性。此外,一旦 View Model 上的 Model 發(fā)生改變,那 View 的屬性也需要更新。Model 的改變應(yīng)該級(jí)聯(lián)向下通過(guò) View Model 進(jìn)入 View。
在 OS X 上,我們可以使用 Cocoa 綁定,但在 iOS 上我們并沒(méi)有這樣好的配置可用。我們想到了 KVO(Key-Value Observation),而且它確實(shí)做了很偉大的工作。然而,對(duì)于一個(gè)簡(jiǎn)單的綁定都需要很大的樣板代碼,更不用說(shuō)有許多屬性需要綁定了。作為替代,我個(gè)人喜歡使用 ReactiveCocoa,但 MVVM 并未強(qiáng)制我們使用 ReactiveCocoa。MVVM 是一個(gè)偉大的典范,它自身獨(dú)立,只是在有一個(gè)良好的綁定框架時(shí)做得更好。
我們覆蓋了不少內(nèi)容:從普通的 MVC 派生出 MVVM,看它們是如何相兼容的范式,從一個(gè)可測(cè)試的例子觀察 MVVM,并看到 MVVM 在有一個(gè)配對(duì)的綁定機(jī)制時(shí)工作得更好。如果你有興趣學(xué)習(xí)更多關(guān)于 MVVM 的知識(shí),你可以看看這篇博客,它用更多細(xì)節(jié)解釋了 MVVM 的好處,或者這一篇關(guān)于我們?nèi)绾卧谧罱捻?xiàng)目里使用 MVVM 獲得巨大的成功的文章。我同樣還有一個(gè)經(jīng)過(guò)完整測(cè)試,基于 MVVM 的應(yīng)用,叫做 C-41 ,它是開(kāi)源的。去看看吧,如果你有任何疑問(wèn),請(qǐng)告訴我。