objc:Issue13——Architecture【1】

編輯推薦

距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)暴:

uikonf meeting

頭腦風(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 StollJeff 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

image

從上圖我們看到了MVC的架構(gòu)圖。模型(Model)展示數(shù)據(jù)返吻,視圖展示用戶界面姑子,控制器協(xié)調(diào)兩者之間的交互。Cool测僵!

思考一下街佑,盡管視圖和控制器在技術(shù)上是不同的組件谢翎,但是他們總是成雙成對(duì),形影不離的沐旨。視圖最后一次匹配不同視圖控制器(View Controller)是什么時(shí)候森逮?反之亦然。因此為什么不形式化他們之間的連接磁携?

MVC

這可以更準(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/controllermodel之間:

image

看起來(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中使用ReactiveCocoaMVVM是一個(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)題可以告訴我晰赞。

未完待續(xù)...

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末稼病,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子掖鱼,更是在濱河造成了極大的恐慌然走,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件戏挡,死亡現(xiàn)場(chǎng)離奇詭異芍瑞,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)褐墅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門拆檬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)洪己,“玉大人,你說(shuō)我怎么就攤上這事竟贯〈鸩叮” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵屑那,是天一觀的道長(zhǎng)噪珊。 經(jīng)常有香客問(wèn)我,道長(zhǎng)齐莲,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任磷箕,我火速辦了婚禮选酗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘岳枷。我一直安慰自己芒填,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布空繁。 她就那樣靜靜地躺著殿衰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪盛泡。 梳的紋絲不亂的頭發(fā)上闷祥,一...
    開(kāi)封第一講書人閱讀 49,764評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音傲诵,去河邊找鬼凯砍。 笑死,一個(gè)胖子當(dāng)著我的面吹牛拴竹,可吹牛的內(nèi)容都是我干的悟衩。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼栓拜,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼座泳!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起幕与,我...
    開(kāi)封第一講書人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤挑势,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后啦鸣,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體薛耻,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年赏陵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了饼齿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饲漾。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖缕溉,靈堂內(nèi)的尸體忽然破棺而出考传,到底是詐尸還是另有隱情,我是刑警寧澤证鸥,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布僚楞,位于F島的核電站,受9級(jí)特大地震影響枉层,放射性物質(zhì)發(fā)生泄漏泉褐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一鸟蜡、第九天 我趴在偏房一處隱蔽的房頂上張望膜赃。 院中可真熱鬧,春花似錦揉忘、人聲如沸跳座。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)疲眷。三九已至,卻和暖如春您朽,著一層夾襖步出監(jiān)牢的瞬間狂丝,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工哗总, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留美侦,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓魂奥,卻偏偏與公主長(zhǎng)得像菠剩,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子耻煤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

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