編輯推薦
距objc.io第一期的出現(xiàn)已經(jīng)有一年了鲫构,我們正在慶祝我們的一周年!感謝在此期間所有支持我們的朋友玫坛,特別是那些讓我們從社區(qū)獲得的卓越貢獻(xiàn)的人结笨。
你肯能和我們一樣正為蘋果上周在WWDC發(fā)布的一系列以開(kāi)發(fā)者為中心的聲明感到不知所措。讓我們開(kāi)心的是今年蘋果的保密協(xié)議也有所松動(dòng)湿镀,這意味著我們不必等到秋天再寫這些炕吸。
在我們深入討論新東西之前,這個(gè)月我們?yōu)槟銣?zhǔn)備了一個(gè)更永恒的話題肠骆。我們想會(huì)過(guò)來(lái)整理一下我們寫過(guò)的第一期文章:更輕的視圖控制器(lighter view controllers)。但是這次我們選擇一個(gè)范圍更廣的話題塞耕,這期的文章會(huì)涉及各種不同的問(wèn)題蚀腿,而這些問(wèn)題可能會(huì)是你在思考應(yīng)用架構(gòu)的時(shí)候遇到的。
上個(gè)月,我們有機(jī)會(huì)和一個(gè)在柏林UIKonf的有趣的開(kāi)發(fā)團(tuán)隊(duì)坐在一起對(duì)這個(gè)話題進(jìn)行頭腦風(fēng)暴:
頭腦風(fēng)暴的結(jié)果是五篇分別對(duì)應(yīng)不同架構(gòu)問(wèn)題的文章:由Ash Furrow 編寫的《
MVVM
概念》莉钙,由Stephen Poletto編寫的《避免單例濫用》廓脆,由Krzystof Zabl?ocki編寫的《用IB模塊化行為(modular behaviors with Interface Builder)》,最后一個(gè)是磁玉,由Conrad Stoll和Jeff Gilbert編寫的有別于傳統(tǒng)MVC
的架構(gòu)——VIPER
停忿。
All the best from a very summery Berlin,
Chris, Daniel, and Florian.
MVVM
簡(jiǎn)介——by Ash Furrow
2011年我從500px得到了我的第一份iOS開(kāi)發(fā)工作。在大學(xué)我已經(jīng)做了幾年iOS外包開(kāi)發(fā)了蚊伞,但是這是我第一份真正的iOS開(kāi)發(fā)工作席赂。我作為唯一的iOS開(kāi)發(fā)者被雇傭去開(kāi)發(fā)設(shè)計(jì)精美的iPad應(yīng)用。僅僅7周我們就發(fā)布了1.0
版本然后繼續(xù)迭代时迫,添加更多的功能颅停,本質(zhì)上講,讓代碼庫(kù)更加復(fù)雜掠拳。
有事癞揉,我都不知道我在做什么東西。像其他好的程序員一樣溺欧,我知道自己的設(shè)計(jì)模式喊熟,但是我對(duì)產(chǎn)品架構(gòu)決策的效率評(píng)估太接近客觀了(but I was way too close to the product I was making to objectively measure the efficacy of my architectural decisions.)。隨著另一個(gè)人加入到團(tuán)隊(duì)姐刁,然我意識(shí)到我們陷入到麻煩中了芥牌。
聽(tīng)說(shuō)過(guò)MVC
?也有人稱之為Massive View Controller龙填。那是當(dāng)時(shí)的感覺(jué)胳泉。令人尷尬的細(xì)節(jié)就不再說(shuō)了,但是如果說(shuō)能再重來(lái)一次的話岩遗,我會(huì)做出不同的決定扇商。
自此,我做的一個(gè)關(guān)鍵架構(gòu)的改變而且在應(yīng)用開(kāi)發(fā)中用就到了它宿礁,那就是使用一個(gè)稱之為Model-View—ViewModel(MVVM)
的MVC
案铺。
MVVM
究竟為何物呢?而非關(guān)注MVVM
出現(xiàn)的歷史背景梆靖,讓我們典型的iOS
應(yīng)用是什么樣的控汉,并從中推出MVVM
:
從上圖我們看到了
MVC
的架構(gòu)圖。模型(Model
)展示數(shù)據(jù)返吻,視圖展示用戶界面姑子,控制器協(xié)調(diào)兩者之間的交互。Cool测僵!
思考一下街佑,盡管視圖和控制器在技術(shù)上是不同的組件谢翎,但是他們總是成雙成對(duì),形影不離的沐旨。視圖最后一次匹配不同視圖控制器(View Controller
)是什么時(shí)候森逮?反之亦然。因此為什么不形式化他們之間的連接磁携?
這可以更準(zhǔn)確地描述你已經(jīng)編寫的
MVC
代碼褒侧。但是它并不能解決應(yīng)用程序中笨重的試圖控制器(Massive View Controller)繼續(xù)笨重下去的趨勢(shì)。在標(biāo)準(zhǔn)的MVC
應(yīng)用程序中谊迄,很多邏輯被放在了視圖控制器(View Controller
)中處理闷供。當(dāng)然有些是屬于視圖控制器(View Controller
)的,但是很多并不屬于鳞上,這些在MVVM
術(shù)語(yǔ)中被稱為展示邏輯这吻,如把一些值轉(zhuǎn)換成可以在視圖中戰(zhàn)士的對(duì)象,如把一個(gè)NSDate對(duì)象轉(zhuǎn)換成格式化的NSString對(duì)象篙议。
從上圖可以看到我們漏掉了一些東西唾糯。在這里我們可以里面放置展示邏輯。我打算把它叫做視圖模型(View Model
)鬼贱,它位于view/controller
和model
之間:
看起來(lái)好了很多移怯!這幅圖準(zhǔn)確地描述了什么是
MVVM
:增強(qiáng)版的MVC
,通過(guò)MVVM
我們正式的連接了視圖(view
)和控制器(controller
)这难,把展示邏輯從從控制器移出到了視圖模型(view model
)中舟误。MVVM
聽(tīng)起來(lái)很復(fù)雜,但本質(zhì)上講姻乓,它是你已經(jīng)熟悉的MVC
架構(gòu)的精心改良版嵌溢。
現(xiàn)在我們已經(jīng)知道MVVM
是什么了,但是為什么有人會(huì)用它呢蹋岩?在iOS中赖草,對(duì)于我來(lái)說(shuō),MVVM
的驅(qū)動(dòng)力是他可以減少視圖控制器(view controller
)的復(fù)雜度剪个,并且使得展示邏輯更容易測(cè)試秧骑。我們通過(guò)例子來(lái)看一下它是如何達(dá)成目標(biāo)的。
我希望你能從這篇文章學(xué)到的有三個(gè)重要的方面:
-
MVVM
兼容你已存在的MVC
架構(gòu)扣囊。 -
MVVM
讓你的應(yīng)用更容易測(cè)試乎折。 -
MVVM
配合綁定機(jī)制使用最佳。
正如我們之前看到的侵歇,本質(zhì)上講MVVM僅僅是MVC的精心改良版骂澄,因此,很容易看到它是如何被整合到一個(gè)具有標(biāo)準(zhǔn)MVC
架構(gòu)的現(xiàn)有應(yīng)用程序中去惕虑。創(chuàng)建一個(gè)簡(jiǎn)單地Person
模型(Model
)和對(duì)應(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.假設(shè)我們有一個(gè)PersonViewController坟冲,在viewDidLoad方法中基于model的屬性僅僅設(shè)置一些labels:
- (void)viewDidLoad {
[super viewDidLoad];
if (self.model.salutation.length > 0) {
self.nameLabel.text = [NSString stirngWithFormat:@"%@ %@ %@", self.model.salutation, self.model.firstName, self.model.lastName];
} else {
self.nameLabel.text = [NSString stringWithFormat:@"%@ %@", self.model.firstName, self.model.lastName];
}
NSDateFormat *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
self.birthdateLabel.text = [dateFormatter stringFromDate:model.birthdate];
}
這是很簡(jiǎn)單MVC
架構(gòu)∈啃蓿現(xiàn)在讓我們看一下如何用一個(gè)視圖模型(View Model
)擴(kuò)展它:
@interface PersonViewModel: NSObject
- (instancetype)initWithPerson:(Person *)person;
@property (nonatomic, readonly) Person *person;
@property (nonatomic, readonly) NSString *nameText;
@property (nonatomic, readonly) NSString *birthdateText;
@end
下面就是這個(gè)模型(Model
)的實(shí)現(xiàn)方式:
- (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.我們把viewDidLoad中的展示邏輯移到了視圖模型(View Model
)中。現(xiàn)在viewDidLoad方法就顯得非常輕量級(jí)樱衷。
- (void)viewDidLoad {
[super viewDidLoad];
self.nameLabel.text = self.viewModel.nameText;
self.birthdateLabel.text = self.viewModel.birthdateText;
}
正如你看到的,與MVC
架構(gòu)相比改變不大酒唉。同樣的代碼矩桂,只是把它移來(lái)移去而已。MVVM
兼容MVC
痪伦,形成了lighter view controllers
侄榴,并且更容易測(cè)試。
可測(cè)試性网沾?這是什么癞蚕?眾所周知,由于視圖控制器(View Controller
)中處理的東西太多導(dǎo)致很難對(duì)它進(jìn)行測(cè)試辉哥。在MVVM
架構(gòu)中桦山,我們?cè)噲D把盡可能多的代碼移到了視圖模型(View Model
)中。由于視圖控制器(View Controller
)處理的東西減少醋旦,從而使得它的測(cè)試更容易恒水,同時(shí)視圖模型(View Model
)也變得極易測(cè)試。讓我們看一下:
SpecBegin(Person)
NSString *salutation = @"Dr.";
NSString *firstName = @"first";
NSString *lastName = @"last";
NSDate *birthdate = [NSDate dateWithTimeIntervalSince1970:0];
it(@"should user 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 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)有把這部分邏輯移到視圖模型(View Model
)中饲齐,如果要對(duì)其進(jìn)行測(cè)試钉凌,就不得不實(shí)例化完整的視圖控制器(View Controller
)及視圖(View
),同時(shí)比較視圖(View
)上標(biāo)簽中的值捂人。這樣不僅測(cè)試起來(lái)不方便御雕,而且測(cè)試結(jié)果也沒(méi)有說(shuō)服力。現(xiàn)在我們可以隨意的改變視圖層級(jí)而不必?fù)?dān)心破壞單元測(cè)試滥搭。使用MVVM
所帶來(lái)的測(cè)試上的好處是顯而易見(jiàn)的酸纲,盡管是這樣一個(gè)簡(jiǎn)單地例子,并且這種效果會(huì)隨著展示邏輯的復(fù)雜變的越來(lái)越明顯论熙。
注意在上述的簡(jiǎn)例中福青,模型(Model
)是不可變的,所以我們可以在初始化的時(shí)候設(shè)置模型(Model
)的屬性值脓诡。對(duì)于可變的model无午,我們需要使用一種綁定機(jī)制,以保證當(dāng)支持這些屬性的模型(Model
)改變時(shí)祝谚,視圖模型(View Model
)也會(huì)跟著更新宪迟。此外,一旦視圖模型(View Model
)中的模型(Model
)發(fā)生改變交惯,視圖(View
)中的屬性也需要更新次泽。模型(Model
)改變需要通過(guò)視圖模型(View Model
)向下傳遞至視圖(View
)穿仪。
在OSX系統(tǒng)中,可以使用Cocoa
綁定意荤,但是在iOS系統(tǒng)中沒(méi)有這種奢侈品啊片。因此,鍵值監(jiān)聽(tīng)(KVO玖像,Key-value observation
)就進(jìn)入我們的視線紫谷,而且效果很棒。然而捐寥,即使是一個(gè)簡(jiǎn)單KVO
綁定也需要很多樣板代碼笤昨,更別說(shuō)當(dāng)有很多屬性需要綁定的時(shí)候了。所以握恳,我喜歡使用ReactiveCocoa
瞒窒,但是并沒(méi)有強(qiáng)制要求在MVVM中使用ReactiveCocoa
。MVVM
是一個(gè)很好的范例乡洼,它可以獨(dú)立運(yùn)行崇裁,并且只有好的綁定框架與其配合才能表現(xiàn)的更加完美。
我們已經(jīng)說(shuō)了很多:從簡(jiǎn)單地MVC
得到MVVM
束昵,知道它們?nèi)绾渭嫒莘独芸牵瑥目蓽y(cè)試性看MVVM
,了解到當(dāng)MVVM
和綁定機(jī)制配合時(shí)效果最好妻怎。如果你想知道MVVM
的更多信息壳炎,可以查看這個(gè)博客,它更詳細(xì)的闡述了MVVM的好處逼侦,或者這篇文章匿辩,它是關(guān)于我們?nèi)绾伟?code>MVVM應(yīng)用在最近的工程中并取得巨大成功的。我也有一個(gè)基于MVVM
的開(kāi)源應(yīng)用——C-41榛丢,我對(duì)它進(jìn)行了完全測(cè)試铲球。你可以從git上把它pull下來(lái),如果有什么問(wèn)題可以告訴我晰赞。