轉(zhuǎn)自http://casatwy.com/iosying-yong-jia-gou-tan-viewceng-de-zu-zhi-he-diao-yong-fang-an.html
前言《iOS應(yīng)用架構(gòu)談 開篇》出來之后八回,很多人來催我趕緊出第二篇。這一篇文章出得相當(dāng)艱難滴铅,因為公司里的破事兒特別多,我自己又有點私事兒噩翠,以至于能用來寫博客的時間不夠充分∩嗣現(xiàn)在好啦猛们,第二篇出來了弯淘。當(dāng)我們開始設(shè)計View層的架構(gòu)時,往往是這個App還沒有開始開發(fā)态鳖,或者這個App已經(jīng)發(fā)過幾個版本了浆竭,然后此時需要做非常徹底的重構(gòu)兆蕉。一般也就是這兩種時機(jī)會去做View層架構(gòu),基于這個時機(jī)的特殊性缸废,我們在這時候必須清楚認(rèn)識到:View層的架構(gòu)一旦實現(xiàn)或定型,在App發(fā)版后可修改的余地就已經(jīng)非常之小了届巩。因為它跟業(yè)務(wù)關(guān)聯(lián)最為緊密,所以哪怕稍微動一點點瘾英,它所引發(fā)的蝴蝶效應(yīng)都不見得是業(yè)務(wù)方能夠hold住的。這樣的情況湿蛔,就要求我們在實現(xiàn)這個架構(gòu)時添谊,代碼必須得改得勤快,不能偷懶喊废。也必須抱著充分的自我懷疑態(tài)度,做決策時要拿捏好尺度瓣蛀。View層的架構(gòu)非常之重要,在我看來诈皿,這部分架構(gòu)是這系列文章涉及4個方面最重要的一部分,沒有之一截歉。為什么這么說?View層架構(gòu)是影響業(yè)務(wù)方迭代周期的因素之一產(chǎn)品經(jīng)理產(chǎn)生需求的速度會非沉构洌快,尤其是公司此時仍處于創(chuàng)業(yè)初期酵使,在規(guī)模稍大的公司里面口渔,產(chǎn)品經(jīng)理也喜歡挖大坑來在leader面前刷存在感,比如阿里。這就導(dǎo)致業(yè)務(wù)工程師任務(wù)非常繁重礁扮。正常情況下讓產(chǎn)品經(jīng)理砍需求是不太可能的,因此作為架構(gòu)師,在架構(gòu)里有一些可做可不做的事情叠赐,最好還是能做就做掉赛不,不要偷懶文黎。這可以幫業(yè)務(wù)方減負(fù),編寫代碼的時候也能更加關(guān)注業(yè)務(wù)劳闹。我跟一些朋友交流的時候业汰,他們都會或多或少地抱怨自己的團(tuán)隊迭代速度不夠快,或者說放祟,迭代速度不合理地慢。我認(rèn)為迭代速度不是想提就能提的,迭代速度的影響因素有很多执桌,一期PRD里的任務(wù)量和任務(wù)復(fù)雜度都會影響迭代周期能達(dá)到什么樣的程度。拋開這些外在的不談,從內(nèi)在可能導(dǎo)致迭代周期達(dá)不到合理的速度的原因來看颓芭,其中有一個原因很有可能就是View層架構(gòu)沒有做好,讓業(yè)務(wù)工程師完成一個不算復(fù)雜的需求時,需要處理太多額外的事情床玻。當(dāng)然贫堰,開會多,工程師水平爛也屬于迭代速度提不上去的內(nèi)部原因,但這個不屬于本文討論范圍睦优。還有询一,加班不是優(yōu)化迭代周期的正確方式菱阵,嗯。一般來說虑稼,一個不夠好的View層架構(gòu),主要原因有以下五種:代碼混亂不規(guī)范過多繼承導(dǎo)致的復(fù)雜依賴關(guān)系模塊化程度不夠高,組件粒度不夠細(xì)橫向依賴架構(gòu)設(shè)計失去傳承這五個地方會影響業(yè)務(wù)工程師實現(xiàn)需求的效率,進(jìn)而拖慢迭代周期。View架構(gòu)的其他缺陷也會或多或少地產(chǎn)生影響,但在我看來這里五個是比較重要的影響因素直砂。如果大家覺得還有什么因素比這四個更高的济丘,可以在評論區(qū)提出來我補(bǔ)上去。對于第五點我想做一下強(qiáng)調(diào):架構(gòu)的設(shè)計是一定需要有傳承的,有傳承的架構(gòu)從整體上看會非常協(xié)調(diào)。但實際情況有可能是一個人走了地来,另一個頂上,即便任務(wù)交接得再完整,都不可避免不同的人有不同的架構(gòu)思路,從而導(dǎo)致整個架構(gòu)的流暢程度受到影響诉瓦。要解決這個問題,一方面要盡量避免單點問題,讓架構(gòu)師做架構(gòu)的時候再帶一個人啄糙。另一方面,架構(gòu)要設(shè)計得盡量簡單燕雁,平緩接手人的學(xué)習(xí)曲線崩泡。我離開安居客的時候呛伴,做過保證:凡是從我手里出來的代碼,終身保修。所以不要想著離職了就什么事兒都不管了奕锌,這不光是職業(yè)素養(yǎng)問題趁桃,還有一個是你對你的代碼是否足夠自信的問題油啤。傳承性對于View層架構(gòu)非常重要,因為它距離業(yè)務(wù)最近,改動余地最小。所以當(dāng)各位CTO、技術(shù)總監(jiān)芋绸、TeamLeader們覺得迭代周期不夠快時全封,你可以先不忙著急吼吼地去招新人行楞,《人月神話》早就說過加人不能完全解決問題。這時候如果你可以回過頭來看一下是不是View層架構(gòu)不合理,把這個弄好也是優(yōu)化迭代周期的手段之一。嗯,至于本系列其他三項的架構(gòu)方案對于迭代周期的影響程度,我認(rèn)為都不如View層架構(gòu)方案對迭代周期的影響高魄宏,所以這是我認(rèn)為View層架構(gòu)是最重要的其中一個理由。View層架構(gòu)是最貼近業(yè)務(wù)的底層架構(gòu)View層架構(gòu)雖然也算底層搏色,但還沒那么底層烁焙,它跟業(yè)務(wù)的對接面最廣膳殷,影響業(yè)務(wù)層代碼的程度也最深秽之。在所有的底層都牽一發(fā)的時候,在View架構(gòu)上牽一發(fā)導(dǎo)致業(yè)務(wù)層動全身的面積最大吃既。所以View架構(gòu)在所有架構(gòu)中一旦定型考榨,可修改的空間就最小鹦倚,我們在一開始考慮View相關(guān)架構(gòu)時河质,不光要實現(xiàn)功能,還要考慮更多規(guī)范上的東西震叙。制定規(guī)范的目的一方面是防止業(yè)務(wù)工程師的代碼腐蝕View架構(gòu)掀鹅,另一方面也是為了能夠有所傳承。按照規(guī)范來媒楼,總還是不那么容易出差池的乐尊。還有就是,架構(gòu)師一開始考慮的東西也會有很多划址,不可能在第一版就把它們?nèi)繉崿F(xiàn)扔嵌,對于一個尚未發(fā)版的App來說,第一版架構(gòu)往往是最小完整功能集夺颤,那么在第二版第三版的發(fā)展過程中痢缎,架構(gòu)的迭代任務(wù)就很有可能不只是你一個人的事情了,相信你一個人也不見得能搞定全部世澜。所以你要跟你的合作者們有所約定独旷。另外,第一版出去之后,業(yè)務(wù)工程師在使用過程中也會產(chǎn)生很多修改意見嵌洼,哪些意見是合理的案疲,哪些意見是不合理的,也要通過事先約定的規(guī)范來進(jìn)行篩選麻养,最終決定如何采納络拌。規(guī)范也不是一成不變的,什么時候槍斃意見回溺,什么時候改規(guī)范,這就要靠各位的技術(shù)和經(jīng)驗了混萝。以上就是前言遗遵。這篇文章講什么?View代碼結(jié)構(gòu)的規(guī)定關(guān)于view的布局何時使用storyboard逸嘀,何時使用nib车要,何時使用代碼寫View是否有必要讓業(yè)務(wù)方統(tǒng)一派生ViewController?方便View布局的小工具M(jìn)VC崭倘、MVVM翼岁、MVCS、VIPER本門心法跨業(yè)務(wù)時View的處理留給評論區(qū)各種補(bǔ)總結(jié)View代碼結(jié)構(gòu)的規(guī)定架構(gòu)師不是寫SDK出來交付業(yè)務(wù)方使用就沒事兒了的司光,每家公司一定都有一套代碼規(guī)范琅坡,架構(gòu)師的職責(zé)也包括定義代碼規(guī)范。按照道理來講残家,定代碼規(guī)范應(yīng)該是屬于通識榆俺,放在這里講的原因只是因為我這邊需要為View添加一個規(guī)范。制定代碼規(guī)范嚴(yán)格來講不屬于View層架構(gòu)的事情坞淮,但它對View層架構(gòu)未來的影響會比較大茴晋,也是屬于架構(gòu)師在設(shè)計View層架構(gòu)時需要考慮的事情。制定View層規(guī)范的重要性在于:提高業(yè)務(wù)方View層的可讀性可維護(hù)性防止業(yè)務(wù)代碼對架構(gòu)產(chǎn)生腐蝕確保傳承保持架構(gòu)發(fā)展的方向不輕易被不合理的意見所左右在這一節(jié)里面我不打算從頭開始定義一套規(guī)范回窘,蘋果有一套Coding Guidelines诺擅,當(dāng)我們定代碼結(jié)構(gòu)或規(guī)范的時候,首先一定要符合這個規(guī)范啡直。然后烁涌,相信大家各自公司里面也都有一套自己的規(guī)范,具體怎么個規(guī)范法其實也是根據(jù)各位架構(gòu)師的經(jīng)驗而定酒觅,我這邊只是建議各位在各自規(guī)范的基礎(chǔ)上再加上下面這一點烹玉。viewController的代碼應(yīng)該差不多是這樣:pic1要點如下:所有的屬性都使用getter和setter不要在viewDidLoad里面初始化你的view然后再add,這樣代碼就很難看阐滩。在viewDidload里面只做addSubview的事情二打,然后在viewWillAppear里面做布局的事情(勘誤1),最后在viewDidAppear里面做Notification的監(jiān)聽之類的事情掂榔。至于屬性的初始化继效,則交給getter去做症杏。比如這樣:#pragma mark - life cycle- (void)viewDidLoad{? ? [super viewDidLoad];? ? self.view.backgroundColor = [UIColor whiteColor];? ? [self.view addSubview:self.firstTableView];? ? [self.view addSubview:self.secondTableView];? ? [self.view addSubview:self.firstFilterLabel];? ? [self.view addSubview:self.secondFilterLabel];? ? [self.view addSubview:self.cleanButton];? ? [self.view addSubview:self.originImageView];? ? [self.view addSubview:self.processedImageView];? ? [self.view addSubview:self.activityIndicator];? ? [self.view addSubview:self.takeImageButton];}- (void)viewWillAppear:(BOOL)animated{? ? [super viewWillAppear:animated];? ? CGFloat width = (self.view.width - 30) / 2.0f;? ? self.originImageView.size = CGSizeMake(width, width);? ? [self.originImageView topInContainer:70 shouldResize:NO];? ? [self.originImageView leftInContainer:10 shouldResize:NO];? ? self.processedImageView.size = CGSizeMake(width, width);? ? [self.processedImageView right:10 FromView:self.originImageView];? ? [self.processedImageView topEqualToView:self.originImageView];? ? CGFloat labelWidth = self.view.width - 100;? ? self.firstFilterLabel.size = CGSizeMake(labelWidth, 20);? ? [self.firstFilterLabel leftInContainer:10 shouldResize:NO];? ? [self.firstFilterLabel top:10 FromView:self.originImageView];? ? ... ...}這樣即便在屬性非常多的情況下,還是能夠保持代碼整齊瑞信,view的初始化都交給getter去做了厉颤。總之就是盡量不要出現(xiàn)以下的情況:- (void)viewDidLoad{? ? [super viewDidLoad];? ? self.textLabel = [[UILabel alloc] init];? ? self.textLabel.textColor = [UIColor blackColor];? ? self.textLabel ... ...? ? self.textLabel ... ...? ? self.textLabel ... ...? ? [self.view addSubview:self.textLabel];}這種做法就不夠干凈凡简,都扔到getter里面去就好了逼友。關(guān)于這個做法,在唐巧的技術(shù)博客里面有一篇文章和我所提倡的做法不同秤涩,這個我會放在后面詳細(xì)論述帜乞。getter和setter全部都放在最后因為一個ViewController很有可能會有非常多的view,就像上面給出的代碼樣例一樣筐眷,如果getter和setter寫在前面黎烈,就會把主要邏輯扯到后面去,其他人看的時候就要先劃過一長串getter和setter匀谣,這樣不太好照棋。然后要求業(yè)務(wù)工程師寫代碼的時候按照順序來分配代碼塊的位置,先是life cycle武翎,然后是Delegate方法實現(xiàn)烈炭,然后是event response,然后才是getters and setters宝恶。這樣后來者閱讀代碼時就能省力很多梳庆。每一個delegate都把對應(yīng)的protocol名字帶上,delegate方法不要到處亂寫卑惜,寫到一塊區(qū)域里面去比如UITableViewDelegate的方法集就老老實實寫上#pragma mark - UITableViewDelegate膏执。這樣有個好處就是,當(dāng)其他人閱讀一個他并不熟悉的Delegate實現(xiàn)方法時露久,他只要按住command然后去點這個protocol名字更米,Xcode就能夠立刻跳轉(zhuǎn)到對應(yīng)這個Delegate的protocol定義的那部分代碼去,就省得他到處找了毫痕。event response專門開一個代碼區(qū)域所有button征峦、gestureRecognizer的響應(yīng)事件都放在這個區(qū)域里面,不要到處亂放消请。關(guān)于private methods栏笆,正常情況下ViewController里面不應(yīng)該寫不是delegate方法的,不是event response方法的臊泰,不是life cycle方法的蛉加,就是private method了。對的,正常情況下ViewController里面一般是不會存在private methods的针饥,這個private methods一般是用于日期換算厂抽、圖片裁剪啥的這種小功能。這種小功能要么把它寫成一個category丁眼,要么把他做成一個模塊筷凤,哪怕這個模塊只有一個函數(shù)也行。ViewController基本上是大部分業(yè)務(wù)的載體苞七,本身代碼已經(jīng)相當(dāng)復(fù)雜藐守,所以跟業(yè)務(wù)關(guān)聯(lián)不大的東西能不放在ViewController里面就不要放。另外一點蹂风,這個private method的功能這時候只是你用得到卢厂,但是將來說不定別的地方也會用到,一開始就獨立出來硫眨,有利于將來的代碼復(fù)用。為什么要這樣要求巢块?我見過無數(shù)ViewController礁阁,代碼布局亂得一塌糊涂,這里一個delegate那里一個getter族奢,然后ViewController的代碼一般都死長死長的姥闭,看了就讓人頭疼。定義好這個規(guī)范越走,就能使得ViewController條理清晰棚品,業(yè)務(wù)方程序員很能夠區(qū)分哪些放在ViewController里面比較合適,哪些不合適廊敌。另外铜跑,也可以提高代碼的可維護(hù)性和可讀性。關(guān)于View的布局業(yè)務(wù)工程師在寫View的時候一定逃不掉的就是這個命題骡澈。用Frame也好用Autolayout也好锅纺,如果沒有精心設(shè)計過,布局部分一定慘不忍睹肋殴。直接使用CGRectMake的話可讀性很差囤锉,光看那幾個數(shù)字,也無法知道view和view之間的位置關(guān)系护锤。用Autolayout可讀性稍微好點兒官地,但生成Constraint的長度實在太長,代碼觀感不太好烙懦。Autolayout這邊可以考慮使用Masonry驱入,代碼的可讀性就能好很多。如果還有使用Frame的,可以考慮一下使用這個項目沧侥。這個項目里面提供了Frame相關(guān)的方便方法(UIView+LayoutMethods)可霎,里面的方法也基本涵蓋了所有布局的需求,可讀性非常好宴杀,使用它之后基本可以和CGRectMake說再見了癣朗。因為天貓在最近才切換到支持iOS6,所以之前天貓都是用Frame布局的旺罢,在天貓App中旷余,首頁,范兒部分頁面的布局就使用了這些方法扁达。使用這些方便方法能起到事半功倍的效果正卧。這個項目也提供了Autolayout方案下生產(chǎn)Constraints的方便方法(UIView+AEBHandyAutoLayout),可讀性比原生好很多跪解。我當(dāng)時在寫這系列方法的時候還不知道有Masonry炉旷。知道有Masonry之后我特地去看了一下,發(fā)現(xiàn)Masonry功能果然強(qiáng)大叉讥。不過這系列方法雖然沒有Masonry那么強(qiáng)大窘行,但是也夠用了。當(dāng)時安居客iPad版App全部都是Autolayout來做的View布局图仓,就是使用的這個項目里面的方法罐盔。可讀性很好救崔。讓業(yè)務(wù)工程師使用良好的工具來做View的布局惶看,能提高他們的工作效率驳癌,也能減少bug發(fā)生的幾率政基。架構(gòu)師不光要關(guān)心那些高大上的內(nèi)容,也要多給業(yè)務(wù)工程師提供方便易用的小工具闽晦,才能發(fā)揮架構(gòu)師的價值劫窒。何時使用storyboard莹桅,何時使用nib,何時使用代碼寫View這個問題唐巧的博客里這篇文章也提到過烛亦,我的意見和他是基本一致的诈泼。在這里我還想補(bǔ)充一些內(nèi)容:具有一定規(guī)模的團(tuán)隊化iOS開發(fā)(10人以上)有以下幾個特點:同一份代碼文件的作者會有很多,不同作者同時修改同一份代碼的情況也不少見煤禽。因此铐达,使用Git進(jìn)行代碼版本管理時出現(xiàn)Conflict的幾率也比較大。需求變化非常頻繁檬果,產(chǎn)品經(jīng)理一時一個主意瓮孙,為了完成需求而針對現(xiàn)有代碼進(jìn)行微調(diào)的情況唐断,以及針對現(xiàn)有代碼的部分復(fù)用的情況也比較多。復(fù)雜界面元素杭抠、復(fù)雜動畫場景的開發(fā)任務(wù)比較多脸甘。如果這三個特點你一看就明白了,下面的解釋就可以不用看了偏灿。如果你針對我的傾向愿意進(jìn)一步討論的丹诀,可以先看我下面的解釋,看完再說翁垂。同一份代碼文件的作者會有很多铆遭,不同作者同時修改同一份代碼的情況也不少見。因此沿猜,使用Git進(jìn)行代碼版本管理時出現(xiàn)Conflict的幾率也比較大枚荣。iOS開發(fā)過程中,會遇到最蛋疼的兩種Conflict一個是project.pbxproj啼肩,另外一個就是StoryBoard或XIB橄妆。因為這些文件的內(nèi)容的可讀性非常差,雖然蘋果在XCode5(現(xiàn)在我有點不確定是不是這個版本了)中對StoryBoard的文件描述方式做了一定的優(yōu)化祈坠,但只是把可讀性從非常差提升為很差害碾。然而在StoryBoard中往往包含了多個頁面,這些頁面基本上不太可能都由一個人去完成颁虐,如果另一個人在做StoryBoard的操作的時候蛮原,出于某些目的動了一下不屬于他的那個頁面卧须,比如為了美觀調(diào)整了一下位置另绩。然后另外一個人也因為要添加一個頁面,而在Storyboard中調(diào)整了一下某個其他頁面的位置花嘶。那么針對這個情況我除了說個呵呵以外笋籽,我就只能說:祝你好運(yùn)⊥衷保看清楚哦车海,這還沒動具體的頁頁面內(nèi)容呢。但如果使用代碼繪制View隘击,Conflict一樣會發(fā)生侍芝,但是這種Conflict就好解很多了,你懂的埋同。需求變化非常頻繁州叠,產(chǎn)品經(jīng)理一時一個主意,為了完成需求而針對現(xiàn)有代碼進(jìn)行微調(diào)的情況凶赁,以及針對現(xiàn)有代碼的部分復(fù)用的情況也比較多咧栗。我覺得產(chǎn)品經(jīng)理一時一個主意不是他的錯逆甜,他說不定也是被逼的,比如誰都會來摻和一下產(chǎn)品的設(shè)計致板,公司里的所有人交煞,上至CEO,下至基層員工都有可能對產(chǎn)品設(shè)計評頭論足斟或,只要他個人有個地方用得不爽(極大可能是個人喜好)然后又正好跟產(chǎn)品經(jīng)理比較熟悉能夠搭得上話素征,都會提出各種意見。產(chǎn)品經(jīng)理躲不起也惹不起缕粹,有時也是沒辦法稚茅,嗯。但落實到工程師這邊來平斩,這種情況就很蛋疼亚享。因為這種改變有時候不光是UI,UI所對應(yīng)的邏輯也有要改的可能绘面,工程師就會兩邊文件都改欺税,你原來link的那個view現(xiàn)在不link了,然后你的outlet對應(yīng)也要刪掉揭璃,這兩部分只要有一個沒做晚凿,編譯通過之后跑一下App,一會兒就crash了瘦馍〖呋啵看起來這不是什么大事兒,但很影響心情情组。另外燥筷,如果出現(xiàn)部分的代碼復(fù)用,比如說某頁面下某個View也希望放在另外一個頁面里院崇,相關(guān)的操作就不是復(fù)制粘貼這么簡單了肆氓,你還得重新link一遍。也很影響心情底瓣。復(fù)雜界面元素谢揪,復(fù)雜動畫交互場景的開發(fā)任務(wù)比較多。要是想在基于StoryBoard的項目中做一個動畫捐凭,很煩拨扶。做幾個復(fù)雜界面元素,也很煩茁肠。有的時候我們掛Custom View上去患民,其實在StoryBoard里面看來就是一個空白View。然后另外一點就是官套,當(dāng)你的layout出現(xiàn)問題需要調(diào)整的時候酒奶,還是挺難找到問題所在的蚁孔,尤其是在復(fù)雜界面元素的情況下。所以在針對View層這邊的要求時惋嚎,我也是建議不要用StoryBoard杠氢。實現(xiàn)簡單的東西,用Code一樣簡單另伍,實現(xiàn)復(fù)雜的東西鼻百,Code比StoryBoard更簡單。所以我更加提倡用code去畫view而不是storyboard摆尝。是否有必要讓業(yè)務(wù)方統(tǒng)一派生ViewController有的時候我們出于記錄用戶操作行為數(shù)據(jù)的需要温艇,或者統(tǒng)一配置頁面的目的,會從UIViewController里面派生一個自己的ViewController堕汞,來執(zhí)行一些通用邏輯勺爱。比如天貓客戶端要求所有的ViewController都要繼承自TMViewController。這個統(tǒng)一的父類里面針對一個ViewController的所有生命周期都做了一些設(shè)置讯检,至于這里都有哪些設(shè)置對于本篇文章來說并不重要琐鲁。在這里我想討論的是,在設(shè)計View架構(gòu)時人灼,如果為了能夠達(dá)到統(tǒng)一設(shè)置或執(zhí)行統(tǒng)一邏輯的目的围段,使用派生的手段是有必要的嗎投放?我覺得沒有必要灸芳,為什么沒有必要涝桅?使用派生比不使用派生更容易增加業(yè)務(wù)方的使用成本不使用派生手段一樣也能達(dá)到統(tǒng)一設(shè)置的目的這兩條原因是我認(rèn)為沒有必要使用派生手段的理由苹支,如果兩條理由你都心領(lǐng)神會砾隅,那么下面的就可以不用看了晴埂。如果你還有點疑惑儒洛,請看下面我來詳細(xì)講一下原因琅锻。為什么使用了派生,業(yè)務(wù)方的使用成本會提升?其實不光是業(yè)務(wù)方的使用成本拇派,架構(gòu)的維護(hù)成本也會上升件豌。那么具體的成本都來自于哪里呢茧彤?集成成本這里講的集成成本是這樣的:如果業(yè)務(wù)方自己開了一個獨立demo,快速完成了某個獨立流程遭殉,現(xiàn)在他想把這個現(xiàn)有流程集合進(jìn)去险污。那么問題就來了,他需要把所有獨立的UIViewController改變成TMViewController蚁飒。那為什么不是一開始就立刻使用TMViewController呢淮逻?因為要想引入TMViewController爬早,就要引入整個天貓App所有的業(yè)務(wù)線,所有的基礎(chǔ)庫桨啃,因為這個父類里面涉及很多天貓環(huán)境才有的內(nèi)容吨拍,所謂拔出蘿卜帶出泥羹饰,你要是想簡單繼承一下就能搞定的事情队秩,搭環(huán)境就要搞半天,然后這個小Demo才能跑得起來鸟蟹。對于業(yè)務(wù)層存在的所有父類來說建钥,它們是很容易跟項目中的其他代碼糾纏不清的,這使得業(yè)務(wù)方開發(fā)時遇到一個兩難問題:要么把所有依賴全部搞定镐依,然后基于App環(huán)境(比如天貓)下開發(fā)Demo,要么就是自己Demo寫好之后务唐,按照環(huán)境要求改代碼来农。這里的兩難問題都會帶來成本沃于,都會影響業(yè)務(wù)方的迭代進(jìn)度檩互。我不確定各位所在公司是否會有這樣的情況闸昨,但我可以在這里給大家舉一個我在阿里的真實的例子:我最近在開發(fā)某濾鏡Demo和相關(guān)頁面流程拍嵌,最終是要合并到天貓這個App里面去的横辆。使用天貓環(huán)境進(jìn)行開發(fā)的話,pod install完所有依賴差不多需要10分鐘脆侮,然后打開workspace之后,差不多要再等待1分鐘讓xcode做好索引筋蓖,然后才能正式開始工作。在這里要感謝一下則平瓮下,因為他在此基礎(chǔ)上做了很多優(yōu)化,使得這個1分鐘已經(jīng)比原來的時間短很多了路呜。但如果天貓環(huán)境有更新胀葱,你就要再重復(fù)一次上面的流程庆锦,否則 就很有可能編譯不過。拜托燕耿,我只是想做個Demo而已,不想搞那么復(fù)雜蚜锨。上手接受成本新來的業(yè)務(wù)工程師有的時候不見得都記得每一個ViewController都必須要派生自TMViewController而不是直接的UIViewController。新來的工程師他不能直接按照蘋果原生的做法去做事情氛悬,他需要額外學(xué)習(xí),比如說:所有的ViewController都必須繼承自TMViewController镜遣。架構(gòu)的維護(hù)難度盡可能少地使用繼承能提高項目的可維護(hù)性,具體內(nèi)容我在《跳出面向?qū)ο笏枷耄ㄒ唬?繼承》里面說了寓辱,在這里我想偷懶不想把那篇文章里說過的東西再說一遍诱鞠。其實對于業(yè)務(wù)方來說肋乍,主要還是第一個集成成本比較蛋疼墓造,因為這是長痛帝雇,每次要做點什么事情都會遇到。第二點倒還好吮廉,短痛。第三點跟業(yè)務(wù)工程師沒啥關(guān)系调卑。那么如果不使用派生,我們應(yīng)該使用什么手段?我的建議是使用AOP现使。在架構(gòu)師實現(xiàn)具體的方案之前,必須要想清楚幾個問題售碳,然后才能決定采用哪種方案间景。是哪幾個問題?方案的效果封拧,和最終要達(dá)到的目的是什么?在自己的知識體系里面,是否具備實現(xiàn)這個方案的能力味抖?在業(yè)界已有的開源組件里面,是否有可以直接拿來用的輪子红柱?這三個問題按照順序一一解答之后锤悄,具體方案就能出來了。我們先看第一個問題:方案的效果,和最終要達(dá)到的目的是什么蚂会?方案的效果應(yīng)該是:業(yè)務(wù)方可以不用通過繼承的方法趁猴,然后框架能夠做到對ViewController的統(tǒng)一配置。業(yè)務(wù)方即使脫離框架環(huán)境捕犬,不需要修改任何代碼也能夠跑完代碼探孝。業(yè)務(wù)方的ViewController一旦丟入框架環(huán)境缸濒,不需要修改任何代碼,框架就能夠起到它應(yīng)該起的作用。其實就是要實現(xiàn)不通過業(yè)務(wù)代碼上對框架的主動迎合啸澡,使得業(yè)務(wù)能夠被框架感知這樣的功能。細(xì)化下來就是兩個問題,框架要能夠攔截到ViewController的生命周期龄广,另一個問題就是,攔截的定義時機(jī)谆刨。對于方法攔截,很容易想到Method Swizzling,那么我們可以寫一個實例番舆,在App啟動的時候添加針對UIViewController的方法攔截,這是一種做法。還有另一種做法就是吗氏,使用NSObject的load函數(shù),在應(yīng)用啟動時自動監(jiān)聽。使用后者的好處在于捂齐,這個模塊只要被項目包含,就能夠發(fā)揮作用压真,不需要在項目里面添加任何代碼佃迄。然后另外一個要考慮的事情就是堆缘,原有的TMViewController(所謂的父類)也是會提供額外方法方便子類使用的录平,Method Swizzling只支持針對現(xiàn)有方法的操作,拓展方法的話,嗯燃逻,當(dāng)然是用Category啦握童。我本人不贊成Category的過度使用,但鑒于Category是最典型的化繼承為組合的手段,在這個場景下還是適合使用的步鉴。還有的就是随闪,關(guān)于Method Swizzling手段實現(xiàn)方法攔截撮奏,業(yè)界也已經(jīng)有了現(xiàn)成的開源庫:Aspects藐石,我們可以直接拿來使用。我這邊有個非常非常小的Demo可以放出來給大家,這個Demo只是一個點睛之筆,有一些話我也寫在這個Demo里面了荠藤,各位架構(gòu)師們你們可以基于各自公司App的需求去拓展念秧。這個Demo不包含Category币狠,畢竟Category還是得你們自己去寫啊~然后這套方案能夠完成原來通過派生手段所有可以完成的任務(wù),但同時又允許業(yè)務(wù)方不必添加任何代碼砾层,直接使用原生的UIViewController漩绵。然后另外要提醒的是,這方案的目的是消除不必要的繼承肛炮,雖然不限定于UIViewController,但它也是有適用范圍的铸董,在適用繼承的地方祟印,還是要老老實實使用繼承。比如你有一個數(shù)據(jù)模型粟害,是由基本模型派生出的一整套模型蕴忆,那么這個時候還是老老實實使用繼承。至于拿捏何時使用繼承悲幅,相信各位架構(gòu)師一定能夠處理好套鹅,或者你也可以參考我前面提到的那篇文章來控制拿捏的尺度站蝠。關(guān)于MVC、MVVM等一大堆思想其實這些都是相對通用的思想卓鹿,萬變不離其宗的還是在開篇里面我提到的那三個角色:數(shù)據(jù)管理者菱魔,數(shù)據(jù)加工者,數(shù)據(jù)展示者吟孙。這些五花八門的思想澜倦,不外乎就是制訂了一個規(guī)范,規(guī)定了這三個角色應(yīng)當(dāng)如何進(jìn)行數(shù)據(jù)交換杰妓。但同時這些也是爭議最多的話題藻治,所以我在這里來把幾個主流思想做一個梳理,當(dāng)你在做View層架構(gòu)時巷挥,能夠有個比較好的參考桩卵。MVCMVC(Model-View-Controller)是最老牌的的思想,老牌到4人幫的書里把它歸成了一種模式倍宾,其中Model就是作為數(shù)據(jù)管理者雏节,View作為數(shù)據(jù)展示者,Controller作為數(shù)據(jù)加工者高职,Model和View又都是由Controller來根據(jù)業(yè)務(wù)需求調(diào)配钩乍,所以Controller還負(fù)擔(dān)了一個數(shù)據(jù)流調(diào)配的功能。正在我寫這篇文章的時候初厚,我看到InfoQ發(fā)了這篇文章件蚕,里面提到了一個移動開發(fā)中的痛點是:對MVC架構(gòu)劃分的理解。我當(dāng)時沒能夠去參加這個座談會产禾,也沒辦法發(fā)表個人意見,所以就只能在這里寫寫了牵啦。在iOS開發(fā)領(lǐng)域亚情,我們應(yīng)當(dāng)如何進(jìn)行MVC的劃分?這里面其實有兩個問題:為什么我們會糾結(jié)于iOS開發(fā)領(lǐng)域中MVC的劃分問題哈雏?在iOS開發(fā)領(lǐng)域中楞件,怎樣才算是劃分的正確姿勢?為什么我們會糾結(jié)于iOS開發(fā)領(lǐng)域中MVC的劃分問題裳瘪?關(guān)于這個土浸,每個人糾結(jié)的點可能不太一樣,我也不知道當(dāng)時座談會上大家的觀點彭羹。但請允許我猜一下:是不是因為UIViewController中自帶了一個View黄伊,且控制了View的整個生命周期(viewDidLoad,viewWillAppear...),而在常識中我們都知道Controller不應(yīng)該和View有如此緊密的聯(lián)系派殷,所以才導(dǎo)致大家對劃分產(chǎn)生困惑还最?墓阀,下面我會針對這個猜測來給出我的意見。在服務(wù)端開發(fā)領(lǐng)域拓轻,Controller和View的交互方式一般都是這樣斯撮,比如Yii:? ? /*? ? ? ? ...? ? ? ? ? ? 數(shù)據(jù)庫取數(shù)據(jù)? ? ? ? ...? ? ? ? ? ? 處理數(shù)據(jù)? ? ? ? ...? ? */? ? // 此處$this就是Controller? ? $this->render("plan",array(? ? ? ? 'planList' => $planList,? ? ? ? 'plan_id' => $_GET['id'],? ? ));這里Controller和View之間區(qū)分得非常明顯,Controller做完自己的事情之后扶叉,就把所有關(guān)于View的工作交給了頁面渲染引擎去做勿锅,Controller不會去做任何關(guān)于View的事情,包括生成View枣氧,這些都由渲染引擎代勞了粱甫。這是一個區(qū)別,但其實服務(wù)端View的概念和Native應(yīng)用View的概念作瞄,真正的區(qū)別在于:從概念上嚴(yán)格劃分的話茶宵,服務(wù)端其實根本沒有View,拜HTTP協(xié)議所賜宗挥,我們平時所討論的View只是用于描述View的字符串(更實質(zhì)的應(yīng)該稱之為數(shù)據(jù))乌庶,真正的View是瀏覽器。契耿。所以服務(wù)端只管生成對View的描述瞒大,至于對View的長相,UI事件監(jiān)聽和處理搪桂,都是瀏覽器負(fù)責(zé)生成和維護(hù)的透敌。但是在Native這邊來看,原本屬于瀏覽器的任務(wù)也逃不掉要自己做踢械。那么這件事情由誰來做最合適酗电?蘋果給出的答案是:UIViewController。鑒于蘋果在這一層做了很多艱苦卓絕的努力内列,讓iOS工程師們不必親自去實現(xiàn)這些內(nèi)容撵术。而且,它把所有的功能都放在了UIView上话瞧,并且把UIView做成不光可以展示UI嫩与,還可以作為容器的一個對象〗慌牛看到這兒你明白了嗎划滋?UIView的另一個身份其實是容器!UIViewController中自帶的那個view埃篓,它的主要任務(wù)就是作為一個容器处坪。如果它所有的相關(guān)命名都改成ViewContainer捧韵,那么代碼就會變成這樣:- (void)viewContainerDidLoad{? ? [self.viewContainer addSubview:self.label];? ? [self.viewContainer addSubview:self.tableView];? ? [self.viewContainer addSubview:self.button];? ? [self.viewContainer addSubview:self.textField];}... ...僅僅改了個名字却紧,現(xiàn)在是不是感覺清晰了很多命辖?如果再要說詳細(xì)一點葵萎,我們平常所認(rèn)為的服務(wù)端MVC是這樣劃分的:? ? ? ? ? ? ? ---------------------------? ? ? ? ? ? ? | C? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? |? ? ? ? Controller? ? ? |? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ---------------------------? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? ? ? ? \? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? ? ? ? ? \? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \------------? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ---------------------| M? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? | V? ? ? ? ? ? ? ? ||? Model? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? Render Engine? ||? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? +? ? ? ? |------------? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? HTML Files? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ---------------------但事實上,整套流程的MVC劃分是這樣:? ? ? ? ? ? ? ---------------------------? ? ? ? ? ? ? | C? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? |? Controller? ? ? ? ? ? |? ? ? ? ? ? ? |? ? ? ? ? \? ? ? ? ? ? |? ? ? ? ? ? ? |? ? ? ? ? Render Engine |? ? ? ? ? ? ? |? ? ? ? ? ? ? ? +? ? ? |? ? ? ? ? ? ? |? ? ? ? ? ? HTML Files? |? ? ? ? ? ? ? ---------------------------? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? ? ? ? \? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? ? ? ? ? \ HTML String? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \------------? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ---------------| M? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? | V? ? ? ? ? ||? Model? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? Browser? ||? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? |------------? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ---------------由圖中可以看出塞椎,我們服務(wù)端開發(fā)在這個概念下桨仿,其實只涉及M和C的開發(fā)工作,瀏覽器作為View的容器案狠,負(fù)責(zé)View的展示和事件的監(jiān)聽服傍。那么對應(yīng)到iOS客戶端的MVC劃分上面來,就是這樣:? ? ? ? ? ? ? ----------------------------? ? ? ? ? ? ? | C? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? |? Controller? ? ? ? ? ? |? ? ? ? ? ? ? |? ? ? ? ? \? ? ? ? ? ? ? |? ? ? ? ? ? ? |? ? ? ? ? View Container |? ? ? ? ? ? ? ----------------------------? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? ? ? ? ? \? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \------------? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ----------------------| M? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? | V? ? ? ? ? ? ? ? ? ||? Model? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? UITableView? ? ||? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? YourCustomView? |------------? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ...? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ----------------------唯一區(qū)別在于骂铁,View的容器在服務(wù)端吹零,是由Browser負(fù)責(zé),在整個網(wǎng)站的流程中拉庵,這個容器放在Browser是非常合理的灿椅。在iOS客戶端,View的容器是由UIViewController中的view負(fù)責(zé)钞支,我也覺得蘋果做的這個選擇是非常正確明智的茫蛹。因為瀏覽器和服務(wù)端之間的關(guān)系非常松散,而且他們分屬于兩個不同陣營烁挟,服務(wù)端將對View的描述生成之后婴洼,交給瀏覽器去負(fù)責(zé)展示,然而一旦view上有什么事件產(chǎn)生撼嗓,基本上是很少傳遞到服務(wù)器(也就是所謂的Controller)的(要傳也可以:AJAX)柬采,都是在瀏覽器這邊把事情都做掉,所以在這種情況下且警,View容器就適合放在瀏覽器(V)這邊粉捻。但是在iOS開發(fā)領(lǐng)域,雖然也有讓View去監(jiān)聽事件的做法振湾,但這種做法非常少杀迹,都是把事件回傳給Controller,然后Controller再另行調(diào)度押搪。所以這時候,View的容器放在Controller就非常合適浅碾。Controller可以因為不同事件的產(chǎn)生去很方便地更改容器內(nèi)容大州,比如加載失敗時,把容器內(nèi)容換成失敗頁面的View垂谢,無網(wǎng)絡(luò)時厦画,把容器頁面換成無網(wǎng)絡(luò)的View等等。在iOS開發(fā)領(lǐng)域中,怎樣才算是MVC劃分的正確姿勢根暑?這個問題其實在上面已經(jīng)解答掉一部分了力试,那么這個問題的答案就當(dāng)是對上面問題的一個總結(jié)吧。M應(yīng)該做的事:給ViewController提供數(shù)據(jù)給ViewController存儲數(shù)據(jù)提供接口提供經(jīng)過抽象的業(yè)務(wù)基本組件排嫌,供Controller調(diào)度C應(yīng)該做的事:管理View Container的生命周期負(fù)責(zé)生成所有的View實例畸裳,并放入View Container監(jiān)聽來自View與業(yè)務(wù)有關(guān)的事件,通過與Model的合作淳地,來完成對應(yīng)事件的業(yè)務(wù)怖糊。V應(yīng)該做的事:響應(yīng)與業(yè)務(wù)無關(guān)的事件,并因此引發(fā)動畫效果颇象,點擊反饋(如果合適的話伍伤,盡量還是放在View去做)等。界面元素表達(dá)我通過與服務(wù)端MVC劃分的對比來回答了這兩個問題遣钳,之所以這么做扰魂,是因為我知道有很多iOS工程師之前是從服務(wù)端轉(zhuǎn)過來的。我也是這樣蕴茴,在進(jìn)安居客之前劝评,我也是做服務(wù)端開發(fā)的,在學(xué)習(xí)iOS的過程中荐开,我也曾經(jīng)對iOS領(lǐng)域的MVC劃分問題產(chǎn)生過疑惑付翁,我疑惑的點就是前面開篇我猜測的點。如果有人問我iOS中應(yīng)該怎么做MVC的劃分晃听,我就會像上面這么回答百侧。MVCS蘋果自身就采用的是這種架構(gòu)思路,從名字也能看出能扒,也是基于MVC衍生出來的一套架構(gòu)佣渴。從概念上來說,它拆分的部分是Model部分初斑,拆出來一個Store辛润。這個Store專門負(fù)責(zé)數(shù)據(jù)存取。但從實際操作的角度上講见秤,它拆開的是Controller砂竖。這算是瘦Model的一種方案,瘦Model只是專門用于表達(dá)數(shù)據(jù)鹃答,然后存儲乎澄、數(shù)據(jù)處理都交給外面的來做。MVCS使用的前提是测摔,它假設(shè)了你是瘦Model置济,同時數(shù)據(jù)的存儲和處理都在Controller去做解恰。所以對應(yīng)到MVCS,它在一開始就是拆分的Controller浙于。因為Controller做了數(shù)據(jù)存儲的事情护盈,就會變得非常龐大,那么就把Controller專門負(fù)責(zé)存取數(shù)據(jù)的那部分抽離出來羞酗,交給另一個對象去做腐宋,這個對象就是Store。這么調(diào)整之后整慎,整個結(jié)構(gòu)也就變成了真正意義上的MVCS脏款。關(guān)于胖Model和瘦Model我在面試和跟別人聊天時,發(fā)現(xiàn)知道胖Model和瘦Model的概念的人不是很多裤园。大約兩三年前國外業(yè)界曾經(jīng)對此有過非常激烈的討論撤师,主題就是Fat model, skinny controller。現(xiàn)在關(guān)于這方面的討論已經(jīng)不多了拧揽,然而直到今天胖Model和瘦Model哪個更好剃盾,業(yè)界也還沒有定論,所以這算是目前業(yè)界懸而未解的一個爭議淤袜。我很少看到國內(nèi)有討論這個的資料痒谴,所以在這里我打算補(bǔ)充一下什么叫胖Model什么叫瘦Model。以及他們的爭論來源于何處铡羡。什么叫胖Model积蔚?胖Model包含了部分弱業(yè)務(wù)邏輯。胖Model要達(dá)到的目的是烦周,Controller從胖Model這里拿到數(shù)據(jù)之后尽爆,不用額外做操作或者只要做非常少的操作,就能夠?qū)?shù)據(jù)直接應(yīng)用在View上读慎。舉個例子:Raw Data:? ? timestamp:1234567FatModel:? ? @property (nonatomic, assign) CGFloat timestamp;? ? - (NSString *)ymdDateString; // 2015-04-20 15:16? ? - (NSString *)gapString; // 3分鐘前漱贱、1小時前、一天前夭委、2015-3-13 12:34Controller:? ? self.dateLabel.text = [FatModel ymdDateString];? ? self.gapLabel.text = [FatModel gapString];把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ù)變動的可能性要比弱業(yè)務(wù)大得多,弱業(yè)務(wù)相對穩(wěn)定杏死,所以弱業(yè)務(wù)塞進(jìn)Model里面是沒問題的。另一方面,弱業(yè)務(wù)重復(fù)出現(xiàn)的頻率要大于強(qiáng)業(yè)務(wù)淑翼,對復(fù)用性的要求更高腐巢,如果這部分業(yè)務(wù)寫在Controller,類似的代碼會灑得到處都是玄括,一旦弱業(yè)務(wù)有修改(弱業(yè)務(wù)修改頻率低不代表就沒有修改)冯丙,這個事情就是一個災(zāi)難。如果塞到Model里面去遭京,改一處很多地方就能跟著改胃惜,就能避免這場災(zāi)難。然而其缺點就在于哪雕,胖Model相對比較難移植船殉,雖然只是包含弱業(yè)務(wù),但好歹也是業(yè)務(wù)斯嚎,遷移的時候很容易拔出蘿卜帶出泥利虫。另外一點,MVC的架構(gòu)思想更加傾向于Model是一個Layer堡僻,而不是一個Object糠惫,不應(yīng)該把一個Layer應(yīng)該做的事情交給一個Object去做。最后一點钉疫,軟件是會成長的硼讽,F(xiàn)atModel很有可能隨著軟件的成長越來越Fat,最終難以維護(hù)牲阁。什么叫瘦Model固阁?瘦Model只負(fù)責(zé)業(yè)務(wù)數(shù)據(jù)的表達(dá),所有業(yè)務(wù)無論強(qiáng)弱一律扔到Controller咨油。瘦Model要達(dá)到的目的是您炉,盡一切可能去編寫細(xì)粒度Model,然后配套各種helper類或方法來對弱業(yè)務(wù)做抽象役电,強(qiáng)業(yè)務(wù)依舊交給Controller赚爵。舉個例子:Raw Data:{? ? "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 ([Helper sexWithString:SlimModel.sex] == Male) {? ? ? ? ...? ? }由于SlimModel跟業(yè)務(wù)完全無關(guān),它的數(shù)據(jù)可以交給任何一個能處理它數(shù)據(jù)的Helper或其他的對象法瑟,來完成業(yè)務(wù)冀膝。在代碼遷移的時候獨立性很強(qiáng),很少會出現(xiàn)拔出蘿卜帶出泥的情況霎挟。另外窝剖,由于SlimModel只是數(shù)據(jù)表達(dá),對它進(jìn)行維護(hù)基本上是0成本酥夭,軟件膨脹得再厲害赐纱,SlimModel也不會大到哪兒去脊奋。缺點就在于,Helper這種做法也不見得很好疙描,這里有一篇文章批判了這個事情诚隙。另外,由于Model的操作會出現(xiàn)在各種地方起胰,SlimModel在一定程度上違背了DRY(Don't Repeat Yourself)的思路久又,Controller仍然不可避免在一定程度上出現(xiàn)代碼膨脹。我的態(tài)度效五?嗯地消,我會在本門心法這一節(jié)里面說。說回來畏妖,MVCS是基于瘦Model的一種架構(gòu)思路脉执,把原本Model要做的很多事情中的其中一部分關(guān)于數(shù)據(jù)存儲的代碼抽象成了Store,在一定程度上降低了Controller的壓力瓜客。MVVMMVVM去年在業(yè)界討論得非常多适瓦,無論國內(nèi)還是國外都討論得非常熱烈,尤其是在ReactiveCocoa這個庫成熟之后谱仪,ViewModel和View的信號機(jī)制在iOS下終于有了一個相對優(yōu)雅的實現(xiàn)玻熙。MVVM本質(zhì)上也是從MVC中派生出來的思想,MVVM著重想要解決的問題是盡可能地減少Controller的任務(wù)疯攒。不管MVVM也好嗦随,MVCS也好,他們的共識都是Controller會隨著軟件的成長敬尺,變很大很難維護(hù)很難測試枚尼。只不過兩種架構(gòu)思路的前提不同,MVCS是認(rèn)為Controller做了一部分Model的事情砂吞,要把它拆出來變成Store署恍,MVVM是認(rèn)為Controller做了太多數(shù)據(jù)加工的事情,所以MVVM把數(shù)據(jù)加工的任務(wù)從Controller中解放了出來蜻直,使得Controller只需要專注于數(shù)據(jù)調(diào)配的工作盯质,ViewModel則去負(fù)責(zé)數(shù)據(jù)加工并通過通知機(jī)制讓View響應(yīng)ViewModel的改變。MVVM是基于胖Model的架構(gòu)思路建立的概而,然后在胖Model中拆出兩部分:Model和ViewModel呼巷。關(guān)于這個觀點我要做一個額外解釋:胖Model做的事情是先為Controller減負(fù),然后由于Model變胖赎瑰,再在此基礎(chǔ)上拆出ViewModel王悍,跟業(yè)界普遍認(rèn)知的MVVM本質(zhì)上是為Controller減負(fù)這個說法并不矛盾,因為胖Model做的事情也是為Controller減負(fù)餐曼。另外压储,我前面說MVVM把數(shù)據(jù)加工的任務(wù)從Controller中解放出來鲜漩,跟MVVM拆分的是胖Model也不矛盾。要做到解放Controller渠脉,首先你得有個胖Model宇整,然后再把這個胖Model拆成Model和ViewModel。那么MVVM究竟應(yīng)該如何實現(xiàn)芋膘?這很有可能是大多數(shù)人糾結(jié)的問題,我打算憑我的個人經(jīng)驗試圖在這里回答這個問題霸饲,歡迎大家在評論區(qū)交流为朋。在iOS領(lǐng)域大部分MVVM架構(gòu)都會使用ReactiveCocoa,但是使用ReactiveCocoa的iOS應(yīng)用就是基于MVVM架構(gòu)的嗎厚脉?那當(dāng)然不是习寸,我覺得很多人都存在這個誤區(qū),我面試過的一些人提到了ReactiveCocoa也提到了MVVM傻工,但他們對此的理解膚淺得讓我忍俊不禁霞溪。嗯,在網(wǎng)絡(luò)層架構(gòu)我會舉出不使用ReactiveCocoa的例子中捆,現(xiàn)在舉我感覺有點兒早鸯匹。MVVM的關(guān)鍵是要有View Model!而不是ReactiveCocoa(勘誤2)ViewModel做什么事情泄伪?就是把RawData變成直接能被View使用的對象的一種Model殴蓬。舉個例子:? ? Raw Data:? ? ? ? {? ? ? ? ? ? (? ? ? ? ? ? ? ? (123, 456),? ? ? ? ? ? ? ? (234, 567),? ? ? ? ? ? ? ? (345, 678)? ? ? ? ? ? )? ? ? ? }這里的RawData我們假設(shè)是經(jīng)緯度,數(shù)字我隨便寫的不要太在意蟋滴。然后你有一個模塊是地圖模塊染厅,把經(jīng)緯度數(shù)組全部都轉(zhuǎn)變成MKAnnotation或其派生類對于Controller來說是弱業(yè)務(wù),(記住津函,胖Model就是用來做弱業(yè)務(wù)的)肖粮,因此我們用ViewModel直接把它轉(zhuǎn)變成MKAnnotation的NSArray,交給Controller之后Controller直接就可以用了尔苦。嗯涩馆,這就是ViewModel要做的事情,是不是覺得很簡單蕉堰,看不出優(yōu)越性凌净?安居客Pad應(yīng)用也有一個地圖模塊,在這里我設(shè)計了一個對象叫做reformer(其實就是ViewModel)屋讶,專門用來干這個事情冰寻。那么這么做的優(yōu)越性體現(xiàn)在哪兒呢?安居客分三大業(yè)務(wù):租房皿渗、二手房斩芭、新房轻腺。這三個業(yè)務(wù)對應(yīng)移動開發(fā)團(tuán)隊有三個API開發(fā)團(tuán)隊,他們各自為政划乖,這就造成了一個結(jié)果:三個API團(tuán)隊回饋給移動客戶端的數(shù)據(jù)內(nèi)容雖然一致贬养,但是數(shù)據(jù)格式是不一致的,也就是相同value對應(yīng)的key是不一致的琴庵。但展示地圖的ViewController不可能寫三個误算,所以肯定少不了要有一個API數(shù)據(jù)兼容的邏輯,這個邏輯我就放在reformer里面去做了迷殿,于是業(yè)務(wù)流程就變成了這樣:? ? ? ? ? ? 用戶進(jìn)入地圖頁發(fā)起地圖API請求? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? -----------------------------------------? ? ? |? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? |? ? ? |? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? |? 新房API? ? ? ? ? ? 二手房API? ? ? ? ? ? 租房API? ? ? |? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? |? ? ? |? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? |? ? ? -----------------------------------------? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? 獲得原始地圖數(shù)據(jù)? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? |? ? [APIManager fetchDataWithReformer:reformer]? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? MKAnnotationList? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? Controller這么一來儿礼,原本復(fù)雜的MKAnnotation組裝邏輯就從Controller里面拆分了出來,Controller可以直接拿著Reformer返回的數(shù)據(jù)進(jìn)行展示庆寺。APIManager就屬于Model蚊夫,reformer就屬于ViewModel。具體關(guān)于reformer的東西我會放在網(wǎng)絡(luò)層架構(gòu)來詳細(xì)解釋懦尝。Reformer此時扮演的ViewModel角色能夠很好地給Controller減負(fù)知纷,同時,維護(hù)成本也大大降低陵霉,經(jīng)過reformer產(chǎn)出的永遠(yuǎn)都是MKAnnotation琅轧,Controller可以直接拿來使用。然后另外一點撩匕,還有一個業(yè)務(wù)需求是取附近的房源鹰晨,地圖API請求是能夠hold住這個需求的,那么其他地方都不用變止毕,在fetchDataWithReformer的時候換一個reformer就可以了模蜡,其他的事情都交給reformer。那么ReactiveCocoa應(yīng)該扮演什么角色扁凛?不用ReactiveCocoa也能MVVM忍疾,用ReactiveCocoa能更好地體現(xiàn)MVVM的精髓。前面我舉到的例子只是數(shù)據(jù)從API到View的方向谨朝,View的操作也會產(chǎn)生"數(shù)據(jù)"卤妒,只不過這里的"數(shù)據(jù)"更多的是體現(xiàn)在表達(dá)用戶的操作上,比如輸入了什么內(nèi)容字币,那么數(shù)據(jù)就是text则披、選擇了哪個cell,那么數(shù)據(jù)就是indexPath洗出。那么在數(shù)據(jù)從view走向API或者Controller的方向上士复,就是ReactiveCocoa發(fā)揮的地方。我們知道,ViewModel本質(zhì)上算是Model層(因為是胖Model里面分出來的一部分)阱洪,所以View并不適合直接持有ViewModel便贵,那么View一旦產(chǎn)生數(shù)據(jù)了怎么辦?扔信號扔給ViewModel冗荸,用誰扔承璃?ReactiveCocoa。在MVVM中使用ReactiveCocoa的第一個目的就是如上所說蚌本,View并不適合直接持有ViewModel盔粹。第二個目的就在于,ViewModel有可能并不是只服務(wù)于特定的一個View魂毁,使用更加松散的綁定關(guān)系能夠降低ViewModel和View之間的耦合度玻佩。那么在MVVM中,Controller扮演什么角色席楚?大部分國內(nèi)外資料闡述MVVM的時候都是這樣排布的:View <-> ViewModel <-> Model,造成了MVVM不需要Controller的錯覺税稼,現(xiàn)在似乎發(fā)展成業(yè)界開始出現(xiàn)MVVM是不需要Controller的烦秩。的聲音了。其實MVVM是一定需要Controller的參與的郎仆,雖然MVVM在一定程度上弱化了Controller的存在感只祠,并且給Controller做了減負(fù)瘦身(這也是MVVM的主要目的)。但是扰肌,這并不代表MVVM中不需要Controller抛寝,MMVC和MVVM他們之間的關(guān)系應(yīng)該是這樣:MVMCV(來源:http://www.sprynthesis.com/2014/12/06/reactivecocoa-mvvm-introduction/)View <-> C <-> ViewModel <-> Model,所以使用MVVM之后曙旭,就不需要Controller的說法是不正確的盗舰。嚴(yán)格來說MVVM其實是MVCVM。從圖中可以得知桂躏,Controller夾在View和ViewModel之間做的其中一個主要事情就是將View和ViewModel進(jìn)行綁定钻趋。在邏輯上,Controller知道應(yīng)當(dāng)展示哪個View剂习,Controller也知道應(yīng)當(dāng)使用哪個ViewModel蛮位,然而View和ViewModel它們之間是互相不知道的,所以Controller就負(fù)責(zé)控制他們的綁定關(guān)系鳞绕,所以叫Controller/控制器就是這個原因失仁。前面扯了那么多,其實歸根結(jié)底就是一句話:在MVC的基礎(chǔ)上们何,把C拆出一個ViewModel專門負(fù)責(zé)數(shù)據(jù)處理的事情萄焦,就是MVVM。然后垂蜗,為了讓View和ViewModel之間能夠有比較松散的綁定關(guān)系楷扬,于是我們使用ReactiveCocoa解幽,因為蘋果本身并沒有提供一個比較適合這種情況的綁定方法。iOS領(lǐng)域里KVO烘苹,Notification躲株,block,delegate和target-action都可以用來做數(shù)據(jù)通信镣衡,從而來實現(xiàn)綁定霜定,但都不如ReactiveCocoa提供的RACSignal來的優(yōu)雅,如果不用ReactiveCocoa廊鸥,綁定關(guān)系可能就做不到那么松散那么好望浩,但并不影響它還是MVVM。在實際iOS應(yīng)用架構(gòu)中惰说,MVVM應(yīng)該出現(xiàn)在了大部分創(chuàng)業(yè)公司或者老牌公司新App的iOS應(yīng)用架構(gòu)圖中磨德,據(jù)我所知易寶支付旗下的某個iOS應(yīng)用就整體采用了MVVM架構(gòu),他們抽出了一個Action層來裝各種ViewModel吆视,也是屬于相對合理的結(jié)構(gòu)典挑。所以Controller在MVVM中,一方面負(fù)責(zé)View和ViewModel之間的綁定啦吧,另一方面也負(fù)責(zé)常規(guī)的UI邏輯處理您觉。VIPERVIPER(View,Interactor授滓,Presenter琳水,Entity,Routing)般堆。VIPER我并沒有實際使用過在孝,我是在objc.io上第13期看到的。但凡出現(xiàn)一個新架構(gòu)或者我之前并不熟悉的新架構(gòu)郁妈,有一點我能夠非郴肼辏肯定,這貨一定又是把MVC的哪個部分給拆開了(壞笑噩咪,做這種判斷的理論依據(jù)在第一篇文章里面我已經(jīng)講過了)顾彰。事實情況是VIPER確實拆了很多很多,除了View沒拆胃碾,其它的都拆了涨享。我提到的這兩篇文章關(guān)于VIPER都講得很詳細(xì),一看就懂仆百。但具體在使用VIPER的時候會有什么坑或者會有哪些爭議我不是很清楚厕隧,硬要寫這一節(jié)的話我只能靠YY,所以我想想還是算了。如果各位讀者有誰在實際App中采用VIPER架構(gòu)的或者對VIPER很有興趣的吁讨,可以評論區(qū)里面提出來髓迎,我們交流一下伊诵。本門心法重劍無鋒鹰溜,大巧不工胸遇。 ---- 《神雕俠侶》這是楊過在挑劍時盆均,玄鐵重劍旁邊寫的一段話。對此我深表認(rèn)同琳拭。提到這段話的目的是想告訴大家枣接,在具體做View層架構(gòu)的設(shè)計時盒至,不需要拘泥于MVC拴曲、MVVM争舞、VIPER等規(guī)矩。這些都是招式澈灼,告訴你你就知道了竞川,然后怎么玩都可以。但是心法不是這樣的叁熔,心法是大巧流译,說出來很簡單,但是能不能在實際架構(gòu)設(shè)計時牢記心法者疤,并且按照規(guī)矩辦事,就都看個人了叠赦。拆分的心法天下功夫出少林驹马,天下架構(gòu)出MVC。 ---- Casa TaloyumMVC其實是非常高Level的抽象除秀,意思也就是糯累,在MVC體系下還可以再衍生無數(shù)的架構(gòu)方式,但萬變不離其宗的是册踩,它一定符合MVC的規(guī)范泳姐。這句話不是我說的,是我在某個英文資料上看到的暂吉,但時過境遷胖秒,我已經(jīng)找不到出處了,我很贊同這句話慕的。我采用的架構(gòu)嚴(yán)格來說也是MVC阎肝,但也做了很多的拆分。根據(jù)前面幾節(jié)的洗禮肮街,相信各位也明白了這樣的道理:拆分方式的不同誕生了各種不同的衍生架構(gòu)方案(MVCS拆胖Controller风题,MVVM拆胖Model,VIPER什么都拆),但即便拆分方式再怎么多樣沛硅,那都只是招式眼刃。而拆分的規(guī)范,就是心法摇肌。這一節(jié)我就講講我在做View架構(gòu)時擂红,做拆分的心法。第一心法:保留最重要的任務(wù)朦蕴,拆分其它不重要的任務(wù)在iOS開發(fā)領(lǐng)域內(nèi)篮条,UIViewController承載了非常多的事情,比如View的初始化吩抓,業(yè)務(wù)邏輯涉茧,事件響應(yīng),數(shù)據(jù)加工等等疹娶,當(dāng)然還有更多我現(xiàn)在也列舉不出來伴栓,但是我們知道有一件事情Controller肯定逃不掉要做:協(xié)調(diào)V和M。也就是說雨饺,不管怎么拆钳垮,協(xié)調(diào)工作是拆不掉的。那么剩下的事情我們就可以拆了额港,比如UITableView的DataSource饺窿。唐巧的博客有一篇文章提到他和另一個工程師關(guān)于是否要拆分DataSource爭論了好久。拆分DataSource這個做法應(yīng)該也算是通用做法移斩,在不復(fù)雜的應(yīng)用里面肚医,它可能確實看上去只是一個數(shù)組而已,但在復(fù)雜的情況下向瓷,它背后可能涉及了文件內(nèi)容讀取肠套,數(shù)據(jù)同步等等復(fù)雜邏輯,這篇文章的第一節(jié)就提倡了這個做法猖任,我其實也蠻提倡的你稚。前面的文章里面也提了很多能拆的東西,我就不搬運(yùn)了朱躺,大家可以進(jìn)去看看刁赖。除了這篇文章提到的內(nèi)容以外,任何比較大的室琢,放在ViewController里面比較臟的乾闰,只要不是Controller的核心邏輯,都可以考慮拆出去盈滴,然后在架構(gòu)的時候作為一個獨立模塊去定義涯肩,以及設(shè)計實現(xiàn)轿钠。第二心法:拆分后的模塊要盡可能提高可復(fù)用性,盡量做到DRY根據(jù)第一心法拆開來的東西病苗,很有可能還是強(qiáng)業(yè)務(wù)相關(guān)的疗垛,這種情況有的時候無法避免。但我們拆也要拆得好看硫朦,拆出來的部分最好能夠歸成某一類對象贷腕,然后最好能夠抽象出一個通用邏輯出來,使他能夠復(fù)用咬展。即使不能抽出通用邏輯泽裳,那也盡量抽象出一個protocol,來實現(xiàn)IOP破婆。這里有篇關(guān)于IOP的文章涮总,大家看了就明白優(yōu)越性了。第三心法:要盡可能提高拆分模塊后的抽象度也就是說祷舀,拆分的粒度要盡可能大一點瀑梗,封裝得要透明一些。唐巧說一切隱藏都是對代碼復(fù)雜性的增加裳扯,除非它帶來了好處抛丽,這在一定程度上有點道理,沒有好處的隱藏確實都不好(笑)饰豺。提高抽象度事實上就是增加封裝的力度亿鲜,將一個負(fù)責(zé)的業(yè)務(wù)抽象成只需要很少的輸入就能完成,就是高度抽象冤吨。嗯狡门,繼承很多層,這種做法雖然也提高了抽象程度锅很,但我不建議這么玩。我不確定唐巧在這里說的隱藏跟我說的封裝是不是同一個概念凤跑,但我在這里想提倡的是盡可能提高抽象程度爆安。提高抽象程度的好處在于,對于業(yè)務(wù)方來說仔引,他只需要收集很少的信息(最小充要條件)扔仓,做很少的調(diào)度(Controller負(fù)責(zé)大模塊調(diào)度,大模塊里面再去做小模塊的調(diào)度)咖耘,就能夠完成任務(wù)翘簇,這才是給Controller減負(fù)的正確姿勢。如果拆分出來的模塊抽象程度不夠儿倒,模塊對外界要求的參數(shù)比較多版保,那么在Controller里面呜笑,關(guān)于收集參數(shù)的代碼就會多了很多。如果一部分參數(shù)的收集邏輯能夠由模塊來完成彻犁,那也可以做到幫Controller減輕負(fù)擔(dān)叫胁。否則就感覺拆得不太干凈,因為Controller里面還是多了一些不必要的參數(shù)收集邏輯汞幢。如果拆分出來的粒度太小驼鹅,Controller在完成任務(wù)的時候調(diào)度代碼要寫很多,那也不太好森篷。導(dǎo)致拆分粒度小的首要因素就是業(yè)務(wù)可能本身就比較復(fù)雜输钩,拆分粒度小并不是不好,能大就大一點仲智,如果小了买乃,那也沒問題。針對這種情況的處理坎藐,就需要采用strategy模式为牍。針對拆分粒度小的情況,我來舉個實際例子岩馍,這個例子來源于我的一個朋友他在做聊天應(yīng)用的消息發(fā)送模塊碉咆。當(dāng)消息是文字時,直接發(fā)送蛀恩。當(dāng)消息是圖片時疫铜,需要先向服務(wù)器申請上傳資源,獲得資源ID之后再上傳圖片双谆,上傳圖片完成之后拿到圖片URL壳咕,后面帶著URL再把信息發(fā)送出去。這時候我們拆模塊顽馋,可以拆成:數(shù)據(jù)發(fā)送(叫A模塊)谓厘,上傳資源申請(叫B模塊),內(nèi)容上傳(叫C模塊)寸谜。那么要發(fā)送文字消息竟稳,Controller調(diào)度A就可以了。如果要發(fā)送圖片消息熊痴,Controller調(diào)度B->C->A他爸,假設(shè)將來還有上傳別的類型消息的任務(wù),他們又要依賴D/E/F模塊果善,那這個事情就很蛋疼诊笤,因為邏輯復(fù)雜了,Controller要調(diào)度的東西要區(qū)分的情況就多了巾陕,Controller就膨脹了讨跟。那么怎么處理呢纪他?可以采用Strategy模式。我們再來分析一下许赃,Controller要完成任務(wù)止喷,它初始情況下所具有的條件是什么?它有這條消息的所有數(shù)據(jù)混聊,也知道這個消息的類型弹谁。那么它最終需要的是什么呢?消息發(fā)送的結(jié)果:發(fā)送成功或失敗句喜。? ? ? ? ? ? ? ? ? ? send msg? ? Controller ------------------> MessageSender? ? ? ? ^? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ----------------------------------? ? ? ? ? ? ? ? success / fail上面就是我們要實現(xiàn)的最終結(jié)果预愤,Controller只要把消息丟給MessageSender,然后讓MessageSender去做事情咳胃,做完了告訴Controller就好了植康。那么MessageSender里面怎么去調(diào)度邏輯?MessageSender里面可以有一個StrategyList展懈,里面存放了表達(dá)各種邏輯的Block或者Invocation(Target-Action)销睁。那么我們先定義一個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];// 然后對外提供一個這樣的接口冻记,同時有一個delegate用來回調(diào)- (void)sendMessage:(BaseMessage *)message withStrategy:(MessageSendStrategy)strategy;@property (nonatomic, weak) iddelegate;@protocol MessageSenderDelegate@required? ? ? - (void)messageSender:(MessageSender *)messageSender? ? ? didSuccessSendMessage:(BaseMessage *)message? ? ? ? ? ? ? ? ? strategy:(MessageSendStrategy)strategy;? ? ? - (void)messageSender:(MessageSender *)messageSender? ? ? ? didFailSendMessage:(BaseMessage *)message? ? ? ? ? ? ? ? ? strategy:(MessageSendStrategy)strategy? ? ? ? ? ? ? ? ? ? ? error:(NSError *)error;@endController里面是這樣使用的:? ? [self.messageSender sendMessage:message withStrategy:MessageSendStrategyText];MessageSender里面是這樣的:? ? [self.strategyList[strategy] invoke];然后在某個Invocation里面,就是這樣的:? ? [A invoke];? ? [B invoke];? ? [C invoke];這樣就好啦来惧,即便拆分粒度因為客觀原因無法細(xì)化冗栗,那也能把復(fù)雜的判斷邏輯和調(diào)度邏輯從Controller中抽出來,真正為Controller做到了減負(fù)供搀∮缇樱總之能夠做到大粒度就盡量大粒度,實在做不到那也行葛虐,用Strategy把它hold住胎源。這個例子是小粒度的情況,大粒度的情況太簡單屿脐,我就不舉了乒融。設(shè)計心法針對View層的架構(gòu)不光是看重如何合理地拆分MVC來給UIViewController減負(fù),另外一點也要照顧到業(yè)務(wù)方的使用成本摄悯。最好的情況是業(yè)務(wù)方什么都不知道,然后他把代碼放進(jìn)去就能跑愧捕,同時還能獲得框架提供的種種功能奢驯。比如天安門廣場上的觀眾看臺,就是我覺得最好的設(shè)計次绘,因為沒人會注意到它瘪阁。第一心法:盡可能減少繼承層級撒遣,涉及蘋果原生對象的盡量不要繼承繼承是罪惡,盡量不要繼承管跺。就我目前了解到的情況看义黎,除了安居客的Pad App沒有在框架級針對UIViewController有繼承的設(shè)計以外,其它公司或多或少都針對UIViewController有繼承豁跑,包括安居客iPhone app(那時候我已經(jīng)對此無能為力廉涕,可見View的架構(gòu)在一開始就設(shè)計好有多么重要)。甚至有的還對UITableView有繼承艇拍,這是一件多么令人發(fā)指狐蜕,多么慘絕人寰,多么喪心病狂的事情啊卸夕。雖然不可避免的是有些情況我們不得不從蘋果原生對象中繼承层释,比如UITableViewCell。但我還是建議盡量不要通過繼承的方案來給原生對象添加功能快集,前面提到的Aspect方案和Category方案都可以使用贡羔。用Aspect+load來實現(xiàn)重載函數(shù),用Category來實現(xiàn)添加函數(shù)个初,當(dāng)然乖寒,耍點手段用Category來添加property也是沒問題的。這些方案已經(jīng)覆蓋了繼承的全部功能勃黍,而且非常好維護(hù)宵统,對于業(yè)務(wù)方也更加透明,何樂而不為呢覆获。不用繼承可能在思路上不會那么直觀马澈,但是對于不使用繼承帶來的好處是足夠頂?shù)蒙鲜褂美^承的壞處的。順便在此我要給Category正一下名:業(yè)界對于Category的態(tài)度比較曖昧弄息,在多種場合(講座痊班、資料文檔)都宣揚(yáng)過盡可能不要使用Category。它們說的都有一定道理摹量,但我認(rèn)為Category是蘋果提供的最好的使用集合代替繼承的方案涤伐,但針對Category的設(shè)計對架構(gòu)師的要求也很高,請合理使用缨称。而且蘋果也在很多場合使用Category凝果,來把一個原本可能很大的對象,根據(jù)不同場景拆分成不同的Category睦尽,從而提高可維護(hù)性器净。不使用繼承的好處我在這里已經(jīng)說了,放到iOS應(yīng)用架構(gòu)來看当凡,還能再多額外兩個好處:1. 在業(yè)務(wù)方做業(yè)務(wù)開發(fā)或者做Demo時山害,可以脫離App環(huán)境纠俭,或花更少的時間搭建環(huán)境。2. 對業(yè)務(wù)方來說功能更加透明浪慌,也符合業(yè)務(wù)方在開發(fā)時的第一直覺冤荆。第二心法:做好代碼規(guī)范,規(guī)定好代碼在文件中的布局权纤,尤其是ViewController這主要是為了提高可維護(hù)性钓简。在一個文件非常大的對象中,尤其要限制好不同類型的代碼在文件中的布局妖碉。比如在寫ViewController時涌庭,我之前給團(tuán)隊制定的規(guī)范就是前面一段全部是getter setter,然后接下來一段是life cycle欧宜,viewDidLoad之類的方法都在這里坐榆。然后下面一段是各種要實現(xiàn)的Delegate,再下面一段就是event response冗茸,Button的或者GestureRecognizer的都在這里席镀。然后后面是private method。一般情況下夏漱,如果做好拆分豪诲,ViewController的private method那一段是沒有方法的。后來隨著時間的推移挂绰,我發(fā)現(xiàn)開頭放getter和setter太影響閱讀了屎篱,所以后面改成全放在ViewController的最后。第三心法:能不放在Controller做的事情就盡量不要放在Controller里面去做Controller會變得龐大的原因葵蒂,一方面是因為Controller承載了業(yè)務(wù)邏輯交播,MVC的總結(jié)者(在正式提出MVC之前,或多或少都有人這么設(shè)計践付,所以說MVC的設(shè)計者不太準(zhǔn)確)對Controller下的定義也是承載業(yè)務(wù)邏輯秦士,所以Controller就是用來干這事兒的,天經(jīng)地義永高。另一方面是因為在MVC中隧土,關(guān)于Model和View的定義都非常明確,很少有人會把一個屬于M或V的東西放到其他地方命爬。然后除了Model和View以外曹傀,還會剩下很多模棱兩可的東西,這些東西從概念上講都算Controller饲宛,而且由于M和V定義得那么明確皆愉,所以直覺上看,這些東西放在M或V是不合適的,于是就往Controller里面塞咯亥啦。正是由于上述兩方面原因?qū)е铝薈ontroller的膨脹。我們再細(xì)細(xì)思考一下练链,Model膨脹和View膨脹翔脱,要針對它們來做拆分其實都是相對容易的,Controller膨脹之后媒鼓,拆分就顯得艱難無比届吁。所以如果能夠在一開始就盡量把能不放在Controller做的事情放到別的地方去做,這樣在第一時間就可以讓你的那部分將來可能會被拆分的代碼遠(yuǎn)離業(yè)務(wù)邏輯绿鸣。所以我們要稍微轉(zhuǎn)變一下思路:模棱兩可的模塊疚沐,就不要塞到Controller去了,塞到V或者塞到M或者其他什么地方都比塞進(jìn)Controller好潮模,便于將來拆分亮蛔。所以關(guān)于前面我按下不表的關(guān)于胖Model和瘦Model的選擇,我的態(tài)度是更傾向于胖Model擎厢【苛鳎客觀地說,業(yè)務(wù)膨脹之后动遭,代碼規(guī)姆姨剑肯定少不了的,不管你技術(shù)再好厘惦,經(jīng)驗再豐富偷仿,代碼量最多只能優(yōu)化,該膨脹還是要膨脹的宵蕉,而且優(yōu)化之后代碼往往也比較難看酝静,使用各種奇技淫巧也是有代價的。所以国裳,針對代碼量優(yōu)化的結(jié)果形入,往往要么就是犧牲可讀性,要么就是犧牲可移植性(通用性)缝左,Every magic always needs a pay, you have to make a trade-off.亿遂。那么既然膨脹出來的代碼,或者將來有可能膨脹的代碼渺杉,不管放在MVC中的哪一個部分蛇数,最后都是要拆分的,既然遲早要拆分是越,那不如放Model里面耳舅,這樣將來拆分胖Model也能比拆分胖Cotroller更加容易。在我還在安居客的時候,安居客Pad app承載最復(fù)雜業(yè)務(wù)的ViewController才不到600行浦徊,其他多數(shù)Controller都是在300-400行之間馏予,這就為后面接手的人降低了非常多的上手難度和維護(hù)復(fù)雜度。拆分出來的東西都是可以直接遷移給iPhone app使用的】裕現(xiàn)在看天貓的ViewControler霞丧,動不動就幾千行,看不了多久頭就暈了冕香,問了一下蛹尝,大家都表示很習(xí)慣這樣的代碼長度,攤手悉尾。第四心法:架構(gòu)師是為業(yè)務(wù)工程師服務(wù)的突那,而不是去使喚業(yè)務(wù)工程師的架構(gòu)師在公司里的職級和地位往往都是要高于業(yè)務(wù)工程師的,架構(gòu)師的技術(shù)實力和經(jīng)驗往往也都是高于業(yè)務(wù)工程師的构眯。所以你值得在公司里獲得較高的地位愕难,但是在公司里的地位高不代表在軟件工程里面的角色地位也高。架構(gòu)師是要為業(yè)務(wù)工程師服務(wù)的鸵赖,是他們使喚你而不是你使喚他們务漩。另外,制定規(guī)范一方面是起到約束業(yè)務(wù)工程師的代碼它褪,但更重要的一點是饵骨,這其實是利用你的能力幫助業(yè)務(wù)工程師避免他無法預(yù)見的危機(jī),所以地位高有一定的好處茫打,畢竟夏蟲不可語冰居触,有的時候不見得能夠解釋得通,因此高地位隨之而來的就是說服力會比較強(qiáng)老赤。但在軟件工程里轮洋,一定要保持謙卑,一定要多為業(yè)務(wù)工程師考慮抬旺。一個不懂這個道理的架構(gòu)師弊予,設(shè)計出來的東西往往復(fù)雜難用,因為他只愿意做核心的東西开财,周邊不愿意做的都期望交給業(yè)務(wù)工程師去做汉柒,甚至有的時候就只做了個Demo,然后就交給業(yè)務(wù)工程師了责鳍,業(yè)務(wù)工程師變成給他打工的了碾褂。但是一個懂得這個道理的架構(gòu)師,設(shè)計出來的東西會非常好用历葛,業(yè)務(wù)方只需要扔很少的參數(shù)然后拿結(jié)果就好了正塌,這樣的架構(gòu)才叫好的架構(gòu)。舉一個保存圖片到本地的例子,一種做法是提供這樣的接口:- (NSString *)saveImageWithData:(NSData *)imageData乓诽,另一種是- (NSString *)saveImage:(UIImage *)image帜羊。后者更好,原因自己想鸠天。你的態(tài)度越謙卑逮壁,就越能設(shè)計出好的架構(gòu),這是我設(shè)計心法里的最后一條粮宛,也是最重要的一條。即使你現(xiàn)在技術(shù)實力不是業(yè)界大牛級別的卖宠,但只要保持這個心態(tài)去做架構(gòu)巍杈,去做設(shè)計,就已經(jīng)是合格的架構(gòu)師了扛伍,要成為業(yè)界大牛也會非晨昶瑁快。小總結(jié)其實針對View層的架構(gòu)設(shè)計刺洒,還是要做好三點:代碼規(guī)范鳖宾,架構(gòu)模式,工具集逆航。代碼規(guī)范對于View層來說意義重大鼎文,畢竟View層非常重業(yè)務(wù),如果代碼布局混亂因俐,后來者很難接手拇惋,也很難維護(hù)。架構(gòu)模式具體如何選擇抹剩,完全取決于業(yè)務(wù)復(fù)雜度撑帖。如果業(yè)務(wù)相當(dāng)相當(dāng)復(fù)雜,那就可以使用VIPER澳眷,如果相對簡單胡嘿,那就直接MVC稍微改改就好了。每一種已經(jīng)成為定式的架構(gòu)模式不見得都適合各自公司對應(yīng)的業(yè)務(wù)钳踊,所以需要各位架構(gòu)師根據(jù)情況去做一些拆分或者改變衷敌。拆分一般都不會出現(xiàn)問題,改變的時候箍土,只要別把MVC三個角色搞混就好了逢享,M該做啥做啥,C該做啥做啥吴藻,V該做啥做啥瞒爬,不要亂來。關(guān)于大部分的架構(gòu)模式應(yīng)該是什么樣子,這篇文章里都已經(jīng)說過了侧但,不過我認(rèn)為最重要的還是后面的心法矢空,模式只是招術(shù),熟悉了心法才能大巧不工禀横。View層的工具集主要還是集中在如何對View進(jìn)行布局费奸,以及一些特定的View,比如帶搜索提示的搜索框這種呆贿。這篇文章只提到了View布局的工具集杂腰,其它的工具集相對而言是更加取決于各自公司的業(yè)務(wù)的,各自實現(xiàn)或者使用CocoaPods里現(xiàn)成的都不是很難趾娃。對于小規(guī)溺缘眨或者中等規(guī)模iOS開發(fā)團(tuán)隊來說,做好以上三點就足夠了抬闷。在大規(guī)模團(tuán)隊中妇蛀,有一個額外問題要考慮,就是跨業(yè)務(wù)頁面調(diào)用方案的設(shè)計笤成∑兰埽跨業(yè)務(wù)頁面調(diào)用方案的設(shè)計跨業(yè)務(wù)頁面調(diào)用是指,當(dāng)一個App中存在A業(yè)務(wù)炕泳,B業(yè)務(wù)等多個業(yè)務(wù)時纵诞,B業(yè)務(wù)有可能會需要展示A業(yè)務(wù)的某個頁面,A業(yè)務(wù)也有可能會調(diào)用其他業(yè)務(wù)的某個頁面培遵。在小規(guī)模的App中挣磨,我們直接import其他業(yè)務(wù)的某個ViewController然后或者push或者present,是不會產(chǎn)生特別大的問題的荤懂。但是如果App的規(guī)模非常大茁裙,涉及業(yè)務(wù)數(shù)量非常多,再這么直接import就會出現(xiàn)問題节仿。? ? --------------? ? ? ? ? ? --------------? ? ? ? ? ? --------------? ? |? ? ? ? ? ? |? page call? |? ? ? ? ? ? |? page call? |? ? ? ? ? ? |? ? | Buisness A | <---------> | Buisness B | <---------> | Buisness C |? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? --------------? ? ? ? ? ? --------------? ? ? ? ? ? --------------? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? ? |? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? |? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? ? --------------------------------? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? App? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? --------------------------------可以看出晤锥,跨業(yè)務(wù)的頁面調(diào)用在多業(yè)務(wù)組成的App中會導(dǎo)致橫向依賴。那么像這樣的橫向依賴廊宪,如果不去設(shè)法解決矾瘾,會導(dǎo)致什么樣的結(jié)果?當(dāng)一個需求需要多業(yè)務(wù)合作開發(fā)時箭启,如果直接依賴壕翩,會導(dǎo)致某些依賴層上端的業(yè)務(wù)工程師在前期空轉(zhuǎn),依賴層下端的工程師任務(wù)繁重傅寡,而整個需求完成的速度會變慢放妈,影響的是團(tuán)隊開發(fā)迭代速度北救。當(dāng)要開辟一個新業(yè)務(wù)時,如果已有各業(yè)務(wù)間直接依賴芜抒,新業(yè)務(wù)又依賴某個舊業(yè)務(wù)珍策,就導(dǎo)致新業(yè)務(wù)的開發(fā)環(huán)境搭建困難,因為必須要把所有相關(guān)業(yè)務(wù)都塞入開發(fā)環(huán)境宅倒,新業(yè)務(wù)才能進(jìn)行開發(fā)攘宙。影響的是新業(yè)務(wù)的響應(yīng)速度。當(dāng)某一個被其他業(yè)務(wù)依賴的頁面有所修改時拐迁,比如改名蹭劈,涉及到的修改面就會特別大。影響的是造成任務(wù)量和維護(hù)成本都上升的結(jié)果线召。當(dāng)然链方,如果App規(guī)模特別小,這三點帶來的影響也會特別小灶搜,但是在阿里這樣大規(guī)模的團(tuán)隊中,像天貓/淘寶這樣大規(guī)模的App工窍,一旦遇上這里面哪怕其中一件事情割卖,就特么很坑爹。那么應(yīng)該怎樣處理這個問題患雏?讓依賴關(guān)系下沉鹏溯。怎么讓依賴關(guān)系下沉?引入Mediator模式淹仑。所謂引入Mediator模式來讓依賴關(guān)系下沉丙挽,實質(zhì)上就是每次呼喚頁面的時候,通過一個中間人來召喚另外一個頁面匀借,這樣只要每個業(yè)務(wù)依賴這個中間人就可以了颜阐,中間人的角色就可以放在業(yè)務(wù)層的下面一層,這就是依賴關(guān)系下沉吓肋。? ? --------------? ? ? ? ? ? --------------? ? ? ? ? ? --------------? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? | Buisness A |? ? ? ? ? ? | Buisness B |? ? ? ? ? ? | Buisness C |? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? --------------? ? ? ? ? ? --------------? ? ? ? ? ? --------------? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? /? 通過Mediater來召喚頁面? ? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? ? |? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? |? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? ? --------------------------------? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? Mediater? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? --------------------------------? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? --------------------------------? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? App? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? --------------------------------當(dāng)A業(yè)務(wù)需要調(diào)用B業(yè)務(wù)的某個頁面的時候凳怨,將請求交給Mediater,然后由Mediater通過某種手段獲取到B業(yè)務(wù)頁面的實例是鬼,交還給A就行了肤舞。在具體實現(xiàn)這個機(jī)制的過程中,有以下幾個問題需要解決:設(shè)計一套通用的請求機(jī)制均蜜,請求機(jī)制需要跟業(yè)務(wù)剝離李剖,使得不同業(yè)務(wù)的頁面請求都能夠被Mediater處理設(shè)計Mediater根據(jù)請求如何獲取其他業(yè)務(wù)的機(jī)制,Mediater需要知道如何處理請求囤耳,上哪兒去找到需要的頁面這個看起來就非常像我們web開發(fā)時候的URL機(jī)制篙顺,發(fā)送一個Get或Post請求偶芍,CGI調(diào)用腳本把請求分發(fā)給某個Controller下的某個Action,然后返回HTML字符串到瀏覽器去解析慰安。蘋果本身也實現(xiàn)了一套跨App調(diào)用機(jī)制腋寨,它也是基于URL機(jī)制來運(yùn)轉(zhuǎn)的,只不過它想要解決的問題是跨App的數(shù)據(jù)交流和頁面調(diào)用化焕,我們想要解決的問題是降低各業(yè)務(wù)的耦合度萄窜。不過我們還不能直接使用蘋果原生的這套機(jī)制,因為這套機(jī)制不能夠返回對象實例撒桨。而我們希望能夠拿到對象實例查刻,這樣不光可以做跨業(yè)務(wù)頁面調(diào)用,也可以做跨業(yè)務(wù)的功能調(diào)用凤类。另外穗泵,我們又希望我們的Mediater也能夠跟蘋果原生的跨App調(diào)用兼容,這樣就又能幫業(yè)務(wù)方省掉一部分開發(fā)量谜疤。就我目前所知道的情況佃延,AutoCad旗下某款iOS應(yīng)用(時間有點久我不記得是哪款應(yīng)用了,如果你是AutoCad的iOS開發(fā)夷磕,可以在評論區(qū)補(bǔ)充一下履肃。)就采用了這種頁面調(diào)用方式。天貓里面目前也在使用這套機(jī)制坐桩,只是這一塊由于歷史原因存在新老版本混用的情況尺棋,因此暫時還沒能夠很好地發(fā)揮應(yīng)有的作用。嗯绵跷,想問我要Demo的同學(xué)膘螟,我可以很大方地告訴你,沒有碾局。不過我打算抽時間寫一個出來荆残,現(xiàn)在除了已經(jīng)想好名字叫Summon以外,其它什么都沒做净当,哈哈脊阴。關(guān)于Getter和Setter?我比較習(xí)慣一個對象的"私有"屬性寫在extension里面蚯瞧,然后這些屬性的初始化全部放在getter里面做嘿期,在init和dealloc之外,是不會出現(xiàn)任何類似_property這樣的寫法的埋合。就是這樣:@interface CustomObject()@property (nonatomic, strong) UILabel *label;@end@implement#pragma mark - life cycle- (void)viewDidLoad{? ? [super viewDidLoad];? ? [self.view addSubview:self.label];}- (void)viewWillAppear:(BOOL)animated{? ? [super viewWillAppear:animated];? ? self.label.frame = CGRectMake(1, 2, 3, 4);}#pragma mark - getters and setters- (UILabel *)label{? ? if (_label == nil) {? ? ? ? _label = [[UILabel alloc] init];? ? ? ? _label.text = @"1234";? ? ? ? _label.font = [UIFont systemFontOfSize:12];? ? ? ? ... ...? ? }? ? return _label;}@end唐巧說他喜歡的做法是用_property這種备徐,然后關(guān)于_property的初始化通過[self setupProperty]這種做法去做。從剛才上面的代碼來看甚颂,就是要在viewDidLoad里面多調(diào)用一個setup方法而已蜜猾,然后我推薦的方法就是不用多調(diào)一個setup方法秀菱,直接走getter。嗯蹭睡,怎么說呢衍菱,其實兩種做法都能完成需求。但是從另一個角度看肩豁,蘋果之所以選擇讓[self getProperty]和self.property可以互相通用脊串,這種做法已經(jīng)很明顯地表達(dá)了蘋果的傾向:希望每個property都是通過getter方法來獲得。早在2003年清钥,Allen Holub就發(fā)了篇文章《Why getter and setter methods are evil》琼锋,自此之后,業(yè)界就對此產(chǎn)生了各種爭議祟昭,雖然是從Java開始說的缕坎,但是發(fā)展到后面各種語言也參與了進(jìn)來。然后雖然現(xiàn)在關(guān)于這個問題討論得少了篡悟,但是依舊屬于沒有定論的狀態(tài)谜叹。setter的情況比較復(fù)雜,也不是我這一節(jié)的重點搬葬,我這邊還是主要說getter荷腊。我們從objc的設(shè)計來看,蘋果的設(shè)計者更加傾向于getter is not evil踩萎。認(rèn)為getter is evil的原因有非常之多,或大或小很钓,隨著爭論的進(jìn)行香府,大家慢慢就聚焦到這樣的一個原因:Getter和Setter提供了一個能讓外部修改對象內(nèi)部數(shù)據(jù)的方式,這是evil的码倦,正常情況下企孩,一個對象自己私有的變量應(yīng)該是只有自己關(guān)心。然后我們回到iOS領(lǐng)域來袁稽,objc也同樣面臨了這樣的問題勿璃,甚至更加嚴(yán)重:objc并沒有像Java那么嚴(yán)格的私有概念。但在實際工作中推汽,我們不太會去操作頭文件里面沒有的變量补疑,這是從規(guī)范上就被禁止的。認(rèn)為getter is not evil的原因也可以聚焦到一個:高度的封裝性歹撒。getter事實上是工廠方法莲组,有了getter之后,業(yè)務(wù)邏輯可以更加專注于調(diào)用暖夭,而不必?fù)?dān)心當(dāng)前變量是否可用锹杈。我們可以想一下撵孤,假設(shè)一個ViewController有20個subview要加入view中,這20個subview的初始化代碼是肯定逃不掉的竭望,放在哪里比較好邪码?放在哪里都比放在addsubview的地方好,我個人認(rèn)為最好的地方還是放在getter里面咬清,結(jié)合單例模式之后闭专,代碼會非常整齊,生產(chǎn)的地方和使用的地方得到了很好的區(qū)分枫振。所以放到iOS來說喻圃,我還是覺得使用getter會比較好,因為evil的地方在iOS這邊基本都避免了粪滤,not evil的地方都能享受到斧拍,還是不錯的≌刃。總結(jié)要做一個View層架構(gòu)肆汹,主要就是從以下三方面入手:制定良好的規(guī)范選擇好合適的模式(MVC、MVCS予权、MVVM昂勉、VIPER)根據(jù)業(yè)務(wù)情況針對ViewController做好拆分,提供一些小工具方便開發(fā)當(dāng)然扫腺,你還會遇到其他的很多問題岗照,這時候你可以參考這篇文章里提出的心法,在后面提到的跨業(yè)務(wù)頁面調(diào)用方案的設(shè)計中笆环,你也能夠看到我的一些心法的影子攒至。對于iOS客戶端來說,它并不像其他語言諸如Python躁劣、PHP他們有那么多的非官方通用框架迫吐。客觀原因在于账忘,蘋果已經(jīng)為我們做了非常多的事情志膀,做了很多的努力。在蘋果已經(jīng)做了這么多事情的基礎(chǔ)上鳖擒,架構(gòu)師要做針對View層的方案時溉浙,最好還是盡量遵守蘋果已有的規(guī)范和設(shè)計思想,然后根據(jù)自己過去開發(fā)iOS時的經(jīng)驗蒋荚,盡可能給業(yè)務(wù)方在開發(fā)業(yè)務(wù)時減負(fù)放航,提高業(yè)務(wù)代碼的可維護(hù)性,就是View層架構(gòu)方案的最大目標(biāo)圆裕。2015-04-28 09:28補(bǔ):關(guān)于AOPAOP(Aspect Oriented Programming)广鳍,面向切片編程荆几,這也是面向XX編程系列術(shù)語之一哈,但它跟我們熟知的面向?qū)ο缶幊虥]什么關(guān)系赊时。什么是切片吨铸?程序要完成一件事情,一定會有一些步驟祖秒,1诞吱,2,3竭缝,4這樣房维。這里分解出來的每一個步驟我們可以認(rèn)為是一個切片。什么是面向切片編程抬纸?你針對每一個切片的間隙咙俩,塞一些代碼進(jìn)去,在程序正常進(jìn)行1湿故,2阿趁,3,4步的間隙可以跑到你塞進(jìn)去的代碼坛猪,那么你寫這些代碼就是面向切片編程脖阵。為什么會出現(xiàn)面向切片編程?你要想做到在每一個步驟中間做你自己的事情墅茉,不用AOP也一樣可以達(dá)到目的命黔,直接往步驟之間塞代碼就好了。但是事實情況往往很復(fù)雜就斤,直接把代碼塞進(jìn)去悍募,主要問題就在于:塞進(jìn)去的代碼很有可能是跟原業(yè)務(wù)無關(guān)的代碼,在同一份代碼文件里面摻雜多種業(yè)務(wù)战转,這會帶來業(yè)務(wù)間耦合搜立。為了降低這種耦合度以躯,我們引入了AOP槐秧。如何實現(xiàn)AOP?AOP一般都是需要有一個攔截器忧设,然后在每一個切片運(yùn)行之前和運(yùn)行之后(或者任何你希望的地方)刁标,通過調(diào)用攔截器的方法來把這個jointpoint扔到外面,在外面獲得這個jointpoint的時候址晕,執(zhí)行相應(yīng)的代碼膀懈。在iOS開發(fā)領(lǐng)域,objective-C的runtime有提供了一系列的方法谨垃,能夠讓我們攔截到某個方法的調(diào)用启搂,來實現(xiàn)攔截器的功能硼控,這種手段我們稱為Method Swizzling。Aspects通過這個手段實現(xiàn)了針對某個類和某個實例中方法的攔截胳赌。另外牢撼,也可以使用protocol的方式來實現(xiàn)攔截器的功能,具體實現(xiàn)方案就是這樣:@protocol RTAPIManagerInterceptor@optional- (void)manager:(RTAPIBaseManager *)manager beforePerformSuccessWithResponse:(AIFURLResponse *)response;- (void)manager:(RTAPIBaseManager *)manager afterPerformSuccessWithResponse:(AIFURLResponse *)response;- (void)manager:(RTAPIBaseManager *)manager beforePerformFailWithResponse:(AIFURLResponse *)response;- (void)manager:(RTAPIBaseManager *)manager afterPerformFailWithResponse:(AIFURLResponse *)response;- (BOOL)manager:(RTAPIBaseManager *)manager shouldCallAPIWithParams:(NSDictionary *)params;- (void)manager:(RTAPIBaseManager *)manager afterCallingAPIWithParams:(NSDictionary *)params;@end@interface RTAPIBaseManager : NSObject@property (nonatomic, weak) idinterceptor;
@end
這么做對比Method Swizzling有個額外好處就是疑苫,你可以通過攔截器來給攔截器的實現(xiàn)者提供更多的信息熏版,便于外部實現(xiàn)更加了解當(dāng)前切片的情況。另外捍掺,你還可以更精細(xì)地對切片進(jìn)行劃分撼短。Method Swizzling的切片粒度是函數(shù)粒度的,自己實現(xiàn)的攔截器的切片粒度可以比函數(shù)更小挺勿,更加精細(xì)曲横。
缺點就是,你得自己在每一個插入點把調(diào)用攔截器方法的代碼寫上(笑)满钟,通過Aspects(本質(zhì)上就是Mehtod Swizzling)來實現(xiàn)的AOP胜榔,就能輕松一些。
2015-4-29 14:25 補(bǔ):關(guān)于在哪兒寫Constraints湃番?
文章發(fā)出來之后夭织,很多人針對勘誤1有很多看法,以至于我覺得很有必要在這里做一份補(bǔ)吠撮。期間過程很多很復(fù)雜尊惰,這篇文章也已經(jīng)很長了,我就直接說結(jié)果了哈泥兰。
pic1
蘋果在文檔中指出弄屡,updateViewConstraints是用來做add constraints的地方。
但是在這里有一個回答者說updateViewConstraints并不適合做添加Constraints的事情鞋诗。
綜合我自己和評論區(qū)各位關(guān)心這個問題的兄弟們的各種測試和各種文檔膀捷,我現(xiàn)在覺得還是在viewDidLoad里面開一個layoutPageSubviews的方法,然后在這個里面創(chuàng)建Constraints并添加削彬,會比較好全庸。就是像下面這樣:
- (void)viewDidLoad
{
[super viewDidLoad];
[self.view addSubview:self.firstView];
[self.view addSubview:self.secondView];
[self.view addSubview:self.thirdView];
[self layoutPageSubviews];
}
- (void)layoutPageSubviews
{
[self.view addConstraints:xxxConstraints];
[self.view addConstraints:yyyConstraints];
[self.view addConstraints:zzzConstraints];
}
最后,要感謝評論區(qū)各位關(guān)心這個問題融痛,并提出自己意見壶笼,甚至是自己親自測試然后告訴我結(jié)果的各位兄弟:@fly2never,@Wythe雁刷,@wtlucky覆劈,@lcddhr,@李新星,@Meigan Fang责语,@匿名炮障,@Xiao Moch。
這個做法是目前我自己覺得可能比較合適的做法坤候,當(dāng)然也歡迎其他同學(xué)繼續(xù)拿出自己的看法铝阐,我們來討論。
勘誤
我的前同事@ddaajing看了這篇文章之后铐拐,給我提出了以下兩個勘誤徘键,和很多行文上的問題。在這里我對他表示非常感謝:
勘誤1:其實在viewWillAppear這里改變UI元素不是很可靠遍蟋,Autolayout發(fā)生在viewWillAppear之后吹害,嚴(yán)格來說這里通常不做視圖位置的修改,而用來更新Form數(shù)據(jù)虚青。改變位置可以放在viewWilllayoutSubview或者didLayoutSubview里它呀,而且在viewDidLayoutSubview確定UI位置關(guān)系之后設(shè)置autoLayout比較穩(wěn)妥。另外棒厘,viewWillAppear在每次頁面即將顯示都會調(diào)用纵穿,viewWillLayoutSubviews雖然在lifeCycle里調(diào)用順序在viewWillAppear之后,但是只有在頁面元素需要調(diào)整時才會調(diào)用奢人,避免了Constraints的重復(fù)添加谓媒。
勘誤2:MVVM要有ViewModel,以及ReactiveCocoa帶來的信號通知效果何乎,在ReactiveCocoa里就是RAC等相關(guān)宏來實現(xiàn)句惯。另外,使用ReactiveCocoa能夠比較優(yōu)雅地實現(xiàn)MVVM模式支救,就是因為有RAC等相關(guān)宏的存在抢野。就像它的名字一樣Reactive-響應(yīng)式,這也是區(qū)分MVVM的VM和MVC的C和MVP的P的一個重要方面各墨。
有任何問題建議直接在評論區(qū)提問指孤,這樣后來的人如果有相同的問題,就能直接找到答案了贬堵。提問之前也可以先看看評論區(qū)有沒有人問過類似問題了恃轩。
所有評論和問題我都會在第一時間回復(fù),QQ上我是不回答問題的哈扁瓢。
評論系統(tǒng)我用的是Disqus详恼,不定期被墻补君。所以如果你看到文章下面沒有加載出評論列表引几,翻個墻就有了。
本文遵守CC-BY。 請保持轉(zhuǎn)載后文章內(nèi)容的完整伟桅,以及文章出處敞掘。本人保留所有版權(quán)相關(guān)權(quán)利。
我的博客拒絕掛任何廣告楣铁,如果您覺得文章有價值玖雁,可以通過支付寶掃描下面的二維碼捐助我。