iOS應(yīng)用架構(gòu)談 view層的組織和調(diào)用方案
iOS應(yīng)用架構(gòu)談 view層的組織和調(diào)用方案
iOS應(yīng)用架構(gòu)談 網(wǎng)絡(luò)層設(shè)計(jì)方案
iOS應(yīng)用架構(gòu)談 本地持久化方案及動(dòng)態(tài)部署
前言
《iOS應(yīng)用架構(gòu)談 開(kāi)篇》出來(lái)之后,很多人來(lái)催我趕緊出第二篇。這一篇文章出得相當(dāng)艱難先紫,因?yàn)楣纠锏钠剖聝禾貏e多,我自己又有點(diǎn)私事兒,以至于能用來(lái)寫(xiě)博客的時(shí)間不夠充分论笔。
現(xiàn)在好啦采郎,第二篇出來(lái)了。
當(dāng)我們開(kāi)始設(shè)計(jì)View層的架構(gòu)時(shí)狂魔,往往是這個(gè)App還沒(méi)有開(kāi)始開(kāi)發(fā)蒜埋,或者這個(gè)App已經(jīng)發(fā)過(guò)幾個(gè)版本了,然后此時(shí)需要做非常徹底的重構(gòu)最楷。
一般也就是這兩種時(shí)機(jī)會(huì)去做View層架構(gòu)整份,基于這個(gè)時(shí)機(jī)的特殊性,我們?cè)谶@時(shí)候必須清楚認(rèn)識(shí)到:View層的架構(gòu)一旦實(shí)現(xiàn)或定型籽孙,在App發(fā)版后可修改的余地就已經(jīng)非常之小了烈评。因?yàn)樗鷺I(yè)務(wù)關(guān)聯(lián)最為緊密,所以哪怕稍微動(dòng)一點(diǎn)點(diǎn)犯建,它所引發(fā)的蝴蝶效應(yīng)都不見(jiàn)得是業(yè)務(wù)方能夠hold住的讲冠。這樣的情況,就要求我們?cè)趯?shí)現(xiàn)這個(gè)架構(gòu)時(shí)适瓦,代碼必須得改得勤快竿开,不能偷懶。也必須抱著充分的自我懷疑態(tài)度玻熙,做決策時(shí)要拿捏好尺度否彩。
View層的架構(gòu)非常之重要,在我看來(lái)嗦随,這部分架構(gòu)是這系列文章涉及4個(gè)方面最重要的一部分列荔,沒(méi)有之一。為什么這么說(shuō)枚尼?
View層架構(gòu)是影響業(yè)務(wù)方迭代周期的因素之一
產(chǎn)品經(jīng)理產(chǎn)生需求的速度會(huì)非程悖快,尤其是公司此時(shí)仍處于創(chuàng)業(yè)初期署恍,在規(guī)模稍大的公司里面崎溃,產(chǎn)品經(jīng)理也喜歡挖大坑來(lái)在leader面前刷存在感,比如阿里锭汛。這就導(dǎo)致業(yè)務(wù)工程師任務(wù)非常繁重。正常情況下讓產(chǎn)品經(jīng)理砍需求是不太可能的袭蝗,因此作為架構(gòu)師唤殴,在架構(gòu)里有一些可做可不做的事情,最好還是能做就做掉到腥,不要偷懶朵逝。這可以幫業(yè)務(wù)方減負(fù),編寫(xiě)代碼的時(shí)候也能更加關(guān)注業(yè)務(wù)乡范。
我跟一些朋友交流的時(shí)候配名,他們都會(huì)或多或少地抱怨自己的團(tuán)隊(duì)迭代速度不夠快啤咽,或者說(shuō),迭代速度不合理地慢渠脉。我認(rèn)為迭代速度不是想提就能提的宇整,迭代速度的影響因素有很多,一期PRD里的任務(wù)量和任務(wù)復(fù)雜度都會(huì)影響迭代周期能達(dá)到什么樣的程度芋膘。拋開(kāi)這些外在的不談鳞青,從內(nèi)在可能導(dǎo)致迭代周期達(dá)不到合理的速度的原因來(lái)看,其中有一個(gè)原因很有可能就是View層架構(gòu)沒(méi)有做好为朋,讓業(yè)務(wù)工程師完成一個(gè)不算復(fù)雜的需求時(shí)臂拓,需要處理太多額外的事情。當(dāng)然习寸,開(kāi)會(huì)多胶惰,工程師水平爛也屬于迭代速度提不上去的內(nèi)部原因,但這個(gè)不屬于本文討論范圍霞溪。還有孵滞,加班不是優(yōu)化迭代周期的正確方式,嗯威鹿。
一般來(lái)說(shuō)剃斧,一個(gè)不夠好的View層架構(gòu),主要原因有以下五種:
代碼混亂不規(guī)范
過(guò)多繼承導(dǎo)致的復(fù)雜依賴關(guān)系
模塊化程度不夠高忽你,組件粒度不夠細(xì)
橫向依賴
架構(gòu)設(shè)計(jì)失去傳承
這五個(gè)地方會(huì)影響業(yè)務(wù)工程師實(shí)現(xiàn)需求的效率幼东,進(jìn)而拖慢迭代周期。View架構(gòu)的其他缺陷也會(huì)或多或少地產(chǎn)生影響科雳,但在我看來(lái)這里五個(gè)是比較重要的影響因素根蟹。如果大家覺(jué)得還有什么因素比這四個(gè)更高的,可以在評(píng)論區(qū)提出來(lái)我補(bǔ)上去糟秘。
對(duì)于第五點(diǎn)我想做一下強(qiáng)調(diào):架構(gòu)的設(shè)計(jì)是一定需要有傳承的简逮,有傳承的架構(gòu)從整體上看會(huì)非常協(xié)調(diào)。但實(shí)際情況有可能是一個(gè)人走了尿赚,另一個(gè)頂上罐栈,即便任務(wù)交接得再完整,都不可避免不同的人有不同的架構(gòu)思路泽台,從而導(dǎo)致整個(gè)架構(gòu)的流暢程度受到影響斧吐。要解決這個(gè)問(wèn)題,一方面要盡量避免單點(diǎn)問(wèn)題冰寻,讓架構(gòu)師做架構(gòu)的時(shí)候再帶一個(gè)人须教。另一方面,架構(gòu)要設(shè)計(jì)得盡量簡(jiǎn)單斩芭,平緩接手人的學(xué)習(xí)曲線轻腺。我離開(kāi)安居客的時(shí)候乐疆,做過(guò)保證:凡是從我手里出來(lái)的代碼,終身保修贬养。所以不要想著離職了就什么事兒都不管了挤土,這不光是職業(yè)素養(yǎng)問(wèn)題,還有一個(gè)是你對(duì)你的代碼是否足夠自信的問(wèn)題煤蚌。傳承性對(duì)于View層架構(gòu)非常重要耕挨,因?yàn)樗嚯x業(yè)務(wù)最近,改動(dòng)余地最小尉桩。
所以當(dāng)各位CTO筒占、技術(shù)總監(jiān)、TeamLeader們覺(jué)得迭代周期不夠快時(shí)蜘犁,你可以先不忙著急吼吼地去招新人翰苫,《人月神話》早就說(shuō)過(guò)加人不能完全解決問(wèn)題。這時(shí)候如果你可以回過(guò)頭來(lái)看一下是不是View層架構(gòu)不合理这橙,把這個(gè)弄好也是優(yōu)化迭代周期的手段之一奏窑。
嗯,至于本系列其他三項(xiàng)的架構(gòu)方案對(duì)于迭代周期的影響程度屈扎,我認(rèn)為都不如View層架構(gòu)方案對(duì)迭代周期的影響高埃唯,所以這是我認(rèn)為View層架構(gòu)是最重要的其中一個(gè)理由。
View層架構(gòu)是最貼近業(yè)務(wù)的底層架構(gòu)
View層架構(gòu)雖然也算底層鹰晨,但還沒(méi)那么底層墨叛,它跟業(yè)務(wù)的對(duì)接面最廣,影響業(yè)務(wù)層代碼的程度也最深模蜡。在所有的底層都牽一發(fā)的時(shí)候漠趁,在View架構(gòu)上牽一發(fā)導(dǎo)致業(yè)務(wù)層動(dòng)全身的面積最大。
所以View架構(gòu)在所有架構(gòu)中一旦定型忍疾,可修改的空間就最小闯传,我們?cè)谝婚_(kāi)始考慮View相關(guān)架構(gòu)時(shí),不光要實(shí)現(xiàn)功能卤妒,還要考慮更多規(guī)范上的東西甥绿。制定規(guī)范的目的一方面是防止業(yè)務(wù)工程師的代碼腐蝕View架構(gòu),另一方面也是為了能夠有所傳承则披。按照規(guī)范來(lái)共缕,總還是不那么容易出差池的。
還有就是收叶,架構(gòu)師一開(kāi)始考慮的東西也會(huì)有很多骄呼,不可能在第一版就把它們?nèi)繉?shí)現(xiàn)共苛,對(duì)于一個(gè)尚未發(fā)版的App來(lái)說(shuō)判没,第一版架構(gòu)往往是最小完整功能集蜓萄,那么在第二版第三版的發(fā)展過(guò)程中,架構(gòu)的迭代任務(wù)就很有可能不只是你一個(gè)人的事情了澄峰,相信你一個(gè)人也不見(jiàn)得能搞定全部嫉沽。所以你要跟你的合作者們有所約定。另外俏竞,第一版出去之后绸硕,業(yè)務(wù)工程師在使用過(guò)程中也會(huì)產(chǎn)生很多修改意見(jiàn),哪些意見(jiàn)是合理的魂毁,哪些意見(jiàn)是不合理的玻佩,也要通過(guò)事先約定的規(guī)范來(lái)進(jìn)行篩選,最終決定如何采納席楚。
規(guī)范也不是一成不變的咬崔,什么時(shí)候槍斃意見(jiàn),什么時(shí)候改規(guī)范烦秩,這就要靠各位的技術(shù)和經(jīng)驗(yàn)了垮斯。
以上就是前言。
這篇文章講什么只祠?
View代碼結(jié)構(gòu)的規(guī)定
關(guān)于view的布局
何時(shí)使用storyboard兜蠕,何時(shí)使用nib,何時(shí)使用代碼寫(xiě)View
是否有必要讓業(yè)務(wù)方統(tǒng)一派生ViewController抛寝?
方便View布局的小工具
MVC熊杨、MVVM、MVCS墩剖、VIPER
本門心法
跨業(yè)務(wù)時(shí)View的處理
留給評(píng)論區(qū)各種補(bǔ)
總結(jié)
View代碼結(jié)構(gòu)的規(guī)定
架構(gòu)師不是寫(xiě)SDK出來(lái)交付業(yè)務(wù)方使用就沒(méi)事兒了的猴凹,每家公司一定都有一套代碼規(guī)范,架構(gòu)師的職責(zé)也包括定義代碼規(guī)范岭皂。按照道理來(lái)講郊霎,定代碼規(guī)范應(yīng)該是屬于通識(shí),放在這里講的原因只是因?yàn)槲疫@邊需要為View添加一個(gè)規(guī)范爷绘。
制定代碼規(guī)范嚴(yán)格來(lái)講不屬于View層架構(gòu)的事情书劝,但它對(duì)View層架構(gòu)未來(lái)的影響會(huì)比較大,也是屬于架構(gòu)師在設(shè)計(jì)View層架構(gòu)時(shí)需要考慮的事情土至。制定View層規(guī)范的重要性在于:
提高業(yè)務(wù)方View層的可讀性可維護(hù)性
防止業(yè)務(wù)代碼對(duì)架構(gòu)產(chǎn)生腐蝕
確保傳承
保持架構(gòu)發(fā)展的方向不輕易被不合理的意見(jiàn)所左右
在這一節(jié)里面我不打算從頭開(kāi)始定義一套規(guī)范购对,蘋(píng)果有一套Coding Guidelines,當(dāng)我們定代碼結(jié)構(gòu)或規(guī)范的時(shí)候陶因,首先一定要符合這個(gè)規(guī)范骡苞。
然后,相信大家各自公司里面也都有一套自己的規(guī)范,具體怎么個(gè)規(guī)范法其實(shí)也是根據(jù)各位架構(gòu)師的經(jīng)驗(yàn)而定解幽,我這邊只是建議各位在各自規(guī)范的基礎(chǔ)上再加上下面這一點(diǎn)贴见。
viewController的代碼應(yīng)該差不多是這樣:
要點(diǎn)如下:
所有的屬性都使用getter和setter
不要在viewDidLoad里面初始化你的view然后再add,這樣代碼就很難看躲株。在viewDidload里面只做addSubview的事情片部,然后在viewWillAppear里面做布局的事情(勘誤1),最后在viewDidAppear里面做Notification的監(jiān)聽(tīng)之類的事情霜定。至于屬性的初始化档悠,則交給getter去做。
比如這樣:
#pragma mark - life cycle-(void)viewDidLoad{[superviewDidLoad];self.view.backgroundColor=[UIColorwhiteColor];[self.viewaddSubview:self.firstTableView];[self.viewaddSubview:self.secondTableView];[self.viewaddSubview:self.firstFilterLabel];[self.viewaddSubview:self.secondFilterLabel];[self.viewaddSubview:self.cleanButton];[self.viewaddSubview:self.originImageView];[self.viewaddSubview:self.processedImageView];[self.viewaddSubview:self.activityIndicator];[self.viewaddSubview:self.takeImageButton];}-(void)viewWillAppear:(BOOL)animated{[superviewWillAppear:animated];CGFloatwidth=(self.view.width-30)/2.0f;self.originImageView.size=CGSizeMake(width,width);[self.originImageViewtopInContainer:70shouldResize:NO];[self.originImageViewleftInContainer:10shouldResize:NO];self.processedImageView.size=CGSizeMake(width,width);[self.processedImageViewright:10FromView:self.originImageView];[self.processedImageViewtopEqualToView:self.originImageView];CGFloatlabelWidth=self.view.width-100;self.firstFilterLabel.size=CGSizeMake(labelWidth,20);[self.firstFilterLabelleftInContainer:10shouldResize:NO];[self.firstFilterLabeltop:10FromView:self.originImageView];......}
這樣即便在屬性非常多的情況下望浩,還是能夠保持代碼整齊辖所,view的初始化都交給getter去做了∧サ拢總之就是盡量不要出現(xiàn)以下的情況:
-(void)viewDidLoad{[superviewDidLoad];self.textLabel=[[UILabelalloc]init];self.textLabel.textColor=[UIColorblackColor];self.textLabel......self.textLabel......self.textLabel......[self.viewaddSubview:self.textLabel];}
這種做法就不夠干凈奴烙,都扔到getter里面去就好了。關(guān)于這個(gè)做法剖张,在唐巧的技術(shù)博客里面有一篇文章和我所提倡的做法不同切诀,這個(gè)我會(huì)放在后面詳細(xì)論述。
getter和setter全部都放在最后
因?yàn)橐粋€(gè)ViewController很有可能會(huì)有非常多的view搔弄,就像上面給出的代碼樣例一樣幅虑,如果getter和setter寫(xiě)在前面,就會(huì)把主要邏輯扯到后面去顾犹,其他人看的時(shí)候就要先劃過(guò)一長(zhǎng)串getter和setter倒庵,這樣不太好。然后要求業(yè)務(wù)工程師寫(xiě)代碼的時(shí)候按照順序來(lái)分配代碼塊的位置炫刷,先是life cycle擎宝,然后是Delegate方法實(shí)現(xiàn),然后是event response浑玛,然后才是getters and setters绍申。這樣后來(lái)者閱讀代碼時(shí)就能省力很多。
每一個(gè)delegate都把對(duì)應(yīng)的protocol名字帶上顾彰,delegate方法不要到處亂寫(xiě)极阅,寫(xiě)到一塊區(qū)域里面去
比如UITableViewDelegate的方法集就老老實(shí)實(shí)寫(xiě)上#pragma mark - UITableViewDelegate。這樣有個(gè)好處就是涨享,當(dāng)其他人閱讀一個(gè)他并不熟悉的Delegate實(shí)現(xiàn)方法時(shí)筋搏,他只要按住command然后去點(diǎn)這個(gè)protocol名字,Xcode就能夠立刻跳轉(zhuǎn)到對(duì)應(yīng)這個(gè)Delegate的protocol定義的那部分代碼去厕隧,就省得他到處找了奔脐。
event response專門開(kāi)一個(gè)代碼區(qū)域
所有button俄周、gestureRecognizer的響應(yīng)事件都放在這個(gè)區(qū)域里面,不要到處亂放髓迎。
關(guān)于private methods栈源,正常情況下ViewController里面不應(yīng)該寫(xiě)
不是delegate方法的,不是event response方法的竖般,不是life cycle方法的,就是private method了茶鹃。對(duì)的涣雕,正常情況下ViewController里面一般是不會(huì)存在private methods的,這個(gè)private methods一般是用于日期換算闭翩、圖片裁剪啥的這種小功能挣郭。這種小功能要么把它寫(xiě)成一個(gè)category,要么把他做成一個(gè)模塊疗韵,哪怕這個(gè)模塊只有一個(gè)函數(shù)也行兑障。
ViewController基本上是大部分業(yè)務(wù)的載體,本身代碼已經(jīng)相當(dāng)復(fù)雜蕉汪,所以跟業(yè)務(wù)關(guān)聯(lián)不大的東西能不放在ViewController里面就不要放流译。另外一點(diǎn),這個(gè)private method的功能這時(shí)候只是你用得到者疤,但是將來(lái)說(shuō)不定別的地方也會(huì)用到福澡,一開(kāi)始就獨(dú)立出來(lái),有利于將來(lái)的代碼復(fù)用驹马。
為什么要這樣要求革砸?
我見(jiàn)過(guò)無(wú)數(shù)ViewController,代碼布局亂得一塌糊涂糯累,這里一個(gè)delegate那里一個(gè)getter算利,然后ViewController的代碼一般都死長(zhǎng)死長(zhǎng)的,看了就讓人頭疼泳姐。
定義好這個(gè)規(guī)范效拭,就能使得ViewController條理清晰,業(yè)務(wù)方程序員很能夠區(qū)分哪些放在ViewController里面比較合適胖秒,哪些不合適允耿。另外,也可以提高代碼的可維護(hù)性和可讀性扒怖。
關(guān)于View的布局
業(yè)務(wù)工程師在寫(xiě)View的時(shí)候一定逃不掉的就是這個(gè)命題较锡。用Frame也好用Autolayout也好,如果沒(méi)有精心設(shè)計(jì)過(guò)盗痒,布局部分一定慘不忍睹蚂蕴。
直接使用CGRectMake的話可讀性很差低散,光看那幾個(gè)數(shù)字,也無(wú)法知道view和view之間的位置關(guān)系骡楼。用Autolayout可讀性稍微好點(diǎn)兒熔号,但生成Constraint的長(zhǎng)度實(shí)在太長(zhǎng),代碼觀感不太好鸟整。
Autolayout這邊可以考慮使用Masonry引镊,代碼的可讀性就能好很多。如果還有使用Frame的篮条,可以考慮一下使用這個(gè)項(xiàng)目弟头。
這個(gè)項(xiàng)目里面提供了Frame相關(guān)的方便方法(UIView+LayoutMethods),里面的方法也基本涵蓋了所有布局的需求涉茧,可讀性非常好赴恨,使用它之后基本可以和CGRectMake說(shuō)再見(jiàn)了。因?yàn)樘熵堅(jiān)谧罱徘袚Q到支持iOS6伴栓,所以之前天貓都是用Frame布局的伦连,在天貓App中,首頁(yè)钳垮,范兒部分頁(yè)面的布局就使用了這些方法惑淳。使用這些方便方法能起到事半功倍的效果。
這個(gè)項(xiàng)目也提供了Autolayout方案下生產(chǎn)Constraints的方便方法(UIView+AEBHandyAutoLayout)饺窿,可讀性比原生好很多汛聚。我當(dāng)時(shí)在寫(xiě)這系列方法的時(shí)候還不知道有Masonry。知道有Masonry之后我特地去看了一下短荐,發(fā)現(xiàn)Masonry功能果然強(qiáng)大倚舀。不過(guò)這系列方法雖然沒(méi)有Masonry那么強(qiáng)大,但是也夠用了忍宋。當(dāng)時(shí)安居客iPad版App全部都是Autolayout來(lái)做的View布局痕貌,就是使用的這個(gè)項(xiàng)目里面的方法】放牛可讀性很好舵稠。
讓業(yè)務(wù)工程師使用良好的工具來(lái)做View的布局,能提高他們的工作效率入宦,也能減少bug發(fā)生的幾率哺徊。架構(gòu)師不光要關(guān)心那些高大上的內(nèi)容,也要多給業(yè)務(wù)工程師提供方便易用的小工具乾闰,才能發(fā)揮架構(gòu)師的價(jià)值落追。
何時(shí)使用storyboard,何時(shí)使用nib涯肩,何時(shí)使用代碼寫(xiě)View
這個(gè)問(wèn)題唐巧的博客里這篇文章也提到過(guò)轿钠,我的意見(jiàn)和他是基本一致的巢钓。
在這里我還想補(bǔ)充一些內(nèi)容:
具有一定規(guī)模的團(tuán)隊(duì)化iOS開(kāi)發(fā)(10人以上)有以下幾個(gè)特點(diǎn):
同一份代碼文件的作者會(huì)有很多,不同作者同時(shí)修改同一份代碼的情況也不少見(jiàn)疗垛。因此症汹,使用Git進(jìn)行代碼版本管理時(shí)出現(xiàn)Conflict的幾率也比較大。
需求變化非常頻繁贷腕,產(chǎn)品經(jīng)理一時(shí)一個(gè)主意背镇,為了完成需求而針對(duì)現(xiàn)有代碼進(jìn)行微調(diào)的情況,以及針對(duì)現(xiàn)有代碼的部分復(fù)用的情況也比較多泽裳。
復(fù)雜界面元素瞒斩、復(fù)雜動(dòng)畫(huà)場(chǎng)景的開(kāi)發(fā)任務(wù)比較多。
如果這三個(gè)特點(diǎn)你一看就明白了诡壁,下面的解釋就可以不用看了。如果你針對(duì)我的傾向愿意進(jìn)一步討論的荠割,可以先看我下面的解釋妹卿,看完再說(shuō)。
同一份代碼文件的作者會(huì)有很多蔑鹦,不同作者同時(shí)修改同一份代碼的情況也不少見(jiàn)夺克。因此,使用Git進(jìn)行代碼版本管理時(shí)出現(xiàn)Conflict的幾率也比較大嚎朽。
iOS開(kāi)發(fā)過(guò)程中铺纽,會(huì)遇到最蛋疼的兩種Conflict一個(gè)是project.pbxproj,另外一個(gè)就是StoryBoard或XIB哟忍。因?yàn)檫@些文件的內(nèi)容的可讀性非常差狡门,雖然蘋(píng)果在XCode5(現(xiàn)在我有點(diǎn)不確定是不是這個(gè)版本了)中對(duì)StoryBoard的文件描述方式做了一定的優(yōu)化,但只是把可讀性從非常差提升為很差锅很。
然而在StoryBoard中往往包含了多個(gè)頁(yè)面其馏,這些頁(yè)面基本上不太可能都由一個(gè)人去完成,如果另一個(gè)人在做StoryBoard的操作的時(shí)候爆安,出于某些目的動(dòng)了一下不屬于他的那個(gè)頁(yè)面叛复,比如為了美觀調(diào)整了一下位置。然后另外一個(gè)人也因?yàn)橐砑右粋€(gè)頁(yè)面扔仓,而在Storyboard中調(diào)整了一下某個(gè)其他頁(yè)面的位置褐奥。那么針對(duì)這個(gè)情況我除了說(shuō)個(gè)呵呵以外,我就只能說(shuō):祝你好運(yùn)翘簇∏寺耄看清楚哦,這還沒(méi)動(dòng)具體的頁(yè)頁(yè)面內(nèi)容呢版保。
但如果使用代碼繪制View耍群,Conflict一樣會(huì)發(fā)生义桂,但是這種Conflict就好解很多了,你懂的蹈垢。
需求變化非常頻繁慷吊,產(chǎn)品經(jīng)理一時(shí)一個(gè)主意,為了完成需求而針對(duì)現(xiàn)有代碼進(jìn)行微調(diào)的情況曹抬,以及針對(duì)現(xiàn)有代碼的部分復(fù)用的情況也比較多溉瓶。
我覺(jué)得產(chǎn)品經(jīng)理一時(shí)一個(gè)主意不是他的錯(cuò),他說(shuō)不定也是被逼的谤民,比如誰(shuí)都會(huì)來(lái)?yè)胶鸵幌庐a(chǎn)品的設(shè)計(jì)堰酿,公司里的所有人,上至CEO张足,下至基層員工都有可能對(duì)產(chǎn)品設(shè)計(jì)評(píng)頭論足触创,只要他個(gè)人有個(gè)地方用得不爽(極大可能是個(gè)人喜好)然后又正好跟產(chǎn)品經(jīng)理比較熟悉能夠搭得上話,都會(huì)提出各種意見(jiàn)为牍。產(chǎn)品經(jīng)理躲不起也惹不起哼绑,有時(shí)也是沒(méi)辦法,嗯碉咆。
但落實(shí)到工程師這邊來(lái)抖韩,這種情況就很蛋疼。因?yàn)檫@種改變有時(shí)候不光是UI疫铜,UI所對(duì)應(yīng)的邏輯也有要改的可能茂浮,工程師就會(huì)兩邊文件都改,你原來(lái)link的那個(gè)view現(xiàn)在不link了壳咕,然后你的outlet對(duì)應(yīng)也要?jiǎng)h掉席揽,這兩部分只要有一個(gè)沒(méi)做,編譯通過(guò)之后跑一下App谓厘,一會(huì)兒就crash了驹尼。看起來(lái)這不是什么大事兒庞呕,但很影響心情新翎。
另外,如果出現(xiàn)部分的代碼復(fù)用住练,比如說(shuō)某頁(yè)面下某個(gè)View也希望放在另外一個(gè)頁(yè)面里地啰,相關(guān)的操作就不是復(fù)制粘貼這么簡(jiǎn)單了,你還得重新link一遍讲逛。也很影響心情亏吝。
復(fù)雜界面元素,復(fù)雜動(dòng)畫(huà)交互場(chǎng)景的開(kāi)發(fā)任務(wù)比較多盏混。
要是想在基于StoryBoard的項(xiàng)目中做一個(gè)動(dòng)畫(huà)蔚鸥,很煩惜论。做幾個(gè)復(fù)雜界面元素,也很煩止喷。有的時(shí)候我們掛Custom View上去馆类,其實(shí)在StoryBoard里面看來(lái)就是一個(gè)空白View。然后另外一點(diǎn)就是弹谁,當(dāng)你的layout出現(xiàn)問(wèn)題需要調(diào)整的時(shí)候奉狈,還是挺難找到問(wèn)題所在的萄金,尤其是在復(fù)雜界面元素的情況下飒泻。
所以在針對(duì)View層這邊的要求時(shí)眼五,我也是建議不要用StoryBoard。實(shí)現(xiàn)簡(jiǎn)單的東西植康,用Code一樣簡(jiǎn)單旷太,實(shí)現(xiàn)復(fù)雜的東西,Code比StoryBoard更簡(jiǎn)單销睁。所以我更加提倡用code去畫(huà)view而不是storyboard供璧。
是否有必要讓業(yè)務(wù)方統(tǒng)一派生ViewController
有的時(shí)候我們出于記錄用戶操作行為數(shù)據(jù)的需要,或者統(tǒng)一配置頁(yè)面的目的榄攀,會(huì)從UIViewController里面派生一個(gè)自己的ViewController嗜傅,來(lái)執(zhí)行一些通用邏輯金句。比如天貓客戶端要求所有的ViewController都要繼承自TMViewController檩赢。這個(gè)統(tǒng)一的父類里面針對(duì)一個(gè)ViewController的所有生命周期都做了一些設(shè)置,至于這里都有哪些設(shè)置對(duì)于本篇文章來(lái)說(shuō)并不重要违寞。在這里我想討論的是贞瞒,在設(shè)計(jì)View架構(gòu)時(shí),如果為了能夠達(dá)到統(tǒng)一設(shè)置或執(zhí)行統(tǒng)一邏輯的目的趁曼,使用派生的手段是有必要的嗎军浆?
我覺(jué)得沒(méi)有必要,為什么沒(méi)有必要挡闰?
使用派生比不使用派生更容易增加業(yè)務(wù)方的使用成本
不使用派生手段一樣也能達(dá)到統(tǒng)一設(shè)置的目的
這兩條原因是我認(rèn)為沒(méi)有必要使用派生手段的理由乒融,如果兩條理由你都心領(lǐng)神會(huì),那么下面的就可以不用看了摄悯。如果你還有點(diǎn)疑惑赞季,請(qǐng)看下面我來(lái)詳細(xì)講一下原因。
為什么使用了派生奢驯,業(yè)務(wù)方的使用成本會(huì)提升申钩?
其實(shí)不光是業(yè)務(wù)方的使用成本,架構(gòu)的維護(hù)成本也會(huì)上升瘪阁。那么具體的成本都來(lái)自于哪里呢撒遣?
集成成本
這里講的集成成本是這樣的:如果業(yè)務(wù)方自己開(kāi)了一個(gè)獨(dú)立demo邮偎,快速完成了某個(gè)獨(dú)立流程,現(xiàn)在他想把這個(gè)現(xiàn)有流程集合進(jìn)去义黎。那么問(wèn)題就來(lái)了禾进,他需要把所有獨(dú)立的UIViewController改變成TMViewController。那為什么不是一開(kāi)始就立刻使用TMViewController呢轩缤?因?yàn)橐胍隩MViewController命迈,就要引入整個(gè)天貓App所有的業(yè)務(wù)線,所有的基礎(chǔ)庫(kù)火的,因?yàn)檫@個(gè)父類里面涉及很多天貓環(huán)境才有的內(nèi)容壶愤,所謂拔出蘿卜帶出泥,你要是想簡(jiǎn)單繼承一下就能搞定的事情馏鹤,搭環(huán)境就要搞半天征椒,然后這個(gè)小Demo才能跑得起來(lái)。
對(duì)于業(yè)務(wù)層存在的所有父類來(lái)說(shuō)湃累,它們是很容易跟項(xiàng)目中的其他代碼糾纏不清的勃救,這使得業(yè)務(wù)方開(kāi)發(fā)時(shí)遇到一個(gè)兩難問(wèn)題:要么把所有依賴全部搞定,然后基于App環(huán)境(比如天貓)下開(kāi)發(fā)Demo治力,要么就是自己Demo寫(xiě)好之后蒙秒,按照環(huán)境要求改代碼。這里的兩難問(wèn)題都會(huì)帶來(lái)成本宵统,都會(huì)影響業(yè)務(wù)方的迭代進(jìn)度晕讲。
我不確定各位所在公司是否會(huì)有這樣的情況,但我可以在這里給大家舉一個(gè)我在阿里的真實(shí)的例子:我最近在開(kāi)發(fā)某濾鏡Demo和相關(guān)頁(yè)面流程马澈,最終是要合并到天貓這個(gè)App里面去的瓢省。使用天貓環(huán)境進(jìn)行開(kāi)發(fā)的話,pod install完所有依賴差不多需要10分鐘痊班,然后打開(kāi)workspace之后勤婚,差不多要再等待1分鐘讓xcode做好索引,然后才能正式開(kāi)始工作涤伐。在這里要感謝一下則平馒胆,因?yàn)樗诖嘶A(chǔ)上做了很多優(yōu)化,使得這個(gè)1分鐘已經(jīng)比原來(lái)的時(shí)間短很多了凝果。但如果天貓環(huán)境有更新祝迂,你就要再重復(fù)一次上面的流程,否則 就很有可能編譯不過(guò)豆村。
拜托液兽,我只是想做個(gè)Demo而已,不想搞那么復(fù)雜。
上手接受成本
新來(lái)的業(yè)務(wù)工程師有的時(shí)候不見(jiàn)得都記得每一個(gè)ViewController都必須要派生自TMViewController而不是直接的UIViewController四啰。新來(lái)的工程師他不能直接按照蘋(píng)果原生的做法去做事情宁玫,他需要額外學(xué)習(xí),比如說(shuō):所有的ViewController都必須繼承自TMViewController柑晒。
架構(gòu)的維護(hù)難度
盡可能少地使用繼承能提高項(xiàng)目的可維護(hù)性欧瘪,具體內(nèi)容我在《跳出面向?qū)ο笏枷耄ㄒ唬?繼承》里面說(shuō)了,在這里我想偷懶不想把那篇文章里說(shuō)過(guò)的東西再說(shuō)一遍匙赞。
其實(shí)對(duì)于業(yè)務(wù)方來(lái)說(shuō)佛掖,主要還是第一個(gè)集成成本比較蛋疼,因?yàn)檫@是長(zhǎng)痛涌庭,每次要做點(diǎn)什么事情都會(huì)遇到芥被。第二點(diǎn)倒還好,短痛坐榆。第三點(diǎn)跟業(yè)務(wù)工程師沒(méi)啥關(guān)系拴魄。
那么如果不使用派生,我們應(yīng)該使用什么手段席镀?
我的建議是使用AOP匹中。
在架構(gòu)師實(shí)現(xiàn)具體的方案之前,必須要想清楚幾個(gè)問(wèn)題豪诲,然后才能決定采用哪種方案顶捷。是哪幾個(gè)問(wèn)題?
方案的效果屎篱,和最終要達(dá)到的目的是什么服赎?
在自己的知識(shí)體系里面,是否具備實(shí)現(xiàn)這個(gè)方案的能力芳室?
在業(yè)界已有的開(kāi)源組件里面专肪,是否有可以直接拿來(lái)用的輪子刹勃?
這三個(gè)問(wèn)題按照順序一一解答之后堪侯,具體方案就能出來(lái)了。
我們先看第一個(gè)問(wèn)題:方案的效果荔仁,和最終要達(dá)到的目的是什么伍宦?
方案的效果應(yīng)該是:
業(yè)務(wù)方可以不用通過(guò)繼承的方法,然后框架能夠做到對(duì)ViewController的統(tǒng)一配置乏梁。
業(yè)務(wù)方即使脫離框架環(huán)境次洼,不需要修改任何代碼也能夠跑完代碼。業(yè)務(wù)方的ViewController一旦丟入框架環(huán)境遇骑,不需要修改任何代碼卖毁,框架就能夠起到它應(yīng)該起的作用。
其實(shí)就是要實(shí)現(xiàn)不通過(guò)業(yè)務(wù)代碼上對(duì)框架的主動(dòng)迎合,使得業(yè)務(wù)能夠被框架感知這樣的功能亥啦。細(xì)化下來(lái)就是兩個(gè)問(wèn)題炭剪,框架要能夠攔截到ViewController的生命周期,另一個(gè)問(wèn)題就是翔脱,攔截的定義時(shí)機(jī)奴拦。
對(duì)于方法攔截,很容易想到Method Swizzling届吁,那么我們可以寫(xiě)一個(gè)實(shí)例错妖,在App啟動(dòng)的時(shí)候添加針對(duì)UIViewController的方法攔截,這是一種做法疚沐。還有另一種做法就是暂氯,使用NSObject的load函數(shù),在應(yīng)用啟動(dòng)時(shí)自動(dòng)監(jiān)聽(tīng)亮蛔。使用后者的好處在于株旷,這個(gè)模塊只要被項(xiàng)目包含,就能夠發(fā)揮作用尔邓,不需要在項(xiàng)目里面添加任何代碼晾剖。
然后另外一個(gè)要考慮的事情就是,原有的TMViewController(所謂的父類)也是會(huì)提供額外方法方便子類使用的梯嗽,Method Swizzling只支持針對(duì)現(xiàn)有方法的操作齿尽,拓展方法的話,嗯灯节,當(dāng)然是用Category啦循头。
我本人不贊成Category的過(guò)度使用,但鑒于Category是最典型的化繼承為組合的手段炎疆,在這個(gè)場(chǎng)景下還是適合使用的卡骂。還有的就是,關(guān)于Method Swizzling手段實(shí)現(xiàn)方法攔截形入,業(yè)界也已經(jīng)有了現(xiàn)成的開(kāi)源庫(kù):Aspects全跨,我們可以直接拿來(lái)使用。
我這邊有個(gè)非常非常小的Demo可以放出來(lái)給大家亿遂,這個(gè)Demo只是一個(gè)點(diǎn)睛之筆浓若,有一些話我也寫(xiě)在這個(gè)Demo里面了,各位架構(gòu)師們你們可以基于各自公司App的需求去拓展蛇数。
這個(gè)Demo不包含Category挪钓,畢竟Category還是得你們自己去寫(xiě)啊~然后這套方案能夠完成原來(lái)通過(guò)派生手段所有可以完成的任務(wù),但同時(shí)又允許業(yè)務(wù)方不必添加任何代碼耳舅,直接使用原生的UIViewController碌上。
然后另外要提醒的是,這方案的目的是消除不必要的繼承,雖然不限定于UIViewController馏予,但它也是有適用范圍的蔓纠,在適用繼承的地方,還是要老老實(shí)實(shí)使用繼承吗蚌。比如你有一個(gè)數(shù)據(jù)模型腿倚,是由基本模型派生出的一整套模型,那么這個(gè)時(shí)候還是老老實(shí)實(shí)使用繼承蚯妇。至于拿捏何時(shí)使用繼承敷燎,相信各位架構(gòu)師一定能夠處理好,或者你也可以參考我前面提到的那篇文章來(lái)控制拿捏的尺度箩言。
關(guān)于MVC硬贯、MVVM等一大堆思想
其實(shí)這些都是相對(duì)通用的思想,萬(wàn)變不離其宗的還是在開(kāi)篇里面我提到的那三個(gè)角色:數(shù)據(jù)管理者陨收,數(shù)據(jù)加工者饭豹,數(shù)據(jù)展示者。這些五花八門的思想务漩,不外乎就是制訂了一個(gè)規(guī)范拄衰,規(guī)定了這三個(gè)角色應(yīng)當(dāng)如何進(jìn)行數(shù)據(jù)交換。但同時(shí)這些也是爭(zhēng)議最多的話題饵骨,所以我在這里來(lái)把幾個(gè)主流思想做一個(gè)梳理翘悉,當(dāng)你在做View層架構(gòu)時(shí),能夠有個(gè)比較好的參考居触。
MVC
MVC(Model-View-Controller)是最老牌的的思想妖混,老牌到4人幫的書(shū)里把它歸成了一種模式,其中Model就是作為數(shù)據(jù)管理者轮洋,View作為數(shù)據(jù)展示者制市,Controller作為數(shù)據(jù)加工者,Model和View又都是由Controller來(lái)根據(jù)業(yè)務(wù)需求調(diào)配弊予,所以Controller還負(fù)擔(dān)了一個(gè)數(shù)據(jù)流調(diào)配的功能祥楣。正在我寫(xiě)這篇文章的時(shí)候,我看到InfoQ發(fā)了這篇文章块促,里面提到了一個(gè)移動(dòng)開(kāi)發(fā)中的痛點(diǎn)是:對(duì)MVC架構(gòu)劃分的理解荣堰。我當(dāng)時(shí)沒(méi)能夠去參加這個(gè)座談會(huì)床未,也沒(méi)辦法發(fā)表個(gè)人意見(jiàn)竭翠,所以就只能在這里寫(xiě)寫(xiě)了。
在iOS開(kāi)發(fā)領(lǐng)域薇搁,我們應(yīng)當(dāng)如何進(jìn)行MVC的劃分斋扰?
這里面其實(shí)有兩個(gè)問(wèn)題:
為什么我們會(huì)糾結(jié)于iOS開(kāi)發(fā)領(lǐng)域中MVC的劃分問(wèn)題?
在iOS開(kāi)發(fā)領(lǐng)域中,怎樣才算是劃分的正確姿勢(shì)传货?
為什么我們會(huì)糾結(jié)于iOS開(kāi)發(fā)領(lǐng)域中MVC的劃分問(wèn)題屎鳍?
關(guān)于這個(gè),每個(gè)人糾結(jié)的點(diǎn)可能不太一樣问裕,我也不知道當(dāng)時(shí)座談會(huì)上大家的觀點(diǎn)逮壁。但請(qǐng)?jiān)试S我猜一下:是不是因?yàn)閁IViewController中自帶了一個(gè)View,且控制了View的整個(gè)生命周期(viewDidLoad,viewWillAppear...)粮宛,而在常識(shí)中我們都知道Controller不應(yīng)該和View有如此緊密的聯(lián)系窥淆,所以才導(dǎo)致大家對(duì)劃分產(chǎn)生困惑?巍杈,下面我會(huì)針對(duì)這個(gè)猜測(cè)來(lái)給出我的意見(jiàn)忧饭。
在服務(wù)端開(kāi)發(fā)領(lǐng)域,Controller和View的交互方式一般都是這樣筷畦,比如Yii:
/*...數(shù)據(jù)庫(kù)取數(shù)據(jù)...處理數(shù)據(jù)...*/// 此處$this就是Controller$this->render("plan",array('planList' => $planList,'plan_id' => $_GET['id'],));
這里Controller和View之間區(qū)分得非常明顯词裤,Controller做完自己的事情之后,就把所有關(guān)于View的工作交給了頁(yè)面渲染引擎去做鳖宾,Controller不會(huì)去做任何關(guān)于View的事情吼砂,包括生成View,這些都由渲染引擎代勞了鼎文。這是一個(gè)區(qū)別帅刊,但其實(shí)服務(wù)端View的概念和Native應(yīng)用View的概念,真正的區(qū)別在于:從概念上嚴(yán)格劃分的話漂问,服務(wù)端其實(shí)根本沒(méi)有View赖瞒,拜HTTP協(xié)議所賜,我們平時(shí)所討論的View只是用于描述View的字符串(更實(shí)質(zhì)的應(yīng)該稱之為數(shù)據(jù))蚤假,真正的View是瀏覽器栏饮。。
所以服務(wù)端只管生成對(duì)View的描述磷仰,至于對(duì)View的長(zhǎng)相袍嬉,UI事件監(jiān)聽(tīng)和處理,都是瀏覽器負(fù)責(zé)生成和維護(hù)的灶平。但是在Native這邊來(lái)看伺通,原本屬于瀏覽器的任務(wù)也逃不掉要自己做。那么這件事情由誰(shuí)來(lái)做最合適逢享?蘋(píng)果給出的答案是:UIViewController罐监。
鑒于蘋(píng)果在這一層做了很多艱苦卓絕的努力,讓iOS工程師們不必親自去實(shí)現(xiàn)這些內(nèi)容瞒爬。而且弓柱,它把所有的功能都放在了UIView上沟堡,并且把UIView做成不光可以展示UI,還可以作為容器的一個(gè)對(duì)象矢空。
看到這兒你明白了嗎航罗?UIView的另一個(gè)身份其實(shí)是容器!UIViewController中自帶的那個(gè)view屁药,它的主要任務(wù)就是作為一個(gè)容器粥血。如果它所有的相關(guān)命名都改成ViewContainer,那么代碼就會(huì)變成這樣:
-(void)viewContainerDidLoad{[self.viewContaineraddSubview:self.label];[self.viewContaineraddSubview:self.tableView];[self.viewContaineraddSubview:self.button];[self.viewContaineraddSubview:self.textField];}......
僅僅改了個(gè)名字酿箭,現(xiàn)在是不是感覺(jué)清晰了很多立莉?如果再要說(shuō)詳細(xì)一點(diǎn),我們平常所認(rèn)為的服務(wù)端MVC是這樣劃分的:
---------------------------
| C? ? ? ? ? ? ? ? ? ? ? |
|? ? ? ? Controller? ? ? |
|? ? ? ? ? ? ? ? ? ? ? ? |
---------------------------
/? ? ? ? ? ? ? ? ? ? ? ? ? \
/? ? ? ? ? ? ? ? ? ? ? ? ? ? \
/? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
------------? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ---------------------
| M? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? | V? ? ? ? ? ? ? ? |
|? Model? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? Render Engine? |
|? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? +? ? ? ? |
------------? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? HTML Files? |
---------------------
但事實(shí)上七问,整套流程的MVC劃分是這樣:
---------------------------
| C? ? ? ? ? ? ? ? ? ? ? |
|? Controller? ? ? ? ? ? |
|? ? ? ? ? \? ? ? ? ? ? |
|? ? ? ? ? Render Engine |
|? ? ? ? ? ? ? ? +? ? ? |
|? ? ? ? ? ? HTML Files? |
---------------------------
/? ? ? ? ? ? ? ? ? ? ? ? ? \
/? ? ? ? ? ? ? ? ? ? ? ? ? ? \ HTML String
/? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
------------? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ---------------
| M? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? | V? ? ? ? ? |
|? Model? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? Browser? |
|? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? |
------------? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ---------------
由圖中可以看出蜓耻,我們服務(wù)端開(kāi)發(fā)在這個(gè)概念下,其實(shí)只涉及M和C的開(kāi)發(fā)工作械巡,瀏覽器作為View的容器刹淌,負(fù)責(zé)View的展示和事件的監(jiān)聽(tīng)。那么對(duì)應(yīng)到iOS客戶端的MVC劃分上面來(lái)讥耗,就是這樣:
----------------------------
| C? ? ? ? ? ? ? ? ? ? ? ? |
|? Controller? ? ? ? ? ? |
|? ? ? ? ? \? ? ? ? ? ? ? |
|? ? ? ? ? View Container |
----------------------------
/? ? ? ? ? ? ? ? ? ? ? ? ? ? \
/? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
/? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
------------? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ----------------------
| M? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? | V? ? ? ? ? ? ? ? ? |
|? Model? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? UITableView? ? |
|? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? YourCustomView? |
------------? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ...? ? ? ? |
----------------------
唯一區(qū)別在于有勾,View的容器在服務(wù)端,是由Browser負(fù)責(zé)古程,在整個(gè)網(wǎng)站的流程中蔼卡,這個(gè)容器放在Browser是非常合理的。在iOS客戶端挣磨,View的容器是由UIViewController中的view負(fù)責(zé)雇逞,我也覺(jué)得蘋(píng)果做的這個(gè)選擇是非常正確明智的。
因?yàn)闉g覽器和服務(wù)端之間的關(guān)系非常松散茁裙,而且他們分屬于兩個(gè)不同陣營(yíng)塘砸,服務(wù)端將對(duì)View的描述生成之后,交給瀏覽器去負(fù)責(zé)展示晤锥,然而一旦view上有什么事件產(chǎn)生掉蔬,基本上是很少傳遞到服務(wù)器(也就是所謂的Controller)的(要傳也可以:AJAX),都是在瀏覽器這邊把事情都做掉矾瘾,所以在這種情況下女轿,View容器就適合放在瀏覽器(V)這邊。
但是在iOS開(kāi)發(fā)領(lǐng)域壕翩,雖然也有讓View去監(jiān)聽(tīng)事件的做法蛉迹,但這種做法非常少,都是把事件回傳給Controller戈泼,然后Controller再另行調(diào)度婿禽。所以這時(shí)候赏僧,View的容器放在Controller就非常合適大猛。Controller可以因?yàn)椴煌录漠a(chǎn)生去很方便地更改容器內(nèi)容扭倾,比如加載失敗時(shí),把容器內(nèi)容換成失敗頁(yè)面的View挽绩,無(wú)網(wǎng)絡(luò)時(shí)膛壹,把容器頁(yè)面換成無(wú)網(wǎng)絡(luò)的View等等。
在iOS開(kāi)發(fā)領(lǐng)域中唉堪,怎樣才算是MVC劃分的正確姿勢(shì)模聋?
這個(gè)問(wèn)題其實(shí)在上面已經(jīng)解答掉一部分了,那么這個(gè)問(wèn)題的答案就當(dāng)是對(duì)上面問(wèn)題的一個(gè)總結(jié)吧唠亚。
M應(yīng)該做的事:
給ViewController提供數(shù)據(jù)
給ViewController存儲(chǔ)數(shù)據(jù)提供接口
提供經(jīng)過(guò)抽象的業(yè)務(wù)基本組件链方,供Controller調(diào)度
C應(yīng)該做的事:
管理View Container的生命周期
負(fù)責(zé)生成所有的View實(shí)例,并放入View Container
監(jiān)聽(tīng)來(lái)自View與業(yè)務(wù)有關(guān)的事件灶搜,通過(guò)與Model的合作祟蚀,來(lái)完成對(duì)應(yīng)事件的業(yè)務(wù)。
V應(yīng)該做的事:
響應(yīng)與業(yè)務(wù)無(wú)關(guān)的事件割卖,并因此引發(fā)動(dòng)畫(huà)效果前酿,點(diǎn)擊反饋(如果合適的話,盡量還是放在View去做)等鹏溯。
界面元素表達(dá)
我通過(guò)與服務(wù)端MVC劃分的對(duì)比來(lái)回答了這兩個(gè)問(wèn)題罢维,之所以這么做,是因?yàn)槲抑烙泻芏鄆OS工程師之前是從服務(wù)端轉(zhuǎn)過(guò)來(lái)的丙挽。我也是這樣肺孵,在進(jìn)安居客之前,我也是做服務(wù)端開(kāi)發(fā)的颜阐,在學(xué)習(xí)iOS的過(guò)程中悬槽,我也曾經(jīng)對(duì)iOS領(lǐng)域的MVC劃分問(wèn)題產(chǎn)生過(guò)疑惑,我疑惑的點(diǎn)就是前面開(kāi)篇我猜測(cè)的點(diǎn)瞬浓。如果有人問(wèn)我iOS中應(yīng)該怎么做MVC的劃分初婆,我就會(huì)像上面這么回答。
MVCS
蘋(píng)果自身就采用的是這種架構(gòu)思路猿棉,從名字也能看出磅叛,也是基于MVC衍生出來(lái)的一套架構(gòu)。從概念上來(lái)說(shuō)萨赁,它拆分的部分是Model部分弊琴,拆出來(lái)一個(gè)Store。這個(gè)Store專門負(fù)責(zé)數(shù)據(jù)存取杖爽。但從實(shí)際操作的角度上講敲董,它拆開(kāi)的是Controller紫皇。
這算是瘦Model的一種方案,瘦Model只是專門用于表達(dá)數(shù)據(jù)腋寨,然后存儲(chǔ)窃款、數(shù)據(jù)處理都交給外面的來(lái)做量淌。MVCS使用的前提是,它假設(shè)了你是瘦Model,同時(shí)數(shù)據(jù)的存儲(chǔ)和處理都在Controller去做檀葛。所以對(duì)應(yīng)到MVCS摧扇,它在一開(kāi)始就是拆分的Controller铐达。因?yàn)镃ontroller做了數(shù)據(jù)存儲(chǔ)的事情秧饮,就會(huì)變得非常龐大,那么就把Controller專門負(fù)責(zé)存取數(shù)據(jù)的那部分抽離出來(lái)穗泵,交給另一個(gè)對(duì)象去做普气,這個(gè)對(duì)象就是Store。這么調(diào)整之后佃延,整個(gè)結(jié)構(gòu)也就變成了真正意義上的MVCS现诀。
關(guān)于胖Model和瘦Model
我在面試和跟別人聊天時(shí),發(fā)現(xiàn)知道胖Model和瘦Model的概念的人不是很多苇侵。大約兩三年前國(guó)外業(yè)界曾經(jīng)對(duì)此有過(guò)非常激烈的討論赶盔,主題就是Fat model, skinny controller。現(xiàn)在關(guān)于這方面的討論已經(jīng)不多了榆浓,然而直到今天胖Model和瘦Model哪個(gè)更好于未,業(yè)界也還沒(méi)有定論,所以這算是目前業(yè)界懸而未解的一個(gè)爭(zhēng)議陡鹃。我很少看到國(guó)內(nèi)有討論這個(gè)的資料烘浦,所以在這里我打算補(bǔ)充一下什么叫胖Model什么叫瘦Model。以及他們的爭(zhēng)論來(lái)源于何處萍鲸。
什么叫胖Model闷叉?
胖Model包含了部分弱業(yè)務(wù)邏輯。胖Model要達(dá)到的目的是脊阴,Controller從胖Model這里拿到數(shù)據(jù)之后握侧,不用額外做操作或者只要做非常少的操作,就能夠?qū)?shù)據(jù)直接應(yīng)用在View上嘿期。舉個(gè)例子:
RawData:timestamp:1234567FatModel:@property(nonatomic,assign)CGFloattimestamp;-(NSString*)ymdDateString;// 2015-04-20 15:16-(NSString*)gapString;// 3分鐘前品擎、1小時(shí)前、一天前备徐、2015-3-13 12:34Controller:self.dateLabel.text=[FatModelymdDateString];self.gapLabel.text=[FatModelgapString];
把timestamp轉(zhuǎn)換成具體業(yè)務(wù)上所需要的字符串萄传,這屬于業(yè)務(wù)代碼,算是弱業(yè)務(wù)蜜猾。FatModel做了這些弱業(yè)務(wù)之后秀菱,Controller就能變得非常skinny振诬,Controller只需要關(guān)注強(qiáng)業(yè)務(wù)代碼就行了。眾所周知衍菱,強(qiáng)業(yè)務(wù)變動(dòng)的可能性要比弱業(yè)務(wù)大得多赶么,弱業(yè)務(wù)相對(duì)穩(wěn)定,所以弱業(yè)務(wù)塞進(jìn)Model里面是沒(méi)問(wèn)題的梦碗。另一方面禽绪,弱業(yè)務(wù)重復(fù)出現(xiàn)的頻率要大于強(qiáng)業(yè)務(wù)蓖救,對(duì)復(fù)用性的要求更高洪规,如果這部分業(yè)務(wù)寫(xiě)在Controller,類似的代碼會(huì)灑得到處都是循捺,一旦弱業(yè)務(wù)有修改(弱業(yè)務(wù)修改頻率低不代表就沒(méi)有修改)斩例,這個(gè)事情就是一個(gè)災(zāi)難。如果塞到Model里面去从橘,改一處很多地方就能跟著改念赶,就能避免這場(chǎng)災(zāi)難。
然而其缺點(diǎn)就在于恰力,胖Model相對(duì)比較難移植叉谜,雖然只是包含弱業(yè)務(wù),但好歹也是業(yè)務(wù)踩萎,遷移的時(shí)候很容易拔出蘿卜帶出泥停局。另外一點(diǎn),MVC的架構(gòu)思想更加傾向于Model是一個(gè)Layer香府,而不是一個(gè)Object董栽,不應(yīng)該把一個(gè)Layer應(yīng)該做的事情交給一個(gè)Object去做。最后一點(diǎn)企孩,軟件是會(huì)成長(zhǎng)的锭碳,F(xiàn)atModel很有可能隨著軟件的成長(zhǎng)越來(lái)越Fat,最終難以維護(hù)勿璃。
什么叫瘦Model擒抛?
瘦Model只負(fù)責(zé)業(yè)務(wù)數(shù)據(jù)的表達(dá),所有業(yè)務(wù)無(wú)論強(qiáng)弱一律扔到Controller补疑。瘦Model要達(dá)到的目的是歧沪,盡一切可能去編寫(xiě)細(xì)粒度Model,然后配套各種helper類或方法來(lái)對(duì)弱業(yè)務(wù)做抽象癣丧,強(qiáng)業(yè)務(wù)依舊交給Controller槽畔。舉個(gè)例子:
RawData:{"name":"casa","sex":"male",}SlimModel:@property(nonatomic,strong)NSString*name;@property(nonatomic,strong)NSString*sex;Helper:#define Male 1;#define Female 0;+(BOOL)sexWithString:(NSString*)sex;Controller:if([HelpersexWithString:SlimModel.sex]==Male){...}
由于SlimModel跟業(yè)務(wù)完全無(wú)關(guān),它的數(shù)據(jù)可以交給任何一個(gè)能處理它數(shù)據(jù)的Helper或其他的對(duì)象胁编,來(lái)完成業(yè)務(wù)厢钧。在代碼遷移的時(shí)候獨(dú)立性很強(qiáng)鳞尔,很少會(huì)出現(xiàn)拔出蘿卜帶出泥的情況。另外早直,由于SlimModel只是數(shù)據(jù)表達(dá)寥假,對(duì)它進(jìn)行維護(hù)基本上是0成本,軟件膨脹得再厲害霞扬,SlimModel也不會(huì)大到哪兒去糕韧。
缺點(diǎn)就在于,Helper這種做法也不見(jiàn)得很好喻圃,這里有一篇文章批判了這個(gè)事情萤彩。另外,由于Model的操作會(huì)出現(xiàn)在各種地方斧拍,SlimModel在一定程度上違背了DRY(Don't Repeat Yourself)的思路雀扶,Controller仍然不可避免在一定程度上出現(xiàn)代碼膨脹。
我的態(tài)度肆汹?嗯愚墓,我會(huì)在本門心法這一節(jié)里面說(shuō)。
說(shuō)回來(lái)昂勉,MVCS是基于瘦Model的一種架構(gòu)思路浪册,把原本Model要做的很多事情中的其中一部分關(guān)于數(shù)據(jù)存儲(chǔ)的代碼抽象成了Store,在一定程度上降低了Controller的壓力岗照。
MVVM
MVVM去年在業(yè)界討論得非常多村象,無(wú)論國(guó)內(nèi)還是國(guó)外都討論得非常熱烈,尤其是在ReactiveCocoa這個(gè)庫(kù)成熟之后谴返,ViewModel和View的信號(hào)機(jī)制在iOS下終于有了一個(gè)相對(duì)優(yōu)雅的實(shí)現(xiàn)煞肾。MVVM本質(zhì)上也是從MVC中派生出來(lái)的思想,MVVM著重想要解決的問(wèn)題是盡可能地減少Controller的任務(wù)嗓袱。不管MVVM也好籍救,MVCS也好,他們的共識(shí)都是Controller會(huì)隨著軟件的成長(zhǎng)渠抹,變很大很難維護(hù)很難測(cè)試蝙昙。只不過(guò)兩種架構(gòu)思路的前提不同,MVCS是認(rèn)為Controller做了一部分Model的事情梧却,要把它拆出來(lái)變成Store奇颠,MVVM是認(rèn)為Controller做了太多數(shù)據(jù)加工的事情,所以MVVM把數(shù)據(jù)加工的任務(wù)從Controller中解放了出來(lái)放航,使得Controller只需要專注于數(shù)據(jù)調(diào)配的工作烈拒,ViewModel則去負(fù)責(zé)數(shù)據(jù)加工并通過(guò)通知機(jī)制讓View響應(yīng)ViewModel的改變。
MVVM是基于胖Model的架構(gòu)思路建立的,然后在胖Model中拆出兩部分:Model和ViewModel荆几。關(guān)于這個(gè)觀點(diǎn)我要做一個(gè)額外解釋:胖Model做的事情是先為Controller減負(fù)吓妆,然后由于Model變胖,再在此基礎(chǔ)上拆出ViewModel吨铸,跟業(yè)界普遍認(rèn)知的MVVM本質(zhì)上是為Controller減負(fù)這個(gè)說(shuō)法并不矛盾行拢,因?yàn)榕諱odel做的事情也是為Controller減負(fù)。
另外诞吱,我前面說(shuō)MVVM把數(shù)據(jù)加工的任務(wù)從Controller中解放出來(lái)舟奠,跟MVVM拆分的是胖Model也不矛盾。要做到解放Controller房维,首先你得有個(gè)胖Model沼瘫,然后再把這個(gè)胖Model拆成Model和ViewModel。
那么MVVM究竟應(yīng)該如何實(shí)現(xiàn)握巢?
這很有可能是大多數(shù)人糾結(jié)的問(wèn)題晕鹊,我打算憑我的個(gè)人經(jīng)驗(yàn)試圖在這里回答這個(gè)問(wèn)題松却,歡迎大家在評(píng)論區(qū)交流暴浦。
在iOS領(lǐng)域大部分MVVM架構(gòu)都會(huì)使用ReactiveCocoa,但是使用ReactiveCocoa的iOS應(yīng)用就是基于MVVM架構(gòu)的嗎晓锻?那當(dāng)然不是歌焦,我覺(jué)得很多人都存在這個(gè)誤區(qū),我面試過(guò)的一些人提到了ReactiveCocoa也提到了MVVM砚哆,但他們對(duì)此的理解膚淺得讓我忍俊不禁独撇。嗯,在網(wǎng)絡(luò)層架構(gòu)我會(huì)舉出不使用ReactiveCocoa的例子躁锁,現(xiàn)在舉我感覺(jué)有點(diǎn)兒早纷铣。
MVVM的關(guān)鍵是要有View Model!而不是ReactiveCocoa(勘誤2)
ViewModel做什么事情战转?就是把RawData變成直接能被View使用的對(duì)象的一種Model搜立。舉個(gè)例子:
Raw Data:
{
(
(123, 456),
(234, 567),
(345, 678)
)
}
這里的RawData我們假設(shè)是經(jīng)緯度,數(shù)字我隨便寫(xiě)的不要太在意槐秧。然后你有一個(gè)模塊是地圖模塊啄踊,把經(jīng)緯度數(shù)組全部都轉(zhuǎn)變成MKAnnotation或其派生類對(duì)于Controller來(lái)說(shuō)是弱業(yè)務(wù),(記住刁标,胖Model就是用來(lái)做弱業(yè)務(wù)的)颠通,因此我們用ViewModel直接把它轉(zhuǎn)變成MKAnnotation的NSArray,交給Controller之后Controller直接就可以用了膀懈。
嗯顿锰,這就是ViewModel要做的事情,是不是覺(jué)得很簡(jiǎn)單,看不出優(yōu)越性硼控?
安居客Pad應(yīng)用也有一個(gè)地圖模塊乘客,在這里我設(shè)計(jì)了一個(gè)對(duì)象叫做reformer(其實(shí)就是ViewModel),專門用來(lái)干這個(gè)事情淀歇。那么這么做的優(yōu)越性體現(xiàn)在哪兒呢易核?
安居客分三大業(yè)務(wù):租房、二手房浪默、新房牡直。這三個(gè)業(yè)務(wù)對(duì)應(yīng)移動(dòng)開(kāi)發(fā)團(tuán)隊(duì)有三個(gè)API開(kāi)發(fā)團(tuán)隊(duì),他們各自為政纳决,這就造成了一個(gè)結(jié)果:三個(gè)API團(tuán)隊(duì)回饋給移動(dòng)客戶端的數(shù)據(jù)內(nèi)容雖然一致碰逸,但是數(shù)據(jù)格式是不一致的,也就是相同value對(duì)應(yīng)的key是不一致的阔加。但展示地圖的ViewController不可能寫(xiě)三個(gè)饵史,所以肯定少不了要有一個(gè)API數(shù)據(jù)兼容的邏輯,這個(gè)邏輯我就放在reformer里面去做了胜榔,于是業(yè)務(wù)流程就變成了這樣:
用戶進(jìn)入地圖頁(yè)發(fā)起地圖API請(qǐng)求|||-----------------------------------------||||||新房API二手房API租房API||||||-----------------------------------------|||獲得原始地圖數(shù)據(jù)|||[APIManagerfetchDataWithReformer:reformer]|||MKAnnotationList|||Controller
這么一來(lái)胳喷,原本復(fù)雜的MKAnnotation組裝邏輯就從Controller里面拆分了出來(lái),Controller可以直接拿著Reformer返回的數(shù)據(jù)進(jìn)行展示夭织。APIManager就屬于Model吭露,reformer就屬于ViewModel。具體關(guān)于reformer的東西我會(huì)放在網(wǎng)絡(luò)層架構(gòu)來(lái)詳細(xì)解釋尊惰。Reformer此時(shí)扮演的ViewModel角色能夠很好地給Controller減負(fù)讲竿,同時(shí),維護(hù)成本也大大降低弄屡,經(jīng)過(guò)reformer產(chǎn)出的永遠(yuǎn)都是MKAnnotation题禀,Controller可以直接拿來(lái)使用。
然后另外一點(diǎn)膀捷,還有一個(gè)業(yè)務(wù)需求是取附近的房源迈嘹,地圖API請(qǐng)求是能夠hold住這個(gè)需求的,那么其他地方都不用變担孔,在fetchDataWithReformer的時(shí)候換一個(gè)reformer就可以了江锨,其他的事情都交給reformer。
那么ReactiveCocoa應(yīng)該扮演什么角色糕篇?
不用ReactiveCocoa也能MVVM啄育,用ReactiveCocoa能更好地體現(xiàn)MVVM的精髓。前面我舉到的例子只是數(shù)據(jù)從API到View的方向拌消,View的操作也會(huì)產(chǎn)生"數(shù)據(jù)"挑豌,只不過(guò)這里的"數(shù)據(jù)"更多的是體現(xiàn)在表達(dá)用戶的操作上安券,比如輸入了什么內(nèi)容,那么數(shù)據(jù)就是text氓英、選擇了哪個(gè)cell侯勉,那么數(shù)據(jù)就是indexPath。那么在數(shù)據(jù)從view走向API或者Controller的方向上铝阐,就是ReactiveCocoa發(fā)揮的地方址貌。
我們知道螟凭,ViewModel本質(zhì)上算是Model層(因?yàn)槭桥諱odel里面分出來(lái)的一部分)政恍,所以View并不適合直接持有ViewModel,那么View一旦產(chǎn)生數(shù)據(jù)了怎么辦邓厕?扔信號(hào)扔給ViewModel,用誰(shuí)扔更扁?ReactiveCocoa。
在MVVM中使用ReactiveCocoa的第一個(gè)目的就是如上所說(shuō)增淹,View并不適合直接持有ViewModel。第二個(gè)目的就在于亚亲,ViewModel有可能并不是只服務(wù)于特定的一個(gè)View门扇,使用更加松散的綁定關(guān)系能夠降低ViewModel和View之間的耦合度留攒。
那么在MVVM中兵怯,Controller扮演什么角色?
大部分國(guó)內(nèi)外資料闡述MVVM的時(shí)候都是這樣排布的:View <-> ViewModel <-> Model腔剂,造成了MVVM不需要Controller的錯(cuò)覺(jué)媒区,現(xiàn)在似乎發(fā)展成業(yè)界開(kāi)始出現(xiàn)MVVM是不需要Controller的。的聲音了掸犬。其實(shí)MVVM是一定需要Controller的參與的袜漩,雖然MVVM在一定程度上弱化了Controller的存在感,并且給Controller做了減負(fù)瘦身(這也是MVVM的主要目的)登渣。但是噪服,這并不代表MVVM中不需要Controller,MMVC和MVVM他們之間的關(guān)系應(yīng)該是這樣:
(來(lái)源:http://www.sprynthesis.com/2014/12/06/reactivecocoa-mvvm-introduction/)
View <-> C <-> ViewModel <-> Model胜茧,所以使用MVVM之后,就不需要Controller的說(shuō)法是不正確的仇味。嚴(yán)格來(lái)說(shuō)MVVM其實(shí)是MVCVM呻顽。從圖中可以得知,Controller夾在View和ViewModel之間做的其中一個(gè)主要事情就是將View和ViewModel進(jìn)行綁定丹墨。在邏輯上廊遍,Controller知道應(yīng)當(dāng)展示哪個(gè)View,Controller也知道應(yīng)當(dāng)使用哪個(gè)ViewModel贩挣,然而View和ViewModel它們之間是互相不知道的喉前,所以Controller就負(fù)責(zé)控制他們的綁定關(guān)系没酣,所以叫Controller/控制器就是這個(gè)原因。
前面扯了那么多卵迂,其實(shí)歸根結(jié)底就是一句話:在MVC的基礎(chǔ)上裕便,把C拆出一個(gè)ViewModel專門負(fù)責(zé)數(shù)據(jù)處理的事情,就是MVVM见咒。然后偿衰,為了讓View和ViewModel之間能夠有比較松散的綁定關(guān)系,于是我們使用ReactiveCocoa改览,因?yàn)樘O(píng)果本身并沒(méi)有提供一個(gè)比較適合這種情況的綁定方法下翎。iOS領(lǐng)域里KVO,Notification宝当,block视事,delegate和target-action都可以用來(lái)做數(shù)據(jù)通信,從而來(lái)實(shí)現(xiàn)綁定庆揩,但都不如ReactiveCocoa提供的RACSignal來(lái)的優(yōu)雅俐东,如果不用ReactiveCocoa,綁定關(guān)系可能就做不到那么松散那么好盾鳞,但并不影響它還是MVVM犬性。
在實(shí)際iOS應(yīng)用架構(gòu)中,MVVM應(yīng)該出現(xiàn)在了大部分創(chuàng)業(yè)公司或者老牌公司新App的iOS應(yīng)用架構(gòu)圖中腾仅,據(jù)我所知易寶支付旗下的某個(gè)iOS應(yīng)用就整體采用了MVVM架構(gòu)乒裆,他們抽出了一個(gè)Action層來(lái)裝各種ViewModel,也是屬于相對(duì)合理的結(jié)構(gòu)推励。
所以Controller在MVVM中鹤耍,一方面負(fù)責(zé)View和ViewModel之間的綁定,另一方面也負(fù)責(zé)常規(guī)的UI邏輯處理验辞。
VIPER
VIPER(View稿黄,Interactor,Presenter跌造,Entity杆怕,Routing)。VIPER我并沒(méi)有實(shí)際使用過(guò)壳贪,我是在objc.io上第13期看到的陵珍。
但凡出現(xiàn)一個(gè)新架構(gòu)或者我之前并不熟悉的新架構(gòu),有一點(diǎn)我能夠非澄ナ肯定互纯,這貨一定又是把MVC的哪個(gè)部分給拆開(kāi)了(壞笑,做這種判斷的理論依據(jù)在第一篇文章里面我已經(jīng)講過(guò)了)磕蒲。事實(shí)情況是VIPER確實(shí)拆了很多很多留潦,除了View沒(méi)拆只盹,其它的都拆了。
我提到的這兩篇文章關(guān)于VIPER都講得很詳細(xì)兔院,一看就懂殖卑。但具體在使用VIPER的時(shí)候會(huì)有什么坑或者會(huì)有哪些爭(zhēng)議我不是很清楚,硬要寫(xiě)這一節(jié)的話我只能靠YY秆乳,所以我想想還是算了懦鼠。如果各位讀者有誰(shuí)在實(shí)際App中采用VIPER架構(gòu)的或者對(duì)VIPER很有興趣的,可以評(píng)論區(qū)里面提出來(lái)屹堰,我們交流一下肛冶。
本門心法
重劍無(wú)鋒,大巧不工扯键。 ---- 《神雕俠侶》
這是楊過(guò)在挑劍時(shí)睦袖,玄鐵重劍旁邊寫(xiě)的一段話。對(duì)此我深表認(rèn)同荣刑。提到這段話的目的是想告訴大家馅笙,在具體做View層架構(gòu)的設(shè)計(jì)時(shí),不需要拘泥于MVC厉亏、MVVM董习、VIPER等規(guī)矩。這些都是招式爱只,告訴你你就知道了皿淋,然后怎么玩都可以。但是心法不是這樣的恬试,心法是大巧窝趣,說(shuō)出來(lái)很簡(jiǎn)單,但是能不能在實(shí)際架構(gòu)設(shè)計(jì)時(shí)牢記心法训柴,并且按照規(guī)矩辦事哑舒,就都看個(gè)人了。
拆分的心法
天下功夫出少林幻馁,天下架構(gòu)出MVC洗鸵。 ---- Casa Taloyum
MVC其實(shí)是非常高Level的抽象,意思也就是仗嗦,在MVC體系下還可以再衍生無(wú)數(shù)的架構(gòu)方式预麸,但萬(wàn)變不離其宗的是,它一定符合MVC的規(guī)范儒将。這句話不是我說(shuō)的,是我在某個(gè)英文資料上看到的对蒲,但時(shí)過(guò)境遷钩蚊,我已經(jīng)找不到出處了贡翘,我很贊同這句話。我采用的架構(gòu)嚴(yán)格來(lái)說(shuō)也是MVC砰逻,但也做了很多的拆分鸣驱。根據(jù)前面幾節(jié)的洗禮,相信各位也明白了這樣的道理:拆分方式的不同誕生了各種不同的衍生架構(gòu)方案(MVCS拆胖Controller蝠咆,MVVM拆胖Model踊东,VIPER什么都拆),但即便拆分方式再怎么多樣刚操,那都只是招式闸翅。而拆分的規(guī)范,就是心法菊霜。這一節(jié)我就講講我在做View架構(gòu)時(shí)坚冀,做拆分的心法。
第一心法:保留最重要的任務(wù)鉴逞,拆分其它不重要的任務(wù)
在iOS開(kāi)發(fā)領(lǐng)域內(nèi)记某,UIViewController承載了非常多的事情,比如View的初始化构捡,業(yè)務(wù)邏輯液南,事件響應(yīng),數(shù)據(jù)加工等等勾徽,當(dāng)然還有更多我現(xiàn)在也列舉不出來(lái)滑凉,但是我們知道有一件事情Controller肯定逃不掉要做:協(xié)調(diào)V和M。也就是說(shuō)捂蕴,不管怎么拆譬涡,協(xié)調(diào)工作是拆不掉的。
那么剩下的事情我們就可以拆了啥辨,比如UITableView的DataSource柔逼。唐巧的博客有一篇文章提到他和另一個(gè)工程師關(guān)于是否要拆分DataSource爭(zhēng)論了好久。拆分DataSource這個(gè)做法應(yīng)該也算是通用做法弓摘,在不復(fù)雜的應(yīng)用里面娜膘,它可能確實(shí)看上去只是一個(gè)數(shù)組而已,但在復(fù)雜的情況下级乍,它背后可能涉及了文件內(nèi)容讀取舌劳,數(shù)據(jù)同步等等復(fù)雜邏輯,這篇文章的第一節(jié)就提倡了這個(gè)做法玫荣,我其實(shí)也蠻提倡的甚淡。
前面的文章里面也提了很多能拆的東西,我就不搬運(yùn)了捅厂,大家可以進(jìn)去看看贯卦。除了這篇文章提到的內(nèi)容以外资柔,任何比較大的,放在ViewController里面比較臟的撵割,只要不是Controller的核心邏輯贿堰,都可以考慮拆出去,然后在架構(gòu)的時(shí)候作為一個(gè)獨(dú)立模塊去定義啡彬,以及設(shè)計(jì)實(shí)現(xiàn)羹与。
第二心法:拆分后的模塊要盡可能提高可復(fù)用性,盡量做到DRY
根據(jù)第一心法拆開(kāi)來(lái)的東西庶灿,很有可能還是強(qiáng)業(yè)務(wù)相關(guān)的纵搁,這種情況有的時(shí)候無(wú)法避免。但我們拆也要拆得好看跳仿,拆出來(lái)的部分最好能夠歸成某一類對(duì)象诡渴,然后最好能夠抽象出一個(gè)通用邏輯出來(lái),使他能夠復(fù)用菲语。即使不能抽出通用邏輯妄辩,那也盡量抽象出一個(gè)protocol,來(lái)實(shí)現(xiàn)IOP山上。這里有篇關(guān)于IOP的文章眼耀,大家看了就明白優(yōu)越性了。
第三心法:要盡可能提高拆分模塊后的抽象度
也就是說(shuō)佩憾,拆分的粒度要盡可能大一點(diǎn)哮伟,封裝得要透明一些。唐巧說(shuō)一切隱藏都是對(duì)代碼復(fù)雜性的增加妄帘,除非它帶來(lái)了好處楞黄,這在一定程度上有點(diǎn)道理,沒(méi)有好處的隱藏確實(shí)都不好(笑)抡驼。提高抽象度事實(shí)上就是增加封裝的力度鬼廓,將一個(gè)負(fù)責(zé)的業(yè)務(wù)抽象成只需要很少的輸入就能完成,就是高度抽象致盟。嗯碎税,繼承很多層,這種做法雖然也提高了抽象程度馏锡,但我不建議這么玩雷蹂。我不確定唐巧在這里說(shuō)的隱藏跟我說(shuō)的封裝是不是同一個(gè)概念,但我在這里想提倡的是盡可能提高抽象程度杯道。
提高抽象程度的好處在于匪煌,對(duì)于業(yè)務(wù)方來(lái)說(shuō),他只需要收集很少的信息(最小充要條件),做很少的調(diào)度(Controller負(fù)責(zé)大模塊調(diào)度虐杯,大模塊里面再去做小模塊的調(diào)度)玛歌,就能夠完成任務(wù),這才是給Controller減負(fù)的正確姿勢(shì)擎椰。
如果拆分出來(lái)的模塊抽象程度不夠,模塊對(duì)外界要求的參數(shù)比較多创肥,那么在Controller里面达舒,關(guān)于收集參數(shù)的代碼就會(huì)多了很多。如果一部分參數(shù)的收集邏輯能夠由模塊來(lái)完成叹侄,那也可以做到幫Controller減輕負(fù)擔(dān)巩搏。否則就感覺(jué)拆得不太干凈,因?yàn)镃ontroller里面還是多了一些不必要的參數(shù)收集邏輯趾代。
如果拆分出來(lái)的粒度太小贯底,Controller在完成任務(wù)的時(shí)候調(diào)度代碼要寫(xiě)很多,那也不太好撒强。導(dǎo)致拆分粒度小的首要因素就是業(yè)務(wù)可能本身就比較復(fù)雜禽捆,拆分粒度小并不是不好,能大就大一點(diǎn)飘哨,如果小了胚想,那也沒(méi)問(wèn)題。針對(duì)這種情況的處理芽隆,就需要采用strategy模式浊服。
針對(duì)拆分粒度小的情況,我來(lái)舉個(gè)實(shí)際例子胚吁,這個(gè)例子來(lái)源于我的一個(gè)朋友他在做聊天應(yīng)用的消息發(fā)送模塊牙躺。當(dāng)消息是文字時(shí),直接發(fā)送腕扶。當(dāng)消息是圖片時(shí)孽拷,需要先向服務(wù)器申請(qǐng)上傳資源,獲得資源ID之后再上傳圖片蕉毯,上傳圖片完成之后拿到圖片URL乓搬,后面帶著URL再把信息發(fā)送出去。
這時(shí)候我們拆模塊代虾,可以拆成:數(shù)據(jù)發(fā)送(叫A模塊)进肯,上傳資源申請(qǐng)(叫B模塊),內(nèi)容上傳(叫C模塊)棉磨。那么要發(fā)送文字消息江掩,Controller調(diào)度A就可以了。如果要發(fā)送圖片消息,Controller調(diào)度B->C->A环形,假設(shè)將來(lái)還有上傳別的類型消息的任務(wù)策泣,他們又要依賴D/E/F模塊,那這個(gè)事情就很蛋疼抬吟,因?yàn)檫壿嫃?fù)雜了萨咕,Controller要調(diào)度的東西要區(qū)分的情況就多了,Controller就膨脹了火本。
那么怎么處理呢危队?可以采用Strategy模式。我們?cè)賮?lái)分析一下钙畔,Controller要完成任務(wù)茫陆,它初始情況下所具有的條件是什么?它有這條消息的所有數(shù)據(jù)擎析,也知道這個(gè)消息的類型簿盅。那么它最終需要的是什么呢?消息發(fā)送的結(jié)果:發(fā)送成功或失敗揍魂。
send msg
Controller ------------------> MessageSender
^? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
|? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
|? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
----------------------------------
success / fail
上面就是我們要實(shí)現(xiàn)的最終結(jié)果桨醋,Controller只要把消息丟給MessageSender,然后讓MessageSender去做事情愉烙,做完了告訴Controller就好了讨盒。那么MessageSender里面怎么去調(diào)度邏輯?MessageSender里面可以有一個(gè)StrategyList步责,里面存放了表達(dá)各種邏輯的Block或者Invocation(Target-Action)返顺。那么我們先定義一個(gè)Enum,里面規(guī)定了每種任務(wù)所需要的調(diào)度邏輯蔓肯。
typedef NS_ENUM (NSUInteger, MessageSendStrategy)
{
MessageSendStrategyText = 0,
MessageSendStrategyImage = 1,
MessageSendStrategyVoice = 2,
MessageSendStrategyVideo = 3
}
然后在MessageSender里面的StrategyList是這樣:
@property(nonatomic,strong)NSArray*strategyList;self.strategyList=@[TextSenderInvocation,ImageSenderInvocation,VoiceSenderInvocation,VideoSenderInvocation];// 然后對(duì)外提供一個(gè)這樣的接口遂鹊,同時(shí)有一個(gè)delegate用來(lái)回調(diào)-(void)sendMessage:(BaseMessage*)messagewithStrategy:(MessageSendStrategy)strategy;@property(nonatomic,weak)iddelegate;@protocolMessageSenderDelegate@required-(void)messageSender:(MessageSender*)messageSenderdidSuccessSendMessage:(BaseMessage*)messagestrategy:(MessageSendStrategy)strategy;-(void)messageSender:(MessageSender*)messageSenderdidFailSendMessage:(BaseMessage*)messagestrategy:(MessageSendStrategy)strategyerror:(NSError*)error;@end
Controller里面是這樣使用的:
[self.messageSendersendMessage:messagewithStrategy:MessageSendStrategyText];
MessageSender里面是這樣的:
[self.strategyList[strategy] invoke];
然后在某個(gè)Invocation里面,就是這樣的:
[Ainvoke];[Binvoke];[Cinvoke];
這樣就好啦蔗包,即便拆分粒度因?yàn)榭陀^原因無(wú)法細(xì)化秉扑,那也能把復(fù)雜的判斷邏輯和調(diào)度邏輯從Controller中抽出來(lái),真正為Controller做到了減負(fù)调限≈勐剑總之能夠做到大粒度就盡量大粒度,實(shí)在做不到那也行耻矮,用Strategy把它hold住秦躯。這個(gè)例子是小粒度的情況,大粒度的情況太簡(jiǎn)單裆装,我就不舉了踱承。
設(shè)計(jì)心法
針對(duì)View層的架構(gòu)不光是看重如何合理地拆分MVC來(lái)給UIViewController減負(fù)倡缠,另外一點(diǎn)也要照顧到業(yè)務(wù)方的使用成本。最好的情況是業(yè)務(wù)方什么都不知道茎活,然后他把代碼放進(jìn)去就能跑昙沦,同時(shí)還能獲得框架提供的種種功能。
比如天安門廣場(chǎng)上的觀眾看臺(tái)载荔,就是我覺(jué)得最好的設(shè)計(jì)盾饮,因?yàn)闆](méi)人會(huì)注意到它。
第一心法:盡可能減少繼承層級(jí)身辨,涉及蘋(píng)果原生對(duì)象的盡量不要繼承
繼承是罪惡丐谋,盡量不要繼承。就我目前了解到的情況看煌珊,除了安居客的Pad App沒(méi)有在框架級(jí)針對(duì)UIViewController有繼承的設(shè)計(jì)以外,其它公司或多或少都針對(duì)UIViewController有繼承泌豆,包括安居客iPhone app(那時(shí)候我已經(jīng)對(duì)此無(wú)能為力定庵,可見(jiàn)View的架構(gòu)在一開(kāi)始就設(shè)計(jì)好有多么重要)。甚至有的還對(duì)UITableView有繼承踪危,這是一件多么令人發(fā)指蔬浙,多么慘絕人寰,多么喪心病狂的事情啊贞远。雖然不可避免的是有些情況我們不得不從蘋(píng)果原生對(duì)象中繼承畴博,比如UITableViewCell。但我還是建議盡量不要通過(guò)繼承的方案來(lái)給原生對(duì)象添加功能蓝仲,前面提到的Aspect方案和Category方案都可以使用俱病。用Aspect+load來(lái)實(shí)現(xiàn)重載函數(shù),用Category來(lái)實(shí)現(xiàn)添加函數(shù)袱结,當(dāng)然亮隙,耍點(diǎn)手段用Category來(lái)添加property也是沒(méi)問(wèn)題的。這些方案已經(jīng)覆蓋了繼承的全部功能垢夹,而且非常好維護(hù)溢吻,對(duì)于業(yè)務(wù)方也更加透明,何樂(lè)而不為呢果元。
不用繼承可能在思路上不會(huì)那么直觀促王,但是對(duì)于不使用繼承帶來(lái)的好處是足夠頂?shù)蒙鲜褂美^承的壞處的。順便在此我要給Category正一下名:業(yè)界對(duì)于Category的態(tài)度比較曖昧而晒,在多種場(chǎng)合(講座蝇狼、資料文檔)都宣揚(yáng)過(guò)盡可能不要使用Category。它們說(shuō)的都有一定道理欣硼,但我認(rèn)為Category是蘋(píng)果提供的最好的使用集合代替繼承的方案题翰,但針對(duì)Category的設(shè)計(jì)對(duì)架構(gòu)師的要求也很高恶阴,請(qǐng)合理使用。而且蘋(píng)果也在很多場(chǎng)合使用Category豹障,來(lái)把一個(gè)原本可能很大的對(duì)象冯事,根據(jù)不同場(chǎng)景拆分成不同的Category,從而提高可維護(hù)性血公。
不使用繼承的好處我在這里已經(jīng)說(shuō)了昵仅,放到iOS應(yīng)用架構(gòu)來(lái)看,還能再多額外兩個(gè)好處:1. 在業(yè)務(wù)方做業(yè)務(wù)開(kāi)發(fā)或者做Demo時(shí)累魔,可以脫離App環(huán)境摔笤,或花更少的時(shí)間搭建環(huán)境。2. 對(duì)業(yè)務(wù)方來(lái)說(shuō)功能更加透明垦写,也符合業(yè)務(wù)方在開(kāi)發(fā)時(shí)的第一直覺(jué)吕世。
第二心法:做好代碼規(guī)范,規(guī)定好代碼在文件中的布局梯投,尤其是ViewController
這主要是為了提高可維護(hù)性命辖。在一個(gè)文件非常大的對(duì)象中,尤其要限制好不同類型的代碼在文件中的布局分蓖。比如在寫(xiě)ViewController時(shí)尔艇,我之前給團(tuán)隊(duì)制定的規(guī)范就是前面一段全部是getter setter,然后接下來(lái)一段是life cycle么鹤,viewDidLoad之類的方法都在這里终娃。然后下面一段是各種要實(shí)現(xiàn)的Delegate,再下面一段就是event response蒸甜,Button的或者GestureRecognizer的都在這里棠耕。然后后面是private method。一般情況下迅皇,如果做好拆分昧辽,ViewController的private method那一段是沒(méi)有方法的。后來(lái)隨著時(shí)間的推移登颓,我發(fā)現(xiàn)開(kāi)頭放getter和setter太影響閱讀了搅荞,所以后面改成全放在ViewController的最后。
第三心法:能不放在Controller做的事情就盡量不要放在Controller里面去做
Controller會(huì)變得龐大的原因框咙,一方面是因?yàn)镃ontroller承載了業(yè)務(wù)邏輯咕痛,MVC的總結(jié)者(在正式提出MVC之前,或多或少都有人這么設(shè)計(jì)喇嘱,所以說(shuō)MVC的設(shè)計(jì)者不太準(zhǔn)確)對(duì)Controller下的定義也是承載業(yè)務(wù)邏輯茉贡,所以Controller就是用來(lái)干這事兒的,天經(jīng)地義者铜。另一方面是因?yàn)樵贛VC中腔丧,關(guān)于Model和View的定義都非常明確放椰,很少有人會(huì)把一個(gè)屬于M或V的東西放到其他地方。然后除了Model和View以外愉粤,還會(huì)剩下很多模棱兩可的東西砾医,這些東西從概念上講都算Controller,而且由于M和V定義得那么明確衣厘,所以直覺(jué)上看如蚜,這些東西放在M或V是不合適的,于是就往Controller里面塞咯影暴。
正是由于上述兩方面原因?qū)е铝薈ontroller的膨脹错邦。我們?cè)偌?xì)細(xì)思考一下,Model膨脹和View膨脹型宙,要針對(duì)它們來(lái)做拆分其實(shí)都是相對(duì)容易的撬呢,Controller膨脹之后,拆分就顯得艱難無(wú)比妆兑。所以如果能夠在一開(kāi)始就盡量把能不放在Controller做的事情放到別的地方去做倾芝,這樣在第一時(shí)間就可以讓你的那部分將來(lái)可能會(huì)被拆分的代碼遠(yuǎn)離業(yè)務(wù)邏輯。所以我們要稍微轉(zhuǎn)變一下思路:模棱兩可的模塊箭跳,就不要塞到Controller去了,塞到V或者塞到M或者其他什么地方都比塞進(jìn)Controller好潭千,便于將來(lái)拆分谱姓。
所以關(guān)于前面我按下不表的關(guān)于胖Model和瘦Model的選擇,我的態(tài)度是更傾向于胖Model刨晴√肜矗客觀地說(shuō),業(yè)務(wù)膨脹之后狈癞,代碼規(guī)那芽浚肯定少不了的,不管你技術(shù)再好蝶桶,經(jīng)驗(yàn)再豐富慨绳,代碼量最多只能優(yōu)化,該膨脹還是要膨脹的真竖,而且優(yōu)化之后代碼往往也比較難看脐雪,使用各種奇技淫巧也是有代價(jià)的。所以恢共,針對(duì)代碼量?jī)?yōu)化的結(jié)果战秋,往往要么就是犧牲可讀性,要么就是犧牲可移植性(通用性)讨韭,Every magic always needs a pay, you have to make a trade-off.脂信。
那么既然膨脹出來(lái)的代碼癣蟋,或者將來(lái)有可能膨脹的代碼,不管放在MVC中的哪一個(gè)部分狰闪,最后都是要拆分的疯搅,既然遲早要拆分,那不如放Model里面尝哆,這樣將來(lái)拆分胖Model也能比拆分胖Cotroller更加容易秉撇。在我還在安居客的時(shí)候,安居客Pad app承載最復(fù)雜業(yè)務(wù)的ViewController才不到600行秋泄,其他多數(shù)Controller都是在300-400行之間琐馆,這就為后面接手的人降低了非常多的上手難度和維護(hù)復(fù)雜度。拆分出來(lái)的東西都是可以直接遷移給iPhone app使用的『阈颍現(xiàn)在看天貓的ViewControler瘦麸,動(dòng)不動(dòng)就幾千行,看不了多久頭就暈了歧胁,問(wèn)了一下滋饲,大家都表示很習(xí)慣這樣的代碼長(zhǎng)度,攤手喊巍。
第四心法:架構(gòu)師是為業(yè)務(wù)工程師服務(wù)的屠缭,而不是去使喚業(yè)務(wù)工程師的
架構(gòu)師在公司里的職級(jí)和地位往往都是要高于業(yè)務(wù)工程師的,架構(gòu)師的技術(shù)實(shí)力和經(jīng)驗(yàn)往往也都是高于業(yè)務(wù)工程師的崭参。所以你值得在公司里獲得較高的地位呵曹,但是在公司里的地位高不代表在軟件工程里面的角色地位也高。架構(gòu)師是要為業(yè)務(wù)工程師服務(wù)的何暮,是他們使喚你而不是你使喚他們奄喂。另外,制定規(guī)范一方面是起到約束業(yè)務(wù)工程師的代碼海洼,但更重要的一點(diǎn)是跨新,這其實(shí)是利用你的能力幫助業(yè)務(wù)工程師避免他無(wú)法預(yù)見(jiàn)的危機(jī),所以地位高有一定的好處坏逢,畢竟夏蟲(chóng)不可語(yǔ)冰域帐,有的時(shí)候不見(jiàn)得能夠解釋得通,因此高地位隨之而來(lái)的就是說(shuō)服力會(huì)比較強(qiáng)词疼。但在軟件工程里俯树,一定要保持謙卑,一定要多為業(yè)務(wù)工程師考慮贰盗。
一個(gè)不懂這個(gè)道理的架構(gòu)師许饿,設(shè)計(jì)出來(lái)的東西往往復(fù)雜難用,因?yàn)樗辉敢庾龊诵牡臇|西舵盈,周邊不愿意做的都期望交給業(yè)務(wù)工程師去做陋率,甚至有的時(shí)候就只做了個(gè)Demo球化,然后就交給業(yè)務(wù)工程師了,業(yè)務(wù)工程師變成給他打工的了瓦糟。但是一個(gè)懂得這個(gè)道理的架構(gòu)師筒愚,設(shè)計(jì)出來(lái)的東西會(huì)非常好用,業(yè)務(wù)方只需要扔很少的參數(shù)然后拿結(jié)果就好了菩浙,這樣的架構(gòu)才叫好的架構(gòu)巢掺。
舉一個(gè)保存圖片到本地的例子,一種做法是提供這樣的接口:- (NSString *)saveImageWithData:(NSData *)imageData劲蜻,另一種是- (NSString *)saveImage:(UIImage *)image陆淀。后者更好,原因自己想先嬉。
你的態(tài)度越謙卑轧苫,就越能設(shè)計(jì)出好的架構(gòu),這是我設(shè)計(jì)心法里的最后一條疫蔓,也是最重要的一條含懊。即使你現(xiàn)在技術(shù)實(shí)力不是業(yè)界大牛級(jí)別的,但只要保持這個(gè)心態(tài)去做架構(gòu)衅胀,去做設(shè)計(jì)岔乔,就已經(jīng)是合格的架構(gòu)師了,要成為業(yè)界大牛也會(huì)非彻銮快重罪。
小總結(jié)
其實(shí)針對(duì)View層的架構(gòu)設(shè)計(jì),還是要做好三點(diǎn):代碼規(guī)范哀九,架構(gòu)模式,工具集搅幅。
代碼規(guī)范對(duì)于View層來(lái)說(shuō)意義重大阅束,畢竟View層非常重業(yè)務(wù),如果代碼布局混亂茄唐,后來(lái)者很難接手息裸,也很難維護(hù)。
架構(gòu)模式具體如何選擇沪编,完全取決于業(yè)務(wù)復(fù)雜度呼盆。如果業(yè)務(wù)相當(dāng)相當(dāng)復(fù)雜,那就可以使用VIPER蚁廓,如果相對(duì)簡(jiǎn)單访圃,那就直接MVC稍微改改就好了。每一種已經(jīng)成為定式的架構(gòu)模式不見(jiàn)得都適合各自公司對(duì)應(yīng)的業(yè)務(wù)相嵌,所以需要各位架構(gòu)師根據(jù)情況去做一些拆分或者改變腿时。拆分一般都不會(huì)出現(xiàn)問(wèn)題况脆,改變的時(shí)候,只要?jiǎng)e把MVC三個(gè)角色搞混就好了批糟,M該做啥做啥格了,C該做啥做啥,V該做啥做啥徽鼎,不要亂來(lái)盛末。關(guān)于大部分的架構(gòu)模式應(yīng)該是什么樣子,這篇文章里都已經(jīng)說(shuō)過(guò)了否淤,不過(guò)我認(rèn)為最重要的還是后面的心法悄但,模式只是招術(shù),熟悉了心法才能大巧不工叹括。
View層的工具集主要還是集中在如何對(duì)View進(jìn)行布局算墨,以及一些特定的View,比如帶搜索提示的搜索框這種汁雷。這篇文章只提到了View布局的工具集净嘀,其它的工具集相對(duì)而言是更加取決于各自公司的業(yè)務(wù)的,各自實(shí)現(xiàn)或者使用CocoaPods里現(xiàn)成的都不是很難侠讯。
對(duì)于小規(guī)耐诓兀或者中等規(guī)模iOS開(kāi)發(fā)團(tuán)隊(duì)來(lái)說(shuō)膜眠,做好以上三點(diǎn)就足夠了溜嗜。在大規(guī)模團(tuán)隊(duì)中,有一個(gè)額外問(wèn)題要考慮,就是跨業(yè)務(wù)頁(yè)面調(diào)用方案的設(shè)計(jì)麸塞。
跨業(yè)務(wù)頁(yè)面調(diào)用方案的設(shè)計(jì)
跨業(yè)務(wù)頁(yè)面調(diào)用是指序攘,當(dāng)一個(gè)App中存在A業(yè)務(wù)伪货,B業(yè)務(wù)等多個(gè)業(yè)務(wù)時(shí)舶斧,B業(yè)務(wù)有可能會(huì)需要展示A業(yè)務(wù)的某個(gè)頁(yè)面怀酷,A業(yè)務(wù)也有可能會(huì)調(diào)用其他業(yè)務(wù)的某個(gè)頁(yè)面。在小規(guī)模的App中嗜闻,我們直接import其他業(yè)務(wù)的某個(gè)ViewController然后或者push或者present蜕依,是不會(huì)產(chǎn)生特別大的問(wèn)題的。但是如果App的規(guī)模非常大琉雳,涉及業(yè)務(wù)數(shù)量非常多样眠,再這么直接import就會(huì)出現(xiàn)問(wèn)題。
--------------? ? ? ? ? ? --------------? ? ? ? ? ? --------------
|? ? ? ? ? ? |? page call? |? ? ? ? ? ? |? page call? |? ? ? ? ? ? |
| Buisness A | <---------> | Buisness B | <---------> | Buisness C |
|? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |
--------------? ? ? ? ? ? --------------? ? ? ? ? ? --------------
\? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? /
\? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? /
\? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? /
\? ? ? ? ? ? ? ? |? ? ? ? ? ? ? /
\? ? ? ? ? ? ? |? ? ? ? ? ? ? /
--------------------------------
|? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
|? ? ? ? ? ? ? App? ? ? ? ? ? |
|? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
--------------------------------
可以看出咐吼,跨業(yè)務(wù)的頁(yè)面調(diào)用在多業(yè)務(wù)組成的App中會(huì)導(dǎo)致橫向依賴吹缔。那么像這樣的橫向依賴,如果不去設(shè)法解決锯茄,會(huì)導(dǎo)致什么樣的結(jié)果厢塘?
當(dāng)一個(gè)需求需要多業(yè)務(wù)合作開(kāi)發(fā)時(shí),如果直接依賴肌幽,會(huì)導(dǎo)致某些依賴層上端的業(yè)務(wù)工程師在前期空轉(zhuǎn)晚碾,依賴層下端的工程師任務(wù)繁重,而整個(gè)需求完成的速度會(huì)變慢喂急,影響的是團(tuán)隊(duì)開(kāi)發(fā)迭代速度肚豺。
當(dāng)要開(kāi)辟一個(gè)新業(yè)務(wù)時(shí)实辑,如果已有各業(yè)務(wù)間直接依賴,新業(yè)務(wù)又依賴某個(gè)舊業(yè)務(wù),就導(dǎo)致新業(yè)務(wù)的開(kāi)發(fā)環(huán)境搭建困難走哺,因?yàn)楸仨氁阉邢嚓P(guān)業(yè)務(wù)都塞入開(kāi)發(fā)環(huán)境,新業(yè)務(wù)才能進(jìn)行開(kāi)發(fā)撩匕。影響的是新業(yè)務(wù)的響應(yīng)速度蜓堕。
當(dāng)某一個(gè)被其他業(yè)務(wù)依賴的頁(yè)面有所修改時(shí),比如改名苗膝,涉及到的修改面就會(huì)特別大殃恒。影響的是造成任務(wù)量和維護(hù)成本都上升的結(jié)果。
當(dāng)然,如果App規(guī)模特別小离唐,這三點(diǎn)帶來(lái)的影響也會(huì)特別小病附,但是在阿里這樣大規(guī)模的團(tuán)隊(duì)中,像天貓/淘寶這樣大規(guī)模的App亥鬓,一旦遇上這里面哪怕其中一件事情完沪,就特么很坑爹。
那么應(yīng)該怎樣處理這個(gè)問(wèn)題贮竟?
讓依賴關(guān)系下沉丽焊。
怎么讓依賴關(guān)系下沉?引入Mediator模式咕别。
所謂引入Mediator模式來(lái)讓依賴關(guān)系下沉技健,實(shí)質(zhì)上就是每次呼喚頁(yè)面的時(shí)候,通過(guò)一個(gè)中間人來(lái)召喚另外一個(gè)頁(yè)面惰拱,這樣只要每個(gè)業(yè)務(wù)依賴這個(gè)中間人就可以了雌贱,中間人的角色就可以放在業(yè)務(wù)層的下面一層,這就是依賴關(guān)系下沉偿短。
--------------? ? ? ? ? ? --------------? ? ? ? ? ? --------------
|? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |
| Buisness A |? ? ? ? ? ? | Buisness B |? ? ? ? ? ? | Buisness C |
|? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |
--------------? ? ? ? ? ? --------------? ? ? ? ? ? --------------
\? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? /
\? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? /
\? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? /? 通過(guò)Mediater來(lái)召喚頁(yè)面
\? ? ? ? ? ? ? ? |? ? ? ? ? ? ? /
\? ? ? ? ? ? ? |? ? ? ? ? ? ? /
--------------------------------
|? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
|? ? ? ? ? ? Mediater? ? ? ? ? |
|? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
--------------------------------
|
|
|
|
|
--------------------------------
|? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
|? ? ? ? ? ? ? App? ? ? ? ? ? |
|? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
--------------------------------
當(dāng)A業(yè)務(wù)需要調(diào)用B業(yè)務(wù)的某個(gè)頁(yè)面的時(shí)候欣孤,將請(qǐng)求交給Mediater,然后由Mediater通過(guò)某種手段獲取到B業(yè)務(wù)頁(yè)面的實(shí)例昔逗,交還給A就行了降传。在具體實(shí)現(xiàn)這個(gè)機(jī)制的過(guò)程中,有以下幾個(gè)問(wèn)題需要解決:
設(shè)計(jì)一套通用的請(qǐng)求機(jī)制勾怒,請(qǐng)求機(jī)制需要跟業(yè)務(wù)剝離婆排,使得不同業(yè)務(wù)的頁(yè)面請(qǐng)求都能夠被Mediater處理
設(shè)計(jì)Mediater根據(jù)請(qǐng)求如何獲取其他業(yè)務(wù)的機(jī)制,Mediater需要知道如何處理請(qǐng)求笔链,上哪兒去找到需要的頁(yè)面
這個(gè)看起來(lái)就非常像我們web開(kāi)發(fā)時(shí)候的URL機(jī)制段只,發(fā)送一個(gè)Get或Post請(qǐng)求,CGI調(diào)用腳本把請(qǐng)求分發(fā)給某個(gè)Controller下的某個(gè)Action鉴扫,然后返回HTML字符串到瀏覽器去解析赞枕。蘋(píng)果本身也實(shí)現(xiàn)了一套跨App調(diào)用機(jī)制,它也是基于URL機(jī)制來(lái)運(yùn)轉(zhuǎn)的坪创,只不過(guò)它想要解決的問(wèn)題是跨App的數(shù)據(jù)交流和頁(yè)面調(diào)用炕婶,我們想要解決的問(wèn)題是降低各業(yè)務(wù)的耦合度。
不過(guò)我們還不能直接使用蘋(píng)果原生的這套機(jī)制莱预,因?yàn)檫@套機(jī)制不能夠返回對(duì)象實(shí)例柠掂。而我們希望能夠拿到對(duì)象實(shí)例,這樣不光可以做跨業(yè)務(wù)頁(yè)面調(diào)用锁施,也可以做跨業(yè)務(wù)的功能調(diào)用。另外,我們又希望我們的Mediater也能夠跟蘋(píng)果原生的跨App調(diào)用兼容悉抵,這樣就又能幫業(yè)務(wù)方省掉一部分開(kāi)發(fā)量肩狂。
就我目前所知道的情況,AutoCad旗下某款iOS應(yīng)用(時(shí)間有點(diǎn)久我不記得是哪款應(yīng)用了姥饰,如果你是AutoCad的iOS開(kāi)發(fā)傻谁,可以在評(píng)論區(qū)補(bǔ)充一下。)就采用了這種頁(yè)面調(diào)用方式列粪。天貓里面目前也在使用這套機(jī)制审磁,只是這一塊由于歷史原因存在新老版本混用的情況,因此暫時(shí)還沒(méi)能夠很好地發(fā)揮應(yīng)有的作用岂座。
嗯态蒂,想問(wèn)我要Demo的同學(xué),我可以很大方地告訴你费什,沒(méi)有钾恢。不過(guò)我打算抽時(shí)間寫(xiě)一個(gè)出來(lái),現(xiàn)在除了已經(jīng)想好名字叫Summon以外鸳址,其它什么都沒(méi)做瘩蚪,哈哈。
關(guān)于Getter和Setter稿黍?
我比較習(xí)慣一個(gè)對(duì)象的"私有"屬性寫(xiě)在extension里面疹瘦,然后這些屬性的初始化全部放在getter里面做,在init和dealloc之外巡球,是不會(huì)出現(xiàn)任何類似_property這樣的寫(xiě)法的言沐。就是這樣:
@interfaceCustomObject()@property(nonatomic,strong)UILabel*label;@end@implement#pragma mark - life cycle-(void)viewDidLoad{[superviewDidLoad];[self.viewaddSubview:self.label];}-(void)viewWillAppear:(BOOL)animated{[superviewWillAppear:animated];self.label.frame=CGRectMake(1,2,3,4);}#pragma mark - getters and setters-(UILabel*)label{if(_label==nil){_label=[[UILabelalloc]init];_label.text=@"1234";_label.font=[UIFontsystemFontOfSize:12];......}return_label;}@end
唐巧說(shuō)他喜歡的做法是用_property這種,然后關(guān)于_property的初始化通過(guò)[self setupProperty]這種做法去做辕漂。從剛才上面的代碼來(lái)看呢灶,就是要在viewDidLoad里面多調(diào)用一個(gè)setup方法而已,然后我推薦的方法就是不用多調(diào)一個(gè)setup方法钉嘹,直接走getter鸯乃。
嗯,怎么說(shuō)呢跋涣,其實(shí)兩種做法都能完成需求缨睡。但是從另一個(gè)角度看,蘋(píng)果之所以選擇讓[self getProperty]和self.property可以互相通用陈辱,這種做法已經(jīng)很明顯地表達(dá)了蘋(píng)果的傾向:希望每個(gè)property都是通過(guò)getter方法來(lái)獲得奖年。
早在2003年,Allen Holub就發(fā)了篇文章《Why getter and setter methods are evil》沛贪,自此之后陋守,業(yè)界就對(duì)此產(chǎn)生了各種爭(zhēng)議震贵,雖然是從Java開(kāi)始說(shuō)的,但是發(fā)展到后面各種語(yǔ)言也參與了進(jìn)來(lái)水评。然后雖然現(xiàn)在關(guān)于這個(gè)問(wèn)題討論得少了猩系,但是依舊屬于沒(méi)有定論的狀態(tài)。setter的情況比較復(fù)雜中燥,也不是我這一節(jié)的重點(diǎn)寇甸,我這邊還是主要說(shuō)getter。我們從objc的設(shè)計(jì)來(lái)看疗涉,蘋(píng)果的設(shè)計(jì)者更加傾向于getter is not evil拿霉。
認(rèn)為getter is evil的原因有非常之多,或大或小咱扣,隨著爭(zhēng)論的進(jìn)行绽淘,大家慢慢就聚焦到這樣的一個(gè)原因:Getter和Setter提供了一個(gè)能讓外部修改對(duì)象內(nèi)部數(shù)據(jù)的方式,這是evil的偏窝,正常情況下收恢,一個(gè)對(duì)象自己私有的變量應(yīng)該是只有自己關(guān)心。
然后我們回到iOS領(lǐng)域來(lái)祭往,objc也同樣面臨了這樣的問(wèn)題伦意,甚至更加嚴(yán)重:objc并沒(méi)有像Java那么嚴(yán)格的私有概念。但在實(shí)際工作中硼补,我們不太會(huì)去操作頭文件里面沒(méi)有的變量驮肉,這是從規(guī)范上就被禁止的。
認(rèn)為getter is not evil的原因也可以聚焦到一個(gè):高度的封裝性已骇。getter事實(shí)上是工廠方法离钝,有了getter之后,業(yè)務(wù)邏輯可以更加專注于調(diào)用褪储,而不必?fù)?dān)心當(dāng)前變量是否可用卵渴。我們可以想一下,假設(shè)一個(gè)ViewController有20個(gè)subview要加入view中鲤竹,這20個(gè)subview的初始化代碼是肯定逃不掉的浪读,放在哪里比較好?放在哪里都比放在addsubview的地方好辛藻,我個(gè)人認(rèn)為最好的地方還是放在getter里面碘橘,結(jié)合單例模式之后,代碼會(huì)非常整齊吱肌,生產(chǎn)的地方和使用的地方得到了很好的區(qū)分痘拆。
所以放到iOS來(lái)說(shuō),我還是覺(jué)得使用getter會(huì)比較好氮墨,因?yàn)閑vil的地方在iOS這邊基本都避免了纺蛆,not evil的地方都能享受到吐葵,還是不錯(cuò)的。
總結(jié)
要做一個(gè)View層架構(gòu)桥氏,主要就是從以下三方面入手:
制定良好的規(guī)范
選擇好合適的模式(MVC折联、MVCS、MVVM识颊、VIPER)
根據(jù)業(yè)務(wù)情況針對(duì)ViewController做好拆分,提供一些小工具方便開(kāi)發(fā)
當(dāng)然奕坟,你還會(huì)遇到其他的很多問(wèn)題祥款,這時(shí)候你可以參考這篇文章里提出的心法,在后面提到的跨業(yè)務(wù)頁(yè)面調(diào)用方案的設(shè)計(jì)中月杉,你也能夠看到我的一些心法的影子刃跛。
對(duì)于iOS客戶端來(lái)說(shuō),它并不像其他語(yǔ)言諸如Python苛萎、PHP他們有那么多的非官方通用框架桨昙。客觀原因在于腌歉,蘋(píng)果已經(jīng)為我們做了非常多的事情蛙酪,做了很多的努力。在蘋(píng)果已經(jīng)做了這么多事情的基礎(chǔ)上翘盖,架構(gòu)師要做針對(duì)View層的方案時(shí)桂塞,最好還是盡量遵守蘋(píng)果已有的規(guī)范和設(shè)計(jì)思想,然后根據(jù)自己過(guò)去開(kāi)發(fā)iOS時(shí)的經(jīng)驗(yàn)馍驯,盡可能給業(yè)務(wù)方在開(kāi)發(fā)業(yè)務(wù)時(shí)減負(fù)阁危,提高業(yè)務(wù)代碼的可維護(hù)性,就是View層架構(gòu)方案的最大目標(biāo)汰瘫。
2015-04-28 09:28補(bǔ):關(guān)于AOP
AOP(Aspect Oriented Programming)狂打,面向切片編程,這也是面向XX編程系列術(shù)語(yǔ)之一哈混弥,但它跟我們熟知的面向?qū)ο缶幊虥](méi)什么關(guān)系趴乡。
什么是切片?
程序要完成一件事情剑逃,一定會(huì)有一些步驟浙宜,1,2蛹磺,3粟瞬,4這樣。這里分解出來(lái)的每一個(gè)步驟我們可以認(rèn)為是一個(gè)切片萤捆。
什么是面向切片編程裙品?
你針對(duì)每一個(gè)切片的間隙俗批,塞一些代碼進(jìn)去,在程序正常進(jìn)行1市怎,2岁忘,3,4步的間隙可以跑到你塞進(jìn)去的代碼区匠,那么你寫(xiě)這些代碼就是面向切片編程干像。
為什么會(huì)出現(xiàn)面向切片編程?
你要想做到在每一個(gè)步驟中間做你自己的事情驰弄,不用AOP也一樣可以達(dá)到目的麻汰,直接往步驟之間塞代碼就好了。但是事實(shí)情況往往很復(fù)雜戚篙,直接把代碼塞進(jìn)去五鲫,主要問(wèn)題就在于:塞進(jìn)去的代碼很有可能是跟原業(yè)務(wù)無(wú)關(guān)的代碼,在同一份代碼文件里面摻雜多種業(yè)務(wù)岔擂,這會(huì)帶來(lái)業(yè)務(wù)間耦合位喂。為了降低這種耦合度,我們引入了AOP乱灵。
如何實(shí)現(xiàn)AOP塑崖?
AOP一般都是需要有一個(gè)攔截器,然后在每一個(gè)切片運(yùn)行之前和運(yùn)行之后(或者任何你希望的地方)痛倚,通過(guò)調(diào)用攔截器的方法來(lái)把這個(gè)jointpoint扔到外面弃舒,在外面獲得這個(gè)jointpoint的時(shí)候,執(zhí)行相應(yīng)的代碼状原。
在iOS開(kāi)發(fā)領(lǐng)域聋呢,objective-C的runtime有提供了一系列的方法,能夠讓我們攔截到某個(gè)方法的調(diào)用颠区,來(lái)實(shí)現(xiàn)攔截器的功能削锰,這種手段我們稱為Method Swizzling。Aspects通過(guò)這個(gè)手段實(shí)現(xiàn)了針對(duì)某個(gè)類和某個(gè)實(shí)例中方法的攔截毕莱。
另外器贩,也可以使用protocol的方式來(lái)實(shí)現(xiàn)攔截器的功能,具體實(shí)現(xiàn)方案就是這樣:
@protocolRTAPIManagerInterceptor@optional-(void)manager:(RTAPIBaseManager*)managerbeforePerformSuccessWithResponse:(AIFURLResponse*)response;-(void)manager:(RTAPIBaseManager*)managerafterPerformSuccessWithResponse:(AIFURLResponse*)response;-(void)manager:(RTAPIBaseManager*)managerbeforePerformFailWithResponse:(AIFURLResponse*)response;-(void)manager:(RTAPIBaseManager*)managerafterPerformFailWithResponse:(AIFURLResponse*)response;-(BOOL)manager:(RTAPIBaseManager*)managershouldCallAPIWithParams:(NSDictionary*)params;-(void)manager:(RTAPIBaseManager*)managerafterCallingAPIWithParams:(NSDictionary*)params;@end@interfaceRTAPIBaseManager:NSObject@property(nonatomic,weak)idinterceptor;@end
這么做對(duì)比Method Swizzling有個(gè)額外好處就是朋截,你可以通過(guò)攔截器來(lái)給攔截器的實(shí)現(xiàn)者提供更多的信息蛹稍,便于外部實(shí)現(xiàn)更加了解當(dāng)前切片的情況。另外部服,你還可以更精細(xì)地對(duì)切片進(jìn)行劃分唆姐。Method Swizzling的切片粒度是函數(shù)粒度的,自己實(shí)現(xiàn)的攔截器的切片粒度可以比函數(shù)更小廓八,更加精細(xì)奉芦。
缺點(diǎn)就是赵抢,你得自己在每一個(gè)插入點(diǎn)把調(diào)用攔截器方法的代碼寫(xiě)上(笑),通過(guò)Aspects(本質(zhì)上就是Mehtod Swizzling)來(lái)實(shí)現(xiàn)的AOP声功,就能輕松一些烦却。
2015-4-29 14:25 補(bǔ):關(guān)于在哪兒寫(xiě)Constraints?
文章發(fā)出來(lái)之后先巴,很多人針對(duì)勘誤1有很多看法其爵,以至于我覺(jué)得很有必要在這里做一份補(bǔ)。期間過(guò)程很多很復(fù)雜伸蚯,這篇文章也已經(jīng)很長(zhǎng)了醋闭,我就直接說(shuō)結(jié)果了哈。
蘋(píng)果在文檔中指出朝卒,updateViewConstraints是用來(lái)做add constraints的地方。
但是在這里有一個(gè)回答者說(shuō)updateViewConstraints并不適合做添加Constraints的事情乐埠。
綜合我自己和評(píng)論區(qū)各位關(guān)心這個(gè)問(wèn)題的兄弟們的各種測(cè)試和各種文檔抗斤,我現(xiàn)在覺(jué)得還是在viewDidLoad里面開(kāi)一個(gè)layoutPageSubviews的方法,然后在這個(gè)里面創(chuàng)建Constraints并添加丈咐,會(huì)比較好瑞眼。就是像下面這樣:
-(void)viewDidLoad{[superviewDidLoad];[self.viewaddSubview:self.firstView];[self.viewaddSubview:self.secondView];[self.viewaddSubview:self.thirdView];[selflayoutPageSubviews];}-(void)layoutPageSubviews{[self.viewaddConstraints:xxxConstraints];[self.viewaddConstraints:yyyConstraints];[self.viewaddConstraints:zzzConstraints];}
最后,要感謝評(píng)論區(qū)各位關(guān)心這個(gè)問(wèn)題棵逊,并提出自己意見(jiàn)伤疙,甚至是自己親自測(cè)試然后告訴我結(jié)果的各位兄弟:@fly2never,@Wythe辆影,@wtlucky徒像,@lcddhr,@李新星蛙讥,@Meigan Fang锯蛀,@匿名,@Xiao Moch次慢。
這個(gè)做法是目前我自己覺(jué)得可能比較合適的做法旁涤,當(dāng)然也歡迎其他同學(xué)繼續(xù)拿出自己的看法,我們來(lái)討論迫像。
勘誤
我的前同事@ddaajing看了這篇文章之后劈愚,給我提出了以下兩個(gè)勘誤,和很多行文上的問(wèn)題闻妓。在這里我對(duì)他表示非常感謝:
勘誤1:其實(shí)在viewWillAppear這里改變UI元素不是很可靠菌羽,Autolayout發(fā)生在viewWillAppear之后,嚴(yán)格來(lái)說(shuō)這里通常不做視圖位置的修改由缆,而用來(lái)更新Form數(shù)據(jù)算凿。改變位置可以放在viewWilllayoutSubview或者didLayoutSubview里份蝴,而且在viewDidLayoutSubview確定UI位置關(guān)系之后設(shè)置autoLayout比較穩(wěn)妥。另外氓轰,viewWillAppear在每次頁(yè)面即將顯示都會(huì)調(diào)用婚夫,viewWillLayoutSubviews雖然在lifeCycle里調(diào)用順序在viewWillAppear之后,但是只有在頁(yè)面元素需要調(diào)整時(shí)才會(huì)調(diào)用署鸡,避免了Constraints的重復(fù)添加案糙。
勘誤2:MVVM要有ViewModel,以及ReactiveCocoa帶來(lái)的信號(hào)通知效果靴庆,在ReactiveCocoa里就是RAC等相關(guān)宏來(lái)實(shí)現(xiàn)时捌。另外,使用ReactiveCocoa能夠比較優(yōu)雅地實(shí)現(xiàn)MVVM模式炉抒,就是因?yàn)橛蠷AC等相關(guān)宏的存在奢讨。就像它的名字一樣Reactive-響應(yīng)式,這也是區(qū)分MVVM的VM和MVC的C和MVP的P的一個(gè)重要方面焰薄。
有任何問(wèn)題建議直接在評(píng)論區(qū)提問(wèn)拿诸,這樣后來(lái)的人如果有相同的問(wèn)題,就能直接找到答案了塞茅。提問(wèn)之前也可以先看看評(píng)論區(qū)有沒(méi)有人問(wèn)過(guò)類似問(wèn)題了亩码。
所有評(píng)論和問(wèn)題我都會(huì)在第一時(shí)間回復(fù),QQ上我是不回答問(wèn)題的哈野瘦。
評(píng)論系統(tǒng)我用的是Disqus描沟,不定期被墻。所以如果你看到文章下面沒(méi)有加載出評(píng)論列表鞭光,翻個(gè)墻就有了吏廉。