原文: ?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)談 開篇》出來之后,很多人來催我趕緊出第二篇。這一篇文章出得相當(dāng)艱難,因?yàn)楣纠锏钠剖聝禾貏e多,我自己又有點(diǎn)私事兒愤诱,以至于能用來寫博客的時(shí)間不夠充分≌纪瘢現(xiàn)在好啦赃阀,第二篇出來了以舒。當(dāng)我們開始設(shè)計(jì)View層的架構(gòu)時(shí)趾痘,往往是這個(gè)App還沒有開始開發(fā),或者這個(gè)App已經(jīng)發(fā)過幾個(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)都不見得是業(yè)務(wù)方能夠hold住的。這樣的情況缀辩,就要求我們?cè)趯?shí)現(xiàn)這個(gè)架構(gòu)時(shí)臭埋,代碼必須得改得勤快,不能偷懶臀玄。也必須抱著充分的自我懷疑態(tài)度瓢阴,做決策時(shí)要拿捏好尺度。View層的架構(gòu)非常之重要健无,在我看來荣恐,這部分架構(gòu)是這系列文章涉及4個(gè)方面最重要的一部分,沒有之一睬涧。為什么這么說募胃?View層架構(gòu)是影響業(yè)務(wù)方迭代周期的因素之一產(chǎn)品經(jīng)理產(chǎn)生需求的速度會(huì)非常快畦浓,尤其是公司此時(shí)仍處于創(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ù),編寫代碼的時(shí)候也能更加關(guān)注業(yè)務(wù)状勤。我跟一些朋友交流的時(shí)候鞋怀,他們都會(huì)或多或少地抱怨自己的團(tuán)隊(duì)迭代速度不夠快双泪,或者說,迭代速度不合理地慢密似。我認(rèn)為迭代速度不是想提就能提的焙矛,迭代速度的影響因素有很多,一期PRD里的任務(wù)量和任務(wù)復(fù)雜度都會(huì)影響迭代周期能達(dá)到什么樣的程度残腌。拋開這些外在的不談村斟,從內(nèi)在可能導(dǎo)致迭代周期達(dá)不到合理的速度的原因來看,其中有一個(gè)原因很有可能就是View層架構(gòu)沒有做好抛猫,讓業(yè)務(wù)工程師完成一個(gè)不算復(fù)雜的需求時(shí)蟆盹,需要處理太多額外的事情。當(dāng)然,開會(huì)多,工程師水平爛也屬于迭代速度提不上去的內(nèi)部原因足删,但這個(gè)不屬于本文討論范圍眨层。還有,加班不是優(yōu)化迭代周期的正確方式面哥,嗯哎壳。一般來說,一個(gè)不夠好的View層架構(gòu)尚卫,主要原因有以下五種:代碼混亂不規(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)生影響刹泄,但在我看來這里五個(gè)是比較重要的影響因素。如果大家覺得還有什么因素比這四個(gè)更高的怎爵,可以在評(píng)論區(qū)提出來我補(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è)問題灌侣,一方面要盡量避免單點(diǎn)問題推捐,讓架構(gòu)師做架構(gòu)的時(shí)候再帶一個(gè)人。另一方面侧啼,架構(gòu)要設(shè)計(jì)得盡量簡單牛柒,平緩接手人的學(xué)習(xí)曲線堪簿。我離開安居客的時(shí)候,做過保證:凡是從我手里出來的代碼焰络,終身保修戴甩。所以不要想著離職了就什么事兒都不管了,這不光是職業(yè)素養(yǎng)問題闪彼,還有一個(gè)是你對(duì)你的代碼是否足夠自信的問題甜孤。傳承性對(duì)于View層架構(gòu)非常重要,因?yàn)樗嚯x業(yè)務(wù)最近畏腕,改動(dòng)余地最小缴川。所以當(dāng)各位CTO、技術(shù)總監(jiān)描馅、TeamLeader們覺得迭代周期不夠快時(shí)把夸,你可以先不忙著急吼吼地去招新人,《人月神話》早就說過加人不能完全解決問題铭污。這時(shí)候如果你可以回過頭來看一下是不是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)雖然也算底層谈截,但還沒那么底層,它跟業(yè)務(wù)的對(duì)接面最廣涧偷,影響業(yè)務(wù)層代碼的程度也最深簸喂。在所有的底層都牽一發(fā)的時(shí)候,在View架構(gòu)上牽一發(fā)導(dǎo)致業(yè)務(wù)層動(dòng)全身的面積最大燎潮。所以View架構(gòu)在所有架構(gòu)中一旦定型喻鳄,可修改的空間就最小,我們?cè)谝婚_始考慮View相關(guān)架構(gòu)時(shí)确封,不光要實(shí)現(xiàn)功能诽表,還要考慮更多規(guī)范上的東西。制定規(guī)范的目的一方面是防止業(yè)務(wù)工程師的代碼腐蝕View架構(gòu)隅肥,另一方面也是為了能夠有所傳承竿奏。按照規(guī)范來,總還是不那么容易出差池的腥放。還有就是泛啸,架構(gòu)師一開始考慮的東西也會(huì)有很多,不可能在第一版就把它們?nèi)繉?shí)現(xiàn)秃症,對(duì)于一個(gè)尚未發(fā)版的App來說候址,第一版架構(gòu)往往是最小完整功能集吕粹,那么在第二版第三版的發(fā)展過程中,架構(gòu)的迭代任務(wù)就很有可能不只是你一個(gè)人的事情了岗仑,相信你一個(gè)人也不見得能搞定全部匹耕。所以你要跟你的合作者們有所約定。另外荠雕,第一版出去之后稳其,業(yè)務(wù)工程師在使用過程中也會(huì)產(chǎn)生很多修改意見,哪些意見是合理的炸卑,哪些意見是不合理的既鞠,也要通過事先約定的規(guī)范來進(jìn)行篩選,最終決定如何采納盖文。規(guī)范也不是一成不變的嘱蛋,什么時(shí)候槍斃意見,什么時(shí)候改規(guī)范五续,這就要靠各位的技術(shù)和經(jīng)驗(yàn)了洒敏。以上就是前言。這篇文章講什么疙驾?View代碼結(jié)構(gòu)的規(guī)定關(guān)于view的布局何時(shí)使用storyboard桐玻,何時(shí)使用nib,何時(shí)使用代碼寫View是否有必要讓業(yè)務(wù)方統(tǒng)一派生ViewController荆萤?方便View布局的小工具M(jìn)VC、MVVM铣卡、MVCS链韭、VIPER本門心法跨業(yè)務(wù)時(shí)View的處理留給評(píng)論區(qū)各種補(bǔ)總結(jié)View代碼結(jié)構(gòu)的規(guī)定架構(gòu)師不是寫SDK出來交付業(yè)務(wù)方使用就沒事兒了的,每家公司一定都有一套代碼規(guī)范煮落,架構(gòu)師的職責(zé)也包括定義代碼規(guī)范敞峭。按照道理來講,定代碼規(guī)范應(yīng)該是屬于通識(shí)蝉仇,放在這里講的原因只是因?yàn)槲疫@邊需要為View添加一個(gè)規(guī)范旋讹。制定代碼規(guī)范嚴(yán)格來講不屬于View層架構(gòu)的事情,但它對(duì)View層架構(gòu)未來的影響會(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é)里面我不打算從頭開始定義一套規(guī)范,蘋果有一套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)該差不多是這樣:pic1要點(diǎn)如下:所有的屬性都使用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)于這個(gè)做法锡搜,在唐巧的技術(shù)博客里面有一篇文章和我所提倡的做法不同,這個(gè)我會(huì)放在后面詳細(xì)論述瞧掺。getter和setter全部都放在最后因?yàn)橐粋€(gè)ViewController很有可能會(huì)有非常多的view耕餐,就像上面給出的代碼樣例一樣,如果getter和setter寫在前面辟狈,就會(huì)把主要邏輯扯到后面去肠缔,其他人看的時(shí)候就要先劃過一長串getter和setter,這樣不太好哼转。然后要求業(yè)務(wù)工程師寫代碼的時(shí)候按照順序來分配代碼塊的位置明未,先是life cycle,然后是Delegate方法實(shí)現(xiàn)壹蔓,然后是event response趟妥,然后才是getters and setters。這樣后來者閱讀代碼時(shí)就能省力很多佣蓉。每一個(gè)delegate都把對(duì)應(yīng)的protocol名字帶上披摄,delegate方法不要到處亂寫,寫到一塊區(qū)域里面去比如UITableViewDelegate的方法集就老老實(shí)實(shí)寫上#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專門開一個(gè)代碼區(qū)域所有button、gestureRecognizer的響應(yīng)事件都放在這個(gè)區(qū)域里面贞让,不要到處亂放周崭。關(guān)于private methods,正常情況下ViewController里面不應(yīng)該寫不是delegate方法的喳张,不是event response方法的续镇,不是life cycle方法的,就是private method了销部。對(duì)的摸航,正常情況下ViewController里面一般是不會(huì)存在private methods的,這個(gè)private methods一般是用于日期換算舅桩、圖片裁剪啥的這種小功能酱虎。這種小功能要么把它寫成一個(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í)候只是你用得到狰右,但是將來說不定別的地方也會(huì)用到杰捂,一開始就獨(dú)立出來,有利于將來的代碼復(fù)用棋蚌。為什么要這樣要求嫁佳?我見過無數(shù)ViewController,代碼布局亂得一塌糊涂谷暮,這里一個(gè)delegate那里一個(gè)getter蒿往,然后ViewController的代碼一般都死長死長的,看了就讓人頭疼坷备。定義好這個(gè)規(guī)范,就能使得ViewController條理清晰情臭,業(yè)務(wù)方程序員很能夠區(qū)分哪些放在ViewController里面比較合適省撑,哪些不合適。另外俯在,也可以提高代碼的可維護(hù)性和可讀性竟秫。關(guān)于View的布局業(yè)務(wù)工程師在寫View的時(shí)候一定逃不掉的就是這個(gè)命題。用Frame也好用Autolayout也好跷乐,如果沒有精心設(shè)計(jì)過肥败,布局部分一定慘不忍睹。直接使用CGRectMake的話可讀性很差,光看那幾個(gè)數(shù)字馒稍,也無法知道view和view之間的位置關(guān)系皿哨。用Autolayout可讀性稍微好點(diǎn)兒,但生成Constraint的長度實(shí)在太長纽谒,代碼觀感不太好证膨。Autolayout這邊可以考慮使用Masonry,代碼的可讀性就能好很多鼓黔。如果還有使用Frame的央勒,可以考慮一下使用這個(gè)項(xiàng)目。這個(gè)項(xiàng)目里面提供了Frame相關(guān)的方便方法(UIView+LayoutMethods)澳化,里面的方法也基本涵蓋了所有布局的需求崔步,可讀性非常好,使用它之后基本可以和CGRectMake說再見了缎谷。因?yàn)樘熵堅(jiān)谧罱徘袚Q到支持iOS6井濒,所以之前天貓都是用Frame布局的,在天貓App中慎陵,首頁眼虱,范兒部分頁面的布局就使用了這些方法。使用這些方便方法能起到事半功倍的效果席纽。這個(gè)項(xiàng)目也提供了Autolayout方案下生產(chǎn)Constraints的方便方法(UIView+AEBHandyAutoLayout)捏悬,可讀性比原生好很多。我當(dāng)時(shí)在寫這系列方法的時(shí)候還不知道有Masonry润梯。知道有Masonry之后我特地去看了一下过牙,發(fā)現(xiàn)Masonry功能果然強(qiáng)大。不過這系列方法雖然沒有Masonry那么強(qiáng)大纺铭,但是也夠用了寇钉。當(dāng)時(shí)安居客iPad版App全部都是Autolayout來做的View布局,就是使用的這個(gè)項(xiàng)目里面的方法舶赔∩ǔ可讀性很好。讓業(yè)務(wù)工程師使用良好的工具來做View的布局竟纳,能提高他們的工作效率撵溃,也能減少bug發(fā)生的幾率。架構(gòu)師不光要關(guān)心那些高大上的內(nèi)容锥累,也要多給業(yè)務(wù)工程師提供方便易用的小工具缘挑,才能發(fā)揮架構(gòu)師的價(jià)值。何時(shí)使用storyboard桶略,何時(shí)使用nib语淘,何時(shí)使用代碼寫View這個(gè)問題唐巧的博客里這篇文章也提到過诲宇,我的意見和他是基本一致的。在這里我還想補(bǔ)充一些內(nèi)容:具有一定規(guī)模的團(tuán)隊(duì)化iOS開發(fā)(10人以上)有以下幾個(gè)特點(diǎn):同一份代碼文件的作者會(huì)有很多惶翻,不同作者同時(shí)修改同一份代碼的情況也不少見姑蓝。因此,使用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)畫場景的開發(fā)任務(wù)比較多垃沦。如果這三個(gè)特點(diǎn)你一看就明白了客给,下面的解釋就可以不用看了。如果你針對(duì)我的傾向愿意進(jìn)一步討論的肢簿,可以先看我下面的解釋靶剑,看完再說。同一份代碼文件的作者會(huì)有很多池充,不同作者同時(shí)修改同一份代碼的情況也不少見桩引。因此,使用Git進(jìn)行代碼版本管理時(shí)出現(xiàn)Conflict的幾率也比較大收夸。iOS開發(fā)過程中坑匠,會(huì)遇到最蛋疼的兩種Conflict一個(gè)是project.pbxproj,另外一個(gè)就是StoryBoard或XIB卧惜。因?yàn)檫@些文件的內(nèi)容的可讀性非常差厘灼,雖然蘋果在XCode5(現(xiàn)在我有點(diǎn)不確定是不是這個(gè)版本了)中對(duì)StoryBoard的文件描述方式做了一定的優(yōu)化,但只是把可讀性從非常差提升為很差咽瓷。然而在StoryBoard中往往包含了多個(gè)頁面设凹,這些頁面基本上不太可能都由一個(gè)人去完成,如果另一個(gè)人在做StoryBoard的操作的時(shí)候茅姜,出于某些目的動(dòng)了一下不屬于他的那個(gè)頁面闪朱,比如為了美觀調(diào)整了一下位置。然后另外一個(gè)人也因?yàn)橐砑右粋€(gè)頁面钻洒,而在Storyboard中調(diào)整了一下某個(gè)其他頁面的位置奋姿。那么針對(duì)這個(gè)情況我除了說個(gè)呵呵以外,我就只能說:祝你好運(yùn)航唆≌吐看清楚哦院刁,這還沒動(dòng)具體的頁頁面內(nèi)容呢糯钙。但如果使用代碼繪制View,Conflict一樣會(huì)發(fā)生,但是這種Conflict就好解很多了任岸,你懂的再榄。需求變化非常頻繁,產(chǎn)品經(jīng)理一時(shí)一個(gè)主意享潜,為了完成需求而針對(duì)現(xiàn)有代碼進(jìn)行微調(diào)的情況困鸥,以及針對(duì)現(xiàn)有代碼的部分復(fù)用的情況也比較多。我覺得產(chǎn)品經(jīng)理一時(shí)一個(gè)主意不是他的錯(cuò)剑按,他說不定也是被逼的疾就,比如誰都會(huì)來摻和一下產(chǎn)品的設(shè)計(jì),公司里的所有人艺蝴,上至CEO猬腰,下至基層員工都有可能對(duì)產(chǎn)品設(shè)計(jì)評(píng)頭論足,只要他個(gè)人有個(gè)地方用得不爽(極大可能是個(gè)人喜好)然后又正好跟產(chǎn)品經(jīng)理比較熟悉能夠搭得上話猜敢,都會(huì)提出各種意見姑荷。產(chǎn)品經(jīng)理躲不起也惹不起,有時(shí)也是沒辦法缩擂,嗯鼠冕。但落實(shí)到工程師這邊來,這種情況就很蛋疼胯盯。因?yàn)檫@種改變有時(shí)候不光是UI懈费,UI所對(duì)應(yīng)的邏輯也有要改的可能,工程師就會(huì)兩邊文件都改陨闹,你原來link的那個(gè)view現(xiàn)在不link了楞捂,然后你的outlet對(duì)應(yīng)也要?jiǎng)h掉,這兩部分只要有一個(gè)沒做趋厉,編譯通過之后跑一下App寨闹,一會(huì)兒就crash了【耍看起來這不是什么大事兒繁堡,但很影響心情。另外乡数,如果出現(xiàn)部分的代碼復(fù)用椭蹄,比如說某頁面下某個(gè)View也希望放在另外一個(gè)頁面里,相關(guān)的操作就不是復(fù)制粘貼這么簡單了净赴,你還得重新link一遍绳矩。也很影響心情。復(fù)雜界面元素玖翅,復(fù)雜動(dòng)畫交互場景的開發(fā)任務(wù)比較多翼馆。要是想在基于StoryBoard的項(xiàng)目中做一個(gè)動(dòng)畫割以,很煩。做幾個(gè)復(fù)雜界面元素应媚,也很煩严沥。有的時(shí)候我們掛Custom View上去,其實(shí)在StoryBoard里面看來就是一個(gè)空白View中姜。然后另外一點(diǎn)就是消玄,當(dāng)你的layout出現(xiàn)問題需要調(diào)整的時(shí)候,還是挺難找到問題所在的丢胚,尤其是在復(fù)雜界面元素的情況下翩瓜。所以在針對(duì)View層這邊的要求時(shí),我也是建議不要用StoryBoard携龟。實(shí)現(xiàn)簡單的東西奥溺,用Code一樣簡單,實(shí)現(xiàn)復(fù)雜的東西骨宠,Code比StoryBoard更簡單浮定。所以我更加提倡用code去畫view而不是storyboard。是否有必要讓業(yè)務(wù)方統(tǒng)一派生ViewController有的時(shí)候我們出于記錄用戶操作行為數(shù)據(jù)的需要层亿,或者統(tǒng)一配置頁面的目的桦卒,會(huì)從UIViewController里面派生一個(gè)自己的ViewController,來執(zhí)行一些通用邏輯匿又。比如天貓客戶端要求所有的ViewController都要繼承自TMViewController方灾。這個(gè)統(tǒng)一的父類里面針對(duì)一個(gè)ViewController的所有生命周期都做了一些設(shè)置哑舒,至于這里都有哪些設(shè)置對(duì)于本篇文章來說并不重要谬盐。在這里我想討論的是禀苦,在設(shè)計(jì)View架構(gòu)時(shí)毛肋,如果為了能夠達(dá)到統(tǒng)一設(shè)置或執(zhí)行統(tǒng)一邏輯的目的,使用派生的手段是有必要的嗎撰茎?我覺得沒有必要汤求,為什么沒有必要挨稿?使用派生比不使用派生更容易增加業(yè)務(wù)方的使用成本不使用派生手段一樣也能達(dá)到統(tǒng)一設(shè)置的目的這兩條原因是我認(rèn)為沒有必要使用派生手段的理由旭绒,如果兩條理由你都心領(lǐng)神會(huì)鸟妙,那么下面的就可以不用看了。如果你還有點(diǎn)疑惑挥吵,請(qǐng)看下面我來詳細(xì)講一下原因重父。為什么使用了派生,業(yè)務(wù)方的使用成本會(huì)提升忽匈?其實(shí)不光是業(yè)務(wù)方的使用成本房午,架構(gòu)的維護(hù)成本也會(huì)上升。那么具體的成本都來自于哪里呢丹允?集成成本這里講的集成成本是這樣的:如果業(yè)務(wù)方自己開了一個(gè)獨(dú)立demo郭厌,快速完成了某個(gè)獨(dú)立流程嗦锐,現(xiàn)在他想把這個(gè)現(xiàn)有流程集合進(jìn)去。那么問題就來了沪曙,他需要把所有獨(dú)立的UIViewController改變成TMViewController。那為什么不是一開始就立刻使用TMViewController呢萎羔?因?yàn)橐胍隩MViewController液走,就要引入整個(gè)天貓App所有的業(yè)務(wù)線,所有的基礎(chǔ)庫贾陷,因?yàn)檫@個(gè)父類里面涉及很多天貓環(huán)境才有的內(nèi)容缘眶,所謂拔出蘿卜帶出泥,你要是想簡單繼承一下就能搞定的事情髓废,搭環(huán)境就要搞半天巷懈,然后這個(gè)小Demo才能跑得起來。對(duì)于業(yè)務(wù)層存在的所有父類來說慌洪,它們是很容易跟項(xiàng)目中的其他代碼糾纏不清的顶燕,這使得業(yè)務(wù)方開發(fā)時(shí)遇到一個(gè)兩難問題:要么把所有依賴全部搞定,然后基于App環(huán)境(比如天貓)下開發(fā)Demo冈爹,要么就是自己Demo寫好之后涌攻,按照環(huán)境要求改代碼。這里的兩難問題都會(huì)帶來成本频伤,都會(huì)影響業(yè)務(wù)方的迭代進(jìn)度恳谎。我不確定各位所在公司是否會(huì)有這樣的情況,但我可以在這里給大家舉一個(gè)我在阿里的真實(shí)的例子:我最近在開發(fā)某濾鏡Demo和相關(guān)頁面流程憋肖,最終是要合并到天貓這個(gè)App里面去的因痛。使用天貓環(huán)境進(jìn)行開發(fā)的話,pod install完所有依賴差不多需要10分鐘岸更,然后打開workspace之后鸵膏,差不多要再等待1分鐘讓xcode做好索引,然后才能正式開始工作怎炊。在這里要感謝一下則平较性,因?yàn)樗诖嘶A(chǔ)上做了很多優(yōu)化,使得這個(gè)1分鐘已經(jīng)比原來的時(shí)間短很多了结胀。但如果天貓環(huán)境有更新赞咙,你就要再重復(fù)一次上面的流程,否則 就很有可能編譯不過糟港。拜托攀操,我只是想做個(gè)Demo而已,不想搞那么復(fù)雜秸抚。上手接受成本新來的業(yè)務(wù)工程師有的時(shí)候不見得都記得每一個(gè)ViewController都必須要派生自TMViewController而不是直接的UIViewController速和。新來的工程師他不能直接按照蘋果原生的做法去做事情歹垫,他需要額外學(xué)習(xí),比如說:所有的ViewController都必須繼承自TMViewController颠放。架構(gòu)的維護(hù)難度盡可能少地使用繼承能提高項(xiàng)目的可維護(hù)性排惨,具體內(nèi)容我在《跳出面向?qū)ο笏枷耄ㄒ唬?繼承》里面說了,在這里我想偷懶不想把那篇文章里說過的東西再說一遍碰凶。其實(shí)對(duì)于業(yè)務(wù)方來說暮芭,主要還是第一個(gè)集成成本比較蛋疼,因?yàn)檫@是長痛欲低,每次要做點(diǎn)什么事情都會(huì)遇到辕宏。第二點(diǎn)倒還好,短痛砾莱。第三點(diǎn)跟業(yè)務(wù)工程師沒啥關(guān)系瑞筐。那么如果不使用派生,我們應(yīng)該使用什么手段腊瑟?我的建議是使用AOP聚假。在架構(gòu)師實(shí)現(xiàn)具體的方案之前,必須要想清楚幾個(gè)問題闰非,然后才能決定采用哪種方案魔策。是哪幾個(gè)問題?方案的效果河胎,和最終要達(dá)到的目的是什么闯袒?在自己的知識(shí)體系里面,是否具備實(shí)現(xiàn)這個(gè)方案的能力游岳?在業(yè)界已有的開源組件里面政敢,是否有可以直接拿來用的輪子?這三個(gè)問題按照順序一一解答之后胚迫,具體方案就能出來了喷户。我們先看第一個(gè)問題:方案的效果,和最終要達(dá)到的目的是什么访锻?方案的效果應(yīng)該是:業(yè)務(wù)方可以不用通過繼承的方法褪尝,然后框架能夠做到對(duì)ViewController的統(tǒng)一配置。業(yè)務(wù)方即使脫離框架環(huán)境期犬,不需要修改任何代碼也能夠跑完代碼河哑。業(yè)務(wù)方的ViewController一旦丟入框架環(huán)境,不需要修改任何代碼龟虎,框架就能夠起到它應(yīng)該起的作用璃谨。其實(shí)就是要實(shí)現(xiàn)不通過業(yè)務(wù)代碼上對(duì)框架的主動(dòng)迎合,使得業(yè)務(wù)能夠被框架感知這樣的功能。細(xì)化下來就是兩個(gè)問題佳吞,框架要能夠攔截到ViewController的生命周期拱雏,另一個(gè)問題就是,攔截的定義時(shí)機(jī)底扳。對(duì)于方法攔截铸抑,很容易想到Method Swizzling,那么我們可以寫一個(gè)實(shí)例衷模,在App啟動(dòng)的時(shí)候添加針對(duì)UIViewController的方法攔截鹊汛,這是一種做法。還有另一種做法就是算芯,使用NSObject的load函數(shù),在應(yīng)用啟動(dòng)時(shí)自動(dòng)監(jiān)聽凳宙。使用后者的好處在于熙揍,這個(gè)模塊只要被項(xiàng)目包含,就能夠發(fā)揮作用氏涩,不需要在項(xiàng)目里面添加任何代碼届囚。然后另外一個(gè)要考慮的事情就是,原有的TMViewController(所謂的父類)也是會(huì)提供額外方法方便子類使用的是尖,Method Swizzling只支持針對(duì)現(xiàn)有方法的操作意系,拓展方法的話,嗯饺汹,當(dāng)然是用Category啦蛔添。我本人不贊成Category的過度使用,但鑒于Category是最典型的化繼承為組合的手段兜辞,在這個(gè)場景下還是適合使用的迎瞧。還有的就是,關(guān)于Method Swizzling手段實(shí)現(xiàn)方法攔截逸吵,業(yè)界也已經(jīng)有了現(xiàn)成的開源庫:Aspects凶硅,我們可以直接拿來使用。我這邊有個(gè)非常非常小的Demo可以放出來給大家扫皱,這個(gè)Demo只是一個(gè)點(diǎn)睛之筆足绅,有一些話我也寫在這個(gè)Demo里面了,各位架構(gòu)師們你們可以基于各自公司App的需求去拓展韩脑。這個(gè)Demo不包含Category氢妈,畢竟Category還是得你們自己去寫啊~然后這套方案能夠完成原來通過派生手段所有可以完成的任務(wù),但同時(shí)又允許業(yè)務(wù)方不必添加任何代碼段多,直接使用原生的UIViewController允懂。然后另外要提醒的是,這方案的目的是消除不必要的繼承衩匣,雖然不限定于UIViewController蕾总,但它也是有適用范圍的粥航,在適用繼承的地方,還是要老老實(shí)實(shí)使用繼承生百。比如你有一個(gè)數(shù)據(jù)模型递雀,是由基本模型派生出的一整套模型,那么這個(gè)時(shí)候還是老老實(shí)實(shí)使用繼承蚀浆。至于拿捏何時(shí)使用繼承缀程,相信各位架構(gòu)師一定能夠處理好,或者你也可以參考我前面提到的那篇文章來控制拿捏的尺度市俊。關(guān)于MVC杨凑、MVVM等一大堆思想其實(shí)這些都是相對(duì)通用的思想,萬變不離其宗的還是在開篇里面我提到的那三個(gè)角色:數(shù)據(jù)管理者摆昧,數(shù)據(jù)加工者撩满,數(shù)據(jù)展示者。這些五花八門的思想绅你,不外乎就是制訂了一個(gè)規(guī)范伺帘,規(guī)定了這三個(gè)角色應(yīng)當(dāng)如何進(jìn)行數(shù)據(jù)交換。但同時(shí)這些也是爭議最多的話題忌锯,所以我在這里來把幾個(gè)主流思想做一個(gè)梳理伪嫁,當(dāng)你在做View層架構(gòu)時(shí),能夠有個(gè)比較好的參考偶垮。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)了一個(gè)數(shù)據(jù)流調(diào)配的功能婚陪。正在我寫這篇文章的時(shí)候,我看到InfoQ發(fā)了這篇文章频祝,里面提到了一個(gè)移動(dòng)開發(fā)中的痛點(diǎn)是:對(duì)MVC架構(gòu)劃分的理解泌参。我當(dāng)時(shí)沒能夠去參加這個(gè)座談會(huì),也沒辦法發(fā)表個(gè)人意見常空,所以就只能在這里寫寫了沽一。在iOS開發(fā)領(lǐng)域,我們應(yīng)當(dāng)如何進(jìn)行MVC的劃分漓糙?這里面其實(shí)有兩個(gè)問題:為什么我們會(huì)糾結(jié)于iOS開發(fā)領(lǐng)域中MVC的劃分問題铣缠?在iOS開發(fā)領(lǐng)域中,怎樣才算是劃分的正確姿勢?為什么我們會(huì)糾結(jié)于iOS開發(fā)領(lǐng)域中MVC的劃分問題蝗蛙?關(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è)猜測來給出我的意見喷屋。在服務(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不會(huì)去做任何關(guān)于View的事情是牢,包括生成View僵井,這些都由渲染引擎代勞了陕截。這是一個(gè)區(qū)別驳棱,但其實(shí)服務(wù)端View的概念和Native應(yīng)用View的概念,真正的區(qū)別在于:從概念上嚴(yán)格劃分的話农曲,服務(wù)端其實(shí)根本沒有View社搅,拜HTTP協(xié)議所賜,我們平時(shí)所討論的View只是用于描述View的字符串(更實(shí)質(zhì)的應(yīng)該稱之為數(shù)據(jù))乳规,真正的View是瀏覽器形葬。。所以服務(wù)端只管生成對(duì)View的描述暮的,至于對(duì)View的長相笙以,UI事件監(jiān)聽和處理,都是瀏覽器負(fù)責(zé)生成和維護(hù)的冻辩。但是在Native這邊來看猖腕,原本屬于瀏覽器的任務(wù)也逃不掉要自己做。那么這件事情由誰來做最合適恨闪?蘋果給出的答案是:UIViewController倘感。鑒于蘋果在這一層做了很多艱苦卓絕的努力,讓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.viewContainer addSubview:self.label];? ? [self.viewContainer addSubview:self.tableView];? ? [self.viewContainer addSubview:self.button];? ? [self.viewContainer addSubview:self.textField];}... ...僅僅改了個(gè)名字威根,現(xiàn)在是不是感覺清晰了很多?如果再要說詳細(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ù)端開發(fā)在這個(gè)概念下佑淀,其實(shí)只涉及M和C的開發(fā)工作留美,瀏覽器作為View的容器,負(fù)責(zé)View的展示和事件的監(jiān)聽伸刃。那么對(duì)應(yīng)到iOS客戶端的MVC劃分上面來谎砾,就是這樣:? ? ? ? ? ? ? ----------------------------? ? ? ? ? ? ? | 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é),我也覺得蘋果做的這個(gè)選擇是非常正確明智的扣典。因?yàn)闉g覽器和服務(wù)端之間的關(guān)系非常松散妆毕,而且他們分屬于兩個(gè)不同陣營,服務(wù)端將對(duì)View的描述生成之后贮尖,交給瀏覽器去負(fù)責(zé)展示笛粘,然而一旦view上有什么事件產(chǎn)生,基本上是很少傳遞到服務(wù)器(也就是所謂的Controller)的(要傳也可以:AJAX)湿硝,都是在瀏覽器這邊把事情都做掉薪前,所以在這種情況下,View容器就適合放在瀏覽器(V)這邊图柏。但是在iOS開發(fā)領(lǐng)域序六,雖然也有讓View去監(jiān)聽事件的做法,但這種做法非常少蚤吹,都是把事件回傳給Controller例诀,然后Controller再另行調(diào)度随抠。所以這時(shí)候,View的容器放在Controller就非常合適繁涂。Controller可以因?yàn)椴煌录漠a(chǎn)生去很方便地更改容器內(nèi)容拱她,比如加載失敗時(shí),把容器內(nèi)容換成失敗頁面的View扔罪,無網(wǎng)絡(luò)時(shí)秉沼,把容器頁面換成無網(wǎng)絡(luò)的View等等。在iOS開發(fā)領(lǐng)域中矿酵,怎樣才算是MVC劃分的正確姿勢唬复?這個(gè)問題其實(shí)在上面已經(jīng)解答掉一部分了,那么這個(gè)問題的答案就當(dāng)是對(duì)上面問題的一個(gè)總結(jié)吧全肮。M應(yīng)該做的事:給ViewController提供數(shù)據(jù)給ViewController存儲(chǔ)數(shù)據(jù)提供接口提供經(jīng)過抽象的業(yè)務(wù)基本組件敞咧,供Controller調(diào)度C應(yīng)該做的事:管理View Container的生命周期負(fù)責(zé)生成所有的View實(shí)例,并放入View Container監(jiān)聽來自View與業(yè)務(wù)有關(guān)的事件辜腺,通過與Model的合作休建,來完成對(duì)應(yīng)事件的業(yè)務(wù)。V應(yīng)該做的事:響應(yīng)與業(yè)務(wù)無關(guān)的事件评疗,并因此引發(fā)動(dòng)畫效果测砂,點(diǎn)擊反饋(如果合適的話,盡量還是放在View去做)等百匆。界面元素表達(dá)我通過與服務(wù)端MVC劃分的對(duì)比來回答了這兩個(gè)問題砌些,之所以這么做,是因?yàn)槲抑烙泻芏鄆OS工程師之前是從服務(wù)端轉(zhuǎn)過來的胧华。我也是這樣寄症,在進(jìn)安居客之前宙彪,我也是做服務(wù)端開發(fā)的矩动,在學(xué)習(xí)iOS的過程中,我也曾經(jīng)對(duì)iOS領(lǐng)域的MVC劃分問題產(chǎn)生過疑惑释漆,我疑惑的點(diǎn)就是前面開篇我猜測的點(diǎn)悲没。如果有人問我iOS中應(yīng)該怎么做MVC的劃分,我就會(huì)像上面這么回答男图。MVCS蘋果自身就采用的是這種架構(gòu)思路示姿,從名字也能看出,也是基于MVC衍生出來的一套架構(gòu)逊笆。從概念上來說栈戳,它拆分的部分是Model部分,拆出來一個(gè)Store难裆。這個(gè)Store專門負(fù)責(zé)數(shù)據(jù)存取子檀。但從實(shí)際操作的角度上講镊掖,它拆開的是Controller。這算是瘦Model的一種方案褂痰,瘦Model只是專門用于表達(dá)數(shù)據(jù)亩进,然后存儲(chǔ)、數(shù)據(jù)處理都交給外面的來做缩歪。MVCS使用的前提是归薛,它假設(shè)了你是瘦Model,同時(shí)數(shù)據(jù)的存儲(chǔ)和處理都在Controller去做匪蝙。所以對(duì)應(yīng)到MVCS主籍,它在一開始就是拆分的Controller。因?yàn)镃ontroller做了數(shù)據(jù)存儲(chǔ)的事情逛球,就會(huì)變得非常龐大崇猫,那么就把Controller專門負(fù)責(zé)存取數(shù)據(jù)的那部分抽離出來,交給另一個(gè)對(duì)象去做需忿,這個(gè)對(duì)象就是Store诅炉。這么調(diào)整之后,整個(gè)結(jié)構(gòu)也就變成了真正意義上的MVCS屋厘。關(guān)于胖Model和瘦Model我在面試和跟別人聊天時(shí)涕烧,發(fā)現(xiàn)知道胖Model和瘦Model的概念的人不是很多。大約兩三年前國外業(yè)界曾經(jīng)對(duì)此有過非常激烈的討論汗洒,主題就是Fat model, skinny controller∫榇浚現(xiàn)在關(guān)于這方面的討論已經(jīng)不多了,然而直到今天胖Model和瘦Model哪個(gè)更好溢谤,業(yè)界也還沒有定論瞻凤,所以這算是目前業(yè)界懸而未解的一個(gè)爭議。我很少看到國內(nèi)有討論這個(gè)的資料世杀,所以在這里我打算補(bǔ)充一下什么叫胖Model什么叫瘦Model阀参。以及他們的爭論來源于何處。什么叫胖Model瞻坝?胖Model包含了部分弱業(yè)務(wù)邏輯蛛壳。胖Model要達(dá)到的目的是,Controller從胖Model這里拿到數(shù)據(jù)之后所刀,不用額外做操作或者只要做非常少的操作衙荐,就能夠?qū)?shù)據(jù)直接應(yīng)用在View上。舉個(gè)例子:Raw Data:? ? timestamp:1234567FatModel:? ? @property (nonatomic, assign) CGFloat timestamp;? ? - (NSString *)ymdDateString; // 2015-04-20 15:16? ? - (NSString *)gapString; // 3分鐘前浮创、1小時(shí)前忧吟、一天前、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ù)變動(dòng)的可能性要比弱業(yè)務(wù)大得多摧玫,弱業(yè)務(wù)相對(duì)穩(wěn)定耳奕,所以弱業(yè)務(wù)塞進(jìn)Model里面是沒問題的。另一方面诬像,弱業(yè)務(wù)重復(fù)出現(xiàn)的頻率要大于強(qiáng)業(yè)務(wù)屋群,對(duì)復(fù)用性的要求更高,如果這部分業(yè)務(wù)寫在Controller坏挠,類似的代碼會(huì)灑得到處都是芍躏,一旦弱業(yè)務(wù)有修改(弱業(yè)務(wù)修改頻率低不代表就沒有修改),這個(gè)事情就是一個(gè)災(zāi)難降狠。如果塞到Model里面去对竣,改一處很多地方就能跟著改春寿,就能避免這場災(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ì)成長的,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類或方法來對(duì)弱業(yè)務(wù)做抽象,強(qiáng)業(yè)務(wù)依舊交給Controller荐吵。舉個(gè)例子: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ù)可以交給任何一個(gè)能處理它數(shù)據(jù)的Helper或其他的對(duì)象赊瞬,來完成業(yè)務(wù)。在代碼遷移的時(shí)候獨(dú)立性很強(qiáng)贼涩,很少會(huì)出現(xiàn)拔出蘿卜帶出泥的情況巧涧。另外,由于SlimModel只是數(shù)據(jù)表達(dá)遥倦,對(duì)它進(jìn)行維護(hù)基本上是0成本谤绳,軟件膨脹得再厲害,SlimModel也不會(huì)大到哪兒去袒哥。缺點(diǎn)就在于缩筛,Helper這種做法也不見得很好,這里有一篇文章批判了這個(gè)事情堡称。另外瞎抛,由于Model的操作會(huì)出現(xiàn)在各種地方,SlimModel在一定程度上違背了DRY(Don't Repeat Yourself)的思路却紧,Controller仍然不可避免在一定程度上出現(xiàn)代碼膨脹桐臊。我的態(tài)度?嗯晓殊,我會(huì)在本門心法這一節(jié)里面說豪硅。說回來,MVCS是基于瘦Model的一種架構(gòu)思路挺物,把原本Model要做的很多事情中的其中一部分關(guān)于數(shù)據(jù)存儲(chǔ)的代碼抽象成了Store懒浮,在一定程度上降低了Controller的壓力。MVVMMVVM去年在業(yè)界討論得非常多识藤,無論國內(nèi)還是國外都討論得非常熱烈砚著,尤其是在ReactiveCocoa這個(gè)庫成熟之后,ViewModel和View的信號(hào)機(jī)制在iOS下終于有了一個(gè)相對(duì)優(yōu)雅的實(shí)現(xiàn)痴昧。MVVM本質(zhì)上也是從MVC中派生出來的思想稽穆,MVVM著重想要解決的問題是盡可能地減少Controller的任務(wù)。不管MVVM也好赶撰,MVCS也好舌镶,他們的共識(shí)都是Controller會(huì)隨著軟件的成長,變很大很難維護(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)于這個(gè)觀點(diǎn)我要做一個(gè)額外解釋:胖Model做的事情是先為Controller減負(fù)扣汪,然后由于Model變胖断楷,再在此基礎(chǔ)上拆出ViewModel,跟業(yè)界普遍認(rèn)知的MVVM本質(zhì)上是為Controller減負(fù)這個(gè)說法并不矛盾崭别,因?yàn)榕諱odel做的事情也是為Controller減負(fù)脐嫂。另外,我前面說MVVM把數(shù)據(jù)加工的任務(wù)從Controller中解放出來紊遵,跟MVVM拆分的是胖Model也不矛盾账千。要做到解放Controller,首先你得有個(gè)胖Model暗膜,然后再把這個(gè)胖Model拆成Model和ViewModel匀奏。那么MVVM究竟應(yīng)該如何實(shí)現(xiàn)?這很有可能是大多數(shù)人糾結(jié)的問題学搜,我打算憑我的個(gè)人經(jīng)驗(yàn)試圖在這里回答這個(gè)問題娃善,歡迎大家在評(píng)論區(qū)交流。在iOS領(lǐng)域大部分MVVM架構(gòu)都會(huì)使用ReactiveCocoa瑞佩,但是使用ReactiveCocoa的iOS應(yīng)用就是基于MVVM架構(gòu)的嗎聚磺?那當(dāng)然不是,我覺得很多人都存在這個(gè)誤區(qū)炬丸,我面試過的一些人提到了ReactiveCocoa也提到了MVVM瘫寝,但他們對(duì)此的理解膚淺得讓我忍俊不禁。嗯稠炬,在網(wǎng)絡(luò)層架構(gòu)我會(huì)舉出不使用ReactiveCocoa的例子焕阿,現(xiàn)在舉我感覺有點(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ù)字我隨便寫的不要太在意褒纲。然后你有一個(gè)模塊是地圖模塊,把經(jīng)緯度數(shù)組全部都轉(zhuǎn)變成MKAnnotation或其派生類對(duì)于Controller來說是弱業(yè)務(wù)钥飞,(記住莺掠,胖Model就是用來做弱業(yè)務(wù)的),因此我們用ViewModel直接把它轉(zhuǎn)變成MKAnnotation的NSArray代承,交給Controller之后Controller直接就可以用了汁蝶。嗯,這就是ViewModel要做的事情论悴,是不是覺得很簡單掖棉,看不出優(yōu)越性?安居客Pad應(yīng)用也有一個(gè)地圖模塊膀估,在這里我設(shè)計(jì)了一個(gè)對(duì)象叫做reformer(其實(shí)就是ViewModel)幔亥,專門用來干這個(gè)事情。那么這么做的優(yōu)越性體現(xiàn)在哪兒呢察纯?安居客分三大業(yè)務(wù):租房帕棉、二手房、新房饼记。這三個(gè)業(yè)務(wù)對(duì)應(yīng)移動(dòng)開發(fā)團(tuán)隊(duì)有三個(gè)API開發(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不可能寫三個(gè)博肋,所以肯定少不了要有一個(gè)API數(shù)據(jù)兼容的邏輯低斋,這個(gè)邏輯我就放在reformer里面去做了,于是業(yè)務(wù)流程就變成了這樣:? ? ? ? ? ? 用戶進(jìn)入地圖頁發(fā)起地圖API請(qǐng)求? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? -----------------------------------------? ? ? |? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? |? ? ? |? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? |? 新房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的東西我會(huì)放在網(wǎng)絡(luò)層架構(gòu)來詳細(xì)解釋。Reformer此時(shí)扮演的ViewModel角色能夠很好地給Controller減負(fù)衬衬,同時(shí)轻绞,維護(hù)成本也大大降低,經(jīng)過reformer產(chǎn)出的永遠(yuǎn)都是MKAnnotation佣耐,Controller可以直接拿來使用政勃。然后另外一點(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ù)",只不過這里的"數(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里面分出來的一部分),所以View并不適合直接持有ViewModel律想,那么View一旦產(chǎn)生數(shù)據(jù)了怎么辦猎莲?扔信號(hào)扔給ViewModel,用誰扔技即?ReactiveCocoa著洼。在MVVM中使用ReactiveCocoa的第一個(gè)目的就是如上所說,View并不適合直接持有ViewModel姥份。第二個(gè)目的就在于郭脂,ViewModel有可能并不是只服務(wù)于特定的一個(gè)View,使用更加松散的綁定關(guān)系能夠降低ViewModel和View之間的耦合度澈歉。那么在MVVM中展鸡,Controller扮演什么角色?大部分國內(nèi)外資料闡述MVVM的時(shí)候都是這樣排布的:View <-> ViewModel <-> Model埃难,造成了MVVM不需要Controller的錯(cuò)覺莹弊,現(xiàn)在似乎發(fā)展成業(yè)界開始出現(xiàn)MVVM是不需要Controller的。的聲音了涡尘。其實(shí)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其實(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果本身并沒有提供一個(gè)比較適合這種情況的綁定方法换帜。iOS領(lǐng)域里KVO,Notification鹤啡,block惯驼,delegate和target-action都可以用來做數(shù)據(jù)通信,從而來實(shí)現(xiàn)綁定递瑰,但都不如ReactiveCocoa提供的RACSignal來的優(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層來裝各種ViewModel,也是屬于相對(duì)合理的結(jié)構(gòu)俯萎。所以Controller在MVVM中傲宜,一方面負(fù)責(zé)View和ViewModel之間的綁定,另一方面也負(fù)責(zé)常規(guī)的UI邏輯處理夫啊。VIPERVIPER(View函卒,Interactor,Presenter撇眯,Entity报嵌,Routing)。VIPER我并沒有實(shí)際使用過叛本,我是在objc.io上第13期看到的沪蓬。但凡出現(xiàn)一個(gè)新架構(gòu)或者我之前并不熟悉的新架構(gòu),有一點(diǎn)我能夠非忱春颍肯定跷叉,這貨一定又是把MVC的哪個(gè)部分給拆開了(壞笑,做這種判斷的理論依據(jù)在第一篇文章里面我已經(jīng)講過了)。事實(shí)情況是VIPER確實(shí)拆了很多很多云挟,除了View沒拆梆砸,其它的都拆了。我提到的這兩篇文章關(guān)于VIPER都講得很詳細(xì)园欣,一看就懂帖世。但具體在使用VIPER的時(shí)候會(huì)有什么坑或者會(huì)有哪些爭議我不是很清楚,硬要寫這一節(jié)的話我只能靠YY沸枯,所以我想想還是算了日矫。如果各位讀者有誰在實(shí)際App中采用VIPER架構(gòu)的或者對(duì)VIPER很有興趣的,可以評(píng)論區(qū)里面提出來绑榴,我們交流一下哪轿。本門心法重劍無鋒,大巧不工翔怎。 ---- 《神雕俠侶》這是楊過在挑劍時(shí)窃诉,玄鐵重劍旁邊寫的一段話。對(duì)此我深表認(rèn)同赤套。提到這段話的目的是想告訴大家飘痛,在具體做View層架構(gòu)的設(shè)計(jì)時(shí),不需要拘泥于MVC容握、MVVM宣脉、VIPER等規(guī)矩。這些都是招式唯沮,告訴你你就知道了脖旱,然后怎么玩都可以。但是心法不是這樣的介蛉,心法是大巧萌庆,說出來很簡單,但是能不能在實(shí)際架構(gòu)設(shè)計(jì)時(shí)牢記心法币旧,并且按照規(guī)矩辦事践险,就都看個(gè)人了。拆分的心法天下功夫出少林吹菱,天下架構(gòu)出MVC巍虫。 ---- Casa TaloyumMVC其實(shí)是非常高Level的抽象,意思也就是鳍刷,在MVC體系下還可以再衍生無數(shù)的架構(gòu)方式占遥,但萬變不離其宗的是,它一定符合MVC的規(guī)范输瓜。這句話不是我說的瓦胎,是我在某個(gè)英文資料上看到的芬萍,但時(shí)過境遷,我已經(jīng)找不到出處了搔啊,我很贊同這句話柬祠。我采用的架構(gòu)嚴(yán)格來說也是MVC,但也做了很多的拆分负芋。根據(jù)前面幾節(jié)的洗禮漫蛔,相信各位也明白了這樣的道理:拆分方式的不同誕生了各種不同的衍生架構(gòu)方案(MVCS拆胖Controller,MVVM拆胖Model旧蛾,VIPER什么都拆)莽龟,但即便拆分方式再怎么多樣,那都只是招式蚜点。而拆分的規(guī)范轧房,就是心法拌阴。這一節(jié)我就講講我在做View架構(gòu)時(shí)绍绘,做拆分的心法。第一心法:保留最重要的任務(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菩咨。唐巧的博客有一篇文章提到他和另一個(gè)工程師關(guān)于是否要拆分DataSource爭論了好久。拆分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ù)第一心法拆開來的東西戴而,很有可能還是強(qiáng)業(yè)務(wù)相關(guān)的凑术,這種情況有的時(shí)候無法避免。但我們拆也要拆得好看所意,拆出來的部分最好能夠歸成某一類對(duì)象淮逊,然后最好能夠抽象出一個(gè)通用邏輯出來,使他能夠復(fù)用扶踊。即使不能抽出通用邏輯泄鹏,那也盡量抽象出一個(gè)protocol,來實(shí)現(xiàn)IOP秧耗。這里有篇關(guān)于IOP的文章备籽,大家看了就明白優(yōu)越性了。第三心法:要盡可能提高拆分模塊后的抽象度也就是說分井,拆分的粒度要盡可能大一點(diǎn)车猬,封裝得要透明一些。唐巧說一切隱藏都是對(duì)代碼復(fù)雜性的增加尺锚,除非它帶來了好處珠闰,這在一定程度上有點(diǎn)道理,沒有好處的隱藏確實(shí)都不好(笑)缩麸。提高抽象度事實(shí)上就是增加封裝的力度铸磅,將一個(gè)負(fù)責(zé)的業(yè)務(wù)抽象成只需要很少的輸入就能完成,就是高度抽象杭朱。嗯阅仔,繼承很多層,這種做法雖然也提高了抽象程度弧械,但我不建議這么玩八酒。我不確定唐巧在這里說的隱藏跟我說的封裝是不是同一個(gè)概念,但我在這里想提倡的是盡可能提高抽象程度刃唐。提高抽象程度的好處在于羞迷,對(duì)于業(yè)務(wù)方來說界轩,他只需要收集很少的信息(最小充要條件),做很少的調(diào)度(Controller負(fù)責(zé)大模塊調(diào)度衔瓮,大模塊里面再去做小模塊的調(diào)度)浊猾,就能夠完成任務(wù),這才是給Controller減負(fù)的正確姿勢热鞍。如果拆分出來的模塊抽象程度不夠葫慎,模塊對(duì)外界要求的參數(shù)比較多,那么在Controller里面薇宠,關(guān)于收集參數(shù)的代碼就會(huì)多了很多偷办。如果一部分參數(shù)的收集邏輯能夠由模塊來完成,那也可以做到幫Controller減輕負(fù)擔(dān)澄港。否則就感覺拆得不太干凈椒涯,因?yàn)镃ontroller里面還是多了一些不必要的參數(shù)收集邏輯。如果拆分出來的粒度太小回梧,Controller在完成任務(wù)的時(shí)候調(diào)度代碼要寫很多废岂,那也不太好。導(dǎo)致拆分粒度小的首要因素就是業(yè)務(wù)可能本身就比較復(fù)雜漂辐,拆分粒度小并不是不好泪喊,能大就大一點(diǎn),如果小了髓涯,那也沒問題。針對(duì)這種情況的處理哈扮,就需要采用strategy模式纬纪。針對(duì)拆分粒度小的情況,我來舉個(gè)實(shí)際例子滑肉,這個(gè)例子來源于我的一個(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è)將來還有上傳別的類型消息的任務(wù)滚粟,他們又要依賴D/E/F模塊,那這個(gè)事情就很蛋疼刃泌,因?yàn)檫壿嫃?fù)雜了坦刀,Controller要調(diào)度的東西要區(qū)分的情況就多了,Controller就膨脹了蔬咬。那么怎么處理呢鲤遥?可以采用Strategy模式蹬竖。我們?cè)賮矸治鲆幌虏┚矗珻ontroller要完成任務(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用來回調(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];然后在某個(gè)Invocation里面,就是這樣的:? ? [A invoke];? ? [B invoke];? ? [C invoke];這樣就好啦帽馋,即便拆分粒度因?yàn)榭陀^原因無法細(xì)化搅方,那也能把復(fù)雜的判斷邏輯和調(diào)度邏輯從Controller中抽出來,真正為Controller做到了減負(fù)绽族∫涛校總之能夠做到大粒度就盡量大粒度,實(shí)在做不到那也行项秉,用Strategy把它hold住绣溜。這個(gè)例子是小粒度的情況,大粒度的情況太簡單娄蔼,我就不舉了怖喻。設(shè)計(jì)心法針對(duì)View層的架構(gòu)不光是看重如何合理地拆分MVC來給UIViewController減負(fù)底哗,另外一點(diǎn)也要照顧到業(yè)務(wù)方的使用成本。最好的情況是業(yè)務(wù)方什么都不知道锚沸,然后他把代碼放進(jìn)去就能跑跋选,同時(shí)還能獲得框架提供的種種功能。比如天安門廣場上的觀眾看臺(tái)哗蜈,就是我覺得最好的設(shè)計(jì)前标,因?yàn)闆]人會(huì)注意到它。第一心法:盡可能減少繼承層級(jí)距潘,涉及蘋果原生對(duì)象的盡量不要繼承繼承是罪惡炼列,盡量不要繼承。就我目前了解到的情況看音比,除了安居客的Pad App沒有在框架級(jí)針對(duì)UIViewController有繼承的設(shè)計(jì)以外俭尖,其它公司或多或少都針對(duì)UIViewController有繼承,包括安居客iPhone app(那時(shí)候我已經(jīng)對(duì)此無能為力洞翩,可見View的架構(gòu)在一開始就設(shè)計(jì)好有多么重要)稽犁。甚至有的還對(duì)UITableView有繼承,這是一件多么令人發(fā)指骚亿,多么慘絕人寰已亥,多么喪心病狂的事情啊。雖然不可避免的是有些情況我們不得不從蘋果原生對(duì)象中繼承来屠,比如UITableViewCell虑椎。但我還是建議盡量不要通過繼承的方案來給原生對(duì)象添加功能,前面提到的Aspect方案和Category方案都可以使用的妖。用Aspect+load來實(shí)現(xiàn)重載函數(shù)绣檬,用Category來實(shí)現(xiàn)添加函數(shù),當(dāng)然嫂粟,耍點(diǎn)手段用Category來添加property也是沒問題的。這些方案已經(jīng)覆蓋了繼承的全部功能墨缘,而且非常好維護(hù)星虹,對(duì)于業(yè)務(wù)方也更加透明,何樂而不為呢镊讼。不用繼承可能在思路上不會(huì)那么直觀宽涌,但是對(duì)于不使用繼承帶來的好處是足夠頂?shù)蒙鲜褂美^承的壞處的。順便在此我要給Category正一下名:業(yè)界對(duì)于Category的態(tài)度比較曖昧蝶棋,在多種場合(講座卸亮、資料文檔)都宣揚(yáng)過盡可能不要使用Category。它們說的都有一定道理玩裙,但我認(rèn)為Category是蘋果提供的最好的使用集合代替繼承的方案兼贸,但針對(duì)Category的設(shè)計(jì)對(duì)架構(gòu)師的要求也很高段直,請(qǐng)合理使用。而且蘋果也在很多場合使用Category溶诞,來把一個(gè)原本可能很大的對(duì)象鸯檬,根據(jù)不同場景拆分成不同的Category,從而提高可維護(hù)性螺垢。不使用繼承的好處我在這里已經(jīng)說了喧务,放到iOS應(yīng)用架構(gòu)來看,還能再多額外兩個(gè)好處:1. 在業(yè)務(wù)方做業(yè)務(wù)開發(fā)或者做Demo時(shí)枉圃,可以脫離App環(huán)境功茴,或花更少的時(shí)間搭建環(huán)境。2. 對(duì)業(yè)務(wù)方來說功能更加透明孽亲,也符合業(yè)務(wù)方在開發(fā)時(shí)的第一直覺坎穿。第二心法:做好代碼規(guī)范,規(guī)定好代碼在文件中的布局墨林,尤其是ViewController這主要是為了提高可維護(hù)性赁酝。在一個(gè)文件非常大的對(duì)象中,尤其要限制好不同類型的代碼在文件中的布局旭等。比如在寫ViewController時(shí)酌呆,我之前給團(tuán)隊(duì)制定的規(guī)范就是前面一段全部是getter setter,然后接下來一段是life cycle搔耕,viewDidLoad之類的方法都在這里隙袁。然后下面一段是各種要實(shí)現(xiàn)的Delegate,再下面一段就是event response弃榨,Button的或者GestureRecognizer的都在這里菩收。然后后面是private method。一般情況下鲸睛,如果做好拆分娜饵,ViewController的private method那一段是沒有方法的。后來隨著時(shí)間的推移官辈,我發(fā)現(xiàn)開頭放getter和setter太影響閱讀了箱舞,所以后面改成全放在ViewController的最后。第三心法:能不放在Controller做的事情就盡量不要放在Controller里面去做Controller會(huì)變得龐大的原因拳亿,一方面是因?yàn)镃ontroller承載了業(yè)務(wù)邏輯晴股,MVC的總結(jié)者(在正式提出MVC之前,或多或少都有人這么設(shè)計(jì)肺魁,所以說MVC的設(shè)計(jì)者不太準(zhǔn)確)對(duì)Controller下的定義也是承載業(yè)務(wù)邏輯电湘,所以Controller就是用來干這事兒的,天經(jīng)地義。另一方面是因?yàn)樵贛VC中寂呛,關(guān)于Model和View的定義都非常明確怎诫,很少有人會(huì)把一個(gè)屬于M或V的東西放到其他地方。然后除了Model和View以外昧谊,還會(huì)剩下很多模棱兩可的東西刽虹,這些東西從概念上講都算Controller,而且由于M和V定義得那么明確呢诬,所以直覺上看涌哲,這些東西放在M或V是不合適的,于是就往Controller里面塞咯尚镰。正是由于上述兩方面原因?qū)е铝薈ontroller的膨脹阀圾。我們?cè)偌?xì)細(xì)思考一下,Model膨脹和View膨脹狗唉,要針對(duì)它們來做拆分其實(shí)都是相對(duì)容易的初烘,Controller膨脹之后,拆分就顯得艱難無比分俯。所以如果能夠在一開始就盡量把能不放在Controller做的事情放到別的地方去做肾筐,這樣在第一時(shí)間就可以讓你的那部分將來可能會(huì)被拆分的代碼遠(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àn)再豐富玄渗,代碼量最多只能優(yōu)化减江,該膨脹還是要膨脹的,而且優(yōu)化之后代碼往往也比較難看捻爷,使用各種奇技淫巧也是有代價(jià)的。所以份企,針對(duì)代碼量優(yōu)化的結(jié)果也榄,往往要么就是犧牲可讀性,要么就是犧牲可移植性(通用性),Every magic always needs a pay, you have to make a trade-off.甜紫。那么既然膨脹出來的代碼降宅,或者將來有可能膨脹的代碼,不管放在MVC中的哪一個(gè)部分囚霸,最后都是要拆分的腰根,既然遲早要拆分,那不如放Model里面拓型,這樣將來拆分胖Model也能比拆分胖Cotroller更加容易额嘿。在我還在安居客的時(shí)候,安居客Pad app承載最復(fù)雜業(yè)務(wù)的ViewController才不到600行劣挫,其他多數(shù)Controller都是在300-400行之間册养,這就為后面接手的人降低了非常多的上手難度和維護(hù)復(fù)雜度。拆分出來的東西都是可以直接遷移給iPhone app使用的⊙构蹋現(xiàn)在看天貓的ViewControler球拦,動(dòng)不動(dòng)就幾千行,看不了多久頭就暈了帐我,問了一下坎炼,大家都表示很習(xí)慣這樣的代碼長度,攤手拦键。第四心法:架構(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ù)工程師避免他無法預(yù)見的危機(jī),所以地位高有一定的好處瑞侮,畢竟夏蟲不可語冰的圆,有的時(shí)候不見得能夠解釋得通,因此高地位隨之而來的就是說服力會(huì)比較強(qiáng)半火。但在軟件工程里越妈,一定要保持謙卑,一定要多為業(yè)務(wù)工程師考慮钮糖。一個(gè)不懂這個(gè)道理的架構(gòu)師梅掠,設(shè)計(jì)出來的東西往往復(fù)雜難用酌住,因?yàn)樗辉敢庾龊诵牡臇|西,周邊不愿意做的都期望交給業(yè)務(wù)工程師去做阎抒,甚至有的時(shí)候就只做了個(gè)Demo酪我,然后就交給業(yè)務(wù)工程師了,業(yè)務(wù)工程師變成給他打工的了且叁。但是一個(gè)懂得這個(gè)道理的架構(gòu)師都哭,設(shè)計(jì)出來的東西會(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層來說意義重大躏结,畢竟View層非常重業(yè)務(wù)却盘,如果代碼布局混亂,后來者很難接手媳拴,也很難維護(hù)黄橘。架構(gòu)模式具體如何選擇,完全取決于業(yè)務(wù)復(fù)雜度屈溉。如果業(yè)務(wù)相當(dāng)相當(dāng)復(fù)雜塞关,那就可以使用VIPER,如果相對(duì)簡單子巾,那就直接MVC稍微改改就好了描孟。每一種已經(jīng)成為定式的架構(gòu)模式不見得都適合各自公司對(duì)應(yīng)的業(yè)務(wù)驶睦,所以需要各位架構(gòu)師根據(jù)情況去做一些拆分或者改變。拆分一般都不會(huì)出現(xiàn)問題匿醒,改變的時(shí)候,只要?jiǎng)e把MVC三個(gè)角色搞混就好了缠导,M該做啥做啥廉羔,C該做啥做啥,V該做啥做啥僻造,不要亂來憋他。關(guān)于大部分的架構(gòu)模式應(yīng)該是什么樣子,這篇文章里都已經(jīng)說過了髓削,不過我認(rèn)為最重要的還是后面的心法竹挡,模式只是招術(shù),熟悉了心法才能大巧不工立膛。View層的工具集主要還是集中在如何對(duì)View進(jìn)行布局揪罕,以及一些特定的View,比如帶搜索提示的搜索框這種宝泵。這篇文章只提到了View布局的工具集好啰,其它的工具集相對(duì)而言是更加取決于各自公司的業(yè)務(wù)的,各自實(shí)現(xiàn)或者使用CocoaPods里現(xiàn)成的都不是很難儿奶。對(duì)于小規(guī)目蛲或者中等規(guī)模iOS開發(fā)團(tuán)隊(duì)來說,做好以上三點(diǎn)就足夠了闯捎。在大規(guī)模團(tuán)隊(duì)中椰弊,有一個(gè)額外問題要考慮,就是跨業(yè)務(wù)頁面調(diào)用方案的設(shè)計(jì)瓤鼻”妫跨業(yè)務(wù)頁面調(diào)用方案的設(shè)計(jì)跨業(yè)務(wù)頁面調(diào)用是指,當(dāng)一個(gè)App中存在A業(yè)務(wù)娱仔,B業(yè)務(wù)等多個(gè)業(yè)務(wù)時(shí)沐飘,B業(yè)務(wù)有可能會(huì)需要展示A業(yè)務(wù)的某個(gè)頁面,A業(yè)務(wù)也有可能會(huì)調(diào)用其他業(yè)務(wù)的某個(gè)頁面牲迫。在小規(guī)模的App中耐朴,我們直接import其他業(yè)務(wù)的某個(gè)ViewController然后或者push或者present,是不會(huì)產(chǎn)生特別大的問題的盹憎。但是如果App的規(guī)模非常大筛峭,涉及業(yè)務(wù)數(shù)量非常多,再這么直接import就會(huì)出現(xiàn)問題陪每。? ? --------------? ? ? ? ? ? --------------? ? ? ? ? ? --------------? ? |? ? ? ? ? ? |? page call? |? ? ? ? ? ? |? page call? |? ? ? ? ? ? |? ? | Buisness A | <---------> | Buisness B | <---------> | Buisness C |? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? --------------? ? ? ? ? ? --------------? ? ? ? ? ? --------------? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? ? |? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? |? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? ? --------------------------------? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? App? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? --------------------------------可以看出影晓,跨業(yè)務(wù)的頁面調(diào)用在多業(yè)務(wù)組成的App中會(huì)導(dǎo)致橫向依賴镰吵。那么像這樣的橫向依賴,如果不去設(shè)法解決挂签,會(huì)導(dǎo)致什么樣的結(jié)果疤祭?當(dāng)一個(gè)需求需要多業(yè)務(wù)合作開發(fā)時(shí),如果直接依賴饵婆,會(huì)導(dǎo)致某些依賴層上端的業(yè)務(wù)工程師在前期空轉(zhuǎn)勺馆,依賴層下端的工程師任務(wù)繁重,而整個(gè)需求完成的速度會(huì)變慢侨核,影響的是團(tuán)隊(duì)開發(fā)迭代速度草穆。當(dāng)要開辟一個(gè)新業(yè)務(wù)時(shí),如果已有各業(yè)務(wù)間直接依賴搓译,新業(yè)務(wù)又依賴某個(gè)舊業(yè)務(wù)悲柱,就導(dǎo)致新業(yè)務(wù)的開發(fā)環(huán)境搭建困難,因?yàn)楸仨氁阉邢嚓P(guān)業(yè)務(wù)都塞入開發(fā)環(huán)境些己,新業(yè)務(wù)才能進(jìn)行開發(fā)豌鸡。影響的是新業(yè)務(wù)的響應(yīng)速度。當(dāng)某一個(gè)被其他業(yè)務(wù)依賴的頁面有所修改時(shí)轴总,比如改名直颅,涉及到的修改面就會(huì)特別大。影響的是造成任務(wù)量和維護(hù)成本都上升的結(jié)果怀樟。當(dāng)然功偿,如果App規(guī)模特別小,這三點(diǎn)帶來的影響也會(huì)特別小往堡,但是在阿里這樣大規(guī)模的團(tuán)隊(duì)中械荷,像天貓/淘寶這樣大規(guī)模的App,一旦遇上這里面哪怕其中一件事情虑灰,就特么很坑爹吨瞎。那么應(yīng)該怎樣處理這個(gè)問題?讓依賴關(guān)系下沉穆咐。怎么讓依賴關(guān)系下沉颤诀?引入Mediator模式。所謂引入Mediator模式來讓依賴關(guān)系下沉对湃,實(shí)質(zhì)上就是每次呼喚頁面的時(shí)候崖叫,通過一個(gè)中間人來召喚另外一個(gè)頁面,這樣只要每個(gè)業(yè)務(wù)依賴這個(gè)中間人就可以了拍柒,中間人的角色就可以放在業(yè)務(wù)層的下面一層心傀,這就是依賴關(guān)系下沉。? ? --------------? ? ? ? ? ? --------------? ? ? ? ? ? --------------? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? | Buisness A |? ? ? ? ? ? | Buisness B |? ? ? ? ? ? | Buisness C |? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? ? ? ? ? |? ? --------------? ? ? ? ? ? --------------? ? ? ? ? ? --------------? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? /? 通過Mediater來召喚頁面? ? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? ? |? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? ? \? ? ? ? ? ? ? |? ? ? ? ? ? ? /? ? ? ? ? ? ? ? ? ? ? --------------------------------? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? Mediater? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? --------------------------------? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? --------------------------------? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? App? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? --------------------------------當(dāng)A業(yè)務(wù)需要調(diào)用B業(yè)務(wù)的某個(gè)頁面的時(shí)候拆讯,將請(qǐng)求交給Mediater脂男,然后由Mediater通過某種手段獲取到B業(yè)務(wù)頁面的實(shí)例养叛,交還給A就行了。在具體實(shí)現(xiàn)這個(gè)機(jī)制的過程中宰翅,有以下幾個(gè)問題需要解決:設(shè)計(jì)一套通用的請(qǐng)求機(jī)制弃甥,請(qǐng)求機(jī)制需要跟業(yè)務(wù)剝離,使得不同業(yè)務(wù)的頁面請(qǐng)求都能夠被Mediater處理設(shè)計(jì)Mediater根據(jù)請(qǐng)求如何獲取其他業(yè)務(wù)的機(jī)制堕油,Mediater需要知道如何處理請(qǐng)求潘飘,上哪兒去找到需要的頁面這個(gè)看起來就非常像我們web開發(fā)時(shí)候的URL機(jī)制,發(fā)送一個(gè)Get或Post請(qǐng)求掉缺,CGI調(diào)用腳本把請(qǐng)求分發(fā)給某個(gè)Controller下的某個(gè)Action,然后返回HTML字符串到瀏覽器去解析戈擒。蘋果本身也實(shí)現(xiàn)了一套跨App調(diào)用機(jī)制眶明,它也是基于URL機(jī)制來運(yùn)轉(zhuǎn)的,只不過它想要解決的問題是跨App的數(shù)據(jù)交流和頁面調(diào)用筐高,我們想要解決的問題是降低各業(yè)務(wù)的耦合度搜囱。不過我們還不能直接使用蘋果原生的這套機(jī)制,因?yàn)檫@套機(jī)制不能夠返回對(duì)象實(shí)例柑土。而我們希望能夠拿到對(duì)象實(shí)例蜀肘,這樣不光可以做跨業(yè)務(wù)頁面調(diào)用,也可以做跨業(yè)務(wù)的功能調(diào)用稽屏。另外扮宠,我們又希望我們的Mediater也能夠跟蘋果原生的跨App調(diào)用兼容,這樣就又能幫業(yè)務(wù)方省掉一部分開發(fā)量狐榔。就我目前所知道的情況坛增,AutoCad旗下某款iOS應(yīng)用(時(shí)間有點(diǎn)久我不記得是哪款應(yīng)用了,如果你是AutoCad的iOS開發(fā)薄腻,可以在評(píng)論區(qū)補(bǔ)充一下收捣。)就采用了這種頁面調(diào)用方式。天貓里面目前也在使用這套機(jī)制庵楷,只是這一塊由于歷史原因存在新老版本混用的情況罢艾,因此暫時(shí)還沒能夠很好地發(fā)揮應(yīng)有的作用。嗯尽纽,想問我要Demo的同學(xué)咐蚯,我可以很大方地告訴你,沒有蜓斧。不過我打算抽時(shí)間寫一個(gè)出來仓蛆,現(xiàn)在除了已經(jīng)想好名字叫Summon以外,其它什么都沒做挎春,哈哈看疙。關(guān)于Getter和Setter豆拨?我比較習(xí)慣一個(gè)對(duì)象的"私有"屬性寫在extension里面,然后這些屬性的初始化全部放在getter里面做能庆,在init和dealloc之外施禾,是不會(huì)出現(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)用一個(gè)setup方法而已渠旁,然后我推薦的方法就是不用多調(diào)一個(gè)setup方法攀例,直接走getter。嗯顾腊,怎么說呢粤铭,其實(shí)兩種做法都能完成需求。但是從另一個(gè)角度看杂靶,蘋果之所以選擇讓[self getProperty]和self.property可以互相通用梆惯,這種做法已經(jīng)很明顯地表達(dá)了蘋果的傾向:希望每個(gè)property都是通過getter方法來獲得。早在2003年吗垮,Allen Holub就發(fā)了篇文章《Why getter and setter methods are evil》垛吗,自此之后,業(yè)界就對(duì)此產(chǎn)生了各種爭議烁登,雖然是從Java開始說的怯屉,但是發(fā)展到后面各種語言也參與了進(jìn)來。然后雖然現(xiàn)在關(guān)于這個(gè)問題討論得少了防泵,但是依舊屬于沒有定論的狀態(tài)蚀之。setter的情況比較復(fù)雜,也不是我這一節(jié)的重點(diǎn)捷泞,我這邊還是主要說getter足删。我們從objc的設(shè)計(jì)來看,蘋果的設(shè)計(jì)者更加傾向于getter is not evil锁右。認(rèn)為getter is evil的原因有非常之多失受,或大或小,隨著爭論的進(jìn)行咏瑟,大家慢慢就聚焦到這樣的一個(gè)原因:Getter和Setter提供了一個(gè)能讓外部修改對(duì)象內(nèi)部數(shù)據(jù)的方式拂到,這是evil的,正常情況下码泞,一個(gè)對(duì)象自己私有的變量應(yīng)該是只有自己關(guān)心兄旬。然后我們回到iOS領(lǐng)域來,objc也同樣面臨了這樣的問題,甚至更加嚴(yán)重:objc并沒有像Java那么嚴(yán)格的私有概念领铐。但在實(shí)際工作中悯森,我們不太會(huì)去操作頭文件里面沒有的變量,這是從規(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來說燕鸽,我還是覺得使用getter會(huì)比較好,因?yàn)閑vil的地方在iOS這邊基本都避免了啼辣,not evil的地方都能享受到啊研,還是不錯(cuò)的∨概。總結(jié)要做一個(gè)View層架構(gòu)党远,主要就是從以下三方面入手:制定良好的規(guī)范選擇好合適的模式(MVC、MVCS富弦、MVVM沟娱、VIPER)根據(jù)業(yè)務(wù)情況針對(duì)ViewController做好拆分,提供一些小工具方便開發(fā)當(dāng)然腕柜,你還會(huì)遇到其他的很多問題济似,這時(shí)候你可以參考這篇文章里提出的心法,在后面提到的跨業(yè)務(wù)頁面調(diào)用方案的設(shè)計(jì)中盏缤,你也能夠看到我的一些心法的影子砰蠢。對(duì)于iOS客戶端來說,它并不像其他語言諸如Python唉铜、PHP他們有那么多的非官方通用框架台舱。客觀原因在于潭流,蘋果已經(jīng)為我們做了非常多的事情竞惋,做了很多的努力柜去。在蘋果已經(jīng)做了這么多事情的基礎(chǔ)上,架構(gòu)師要做針對(duì)View層的方案時(shí)碰声,最好還是盡量遵守蘋果已有的規(guī)范和設(shè)計(jì)思想诡蜓,然后根據(jù)自己過去開發(fā)iOS時(shí)的經(jīng)驗(yàn),盡可能給業(yè)務(wù)方在開發(fā)業(yè)務(wù)時(shí)減負(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)系岩调。什么是切片戈泼?程序要完成一件事情候址,一定會(huì)有一些步驟慢叨,1盖矫,2丽惭,3,4這樣辈双。這里分解出來的每一個(gè)步驟我們可以認(rèn)為是一個(gè)切片责掏。什么是面向切片編程?你針對(duì)每一個(gè)切片的間隙湃望,塞一些代碼進(jìn)去换衬,在程序正常進(jìn)行1,2证芭,3瞳浦,4步的間隙可以跑到你塞進(jìn)去的代碼,那么你寫這些代碼就是面向切片編程废士。為什么會(huì)出現(xiàn)面向切片編程叫潦?你要想做到在每一個(gè)步驟中間做你自己的事情,不用AOP也一樣可以達(dá)到目的湃密,直接往步驟之間塞代碼就好了诅挑。但是事實(shí)情況往往很復(fù)雜,直接把代碼塞進(jìn)去泛源,主要問題就在于:塞進(jìn)去的代碼很有可能是跟原業(yè)務(wù)無關(guān)的代碼拔妥,在同一份代碼文件里面摻雜多種業(yè)務(wù),這會(huì)帶來業(yè)務(wù)間耦合达箍。為了降低這種耦合度没龙,我們引入了AOP。如何實(shí)現(xiàn)AOP?AOP一般都是需要有一個(gè)攔截器硬纤,然后在每一個(gè)切片運(yùn)行之前和運(yùn)行之后(或者任何你希望的地方)解滓,通過調(diào)用攔截器的方法來把這個(gè)jointpoint扔到外面,在外面獲得這個(gè)jointpoint的時(shí)候筝家,執(zhí)行相應(yīng)的代碼洼裤。在iOS開發(fā)領(lǐng)域,objective-C的runtime有提供了一系列的方法溪王,能夠讓我們攔截到某個(gè)方法的調(diào)用腮鞍,來實(shí)現(xiàn)攔截器的功能,這種手段我們稱為Method Swizzling莹菱。Aspects通過這個(gè)手段實(shí)現(xiàn)了針對(duì)某個(gè)類和某個(gè)實(shí)例中方法的攔截移国。另外,也可以使用protocol的方式來實(shí)現(xiàn)攔截器的功能道伟,具體實(shí)現(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
這么做對(duì)比Method Swizzling有個(gè)額外好處就是迹缀,你可以通過攔截器來給攔截器的實(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)用攔截器方法的代碼寫上(笑),通過Aspects(本質(zhì)上就是Mehtod Swizzling)來實(shí)現(xiàn)的AOP颅和,就能輕松一些傅事。
2015-4-29 14:25 補(bǔ):關(guān)于在哪兒寫Constraints?
文章發(fā)出來之后峡扩,很多人針對(duì)勘誤1有很多看法蹭越,以至于我覺得很有必要在這里做一份補(bǔ)。期間過程很多很復(fù)雜教届,這篇文章也已經(jīng)很長了响鹃,我就直接說結(jié)果了哈。
pic1
蘋果在文檔中指出案训,updateViewConstraints是用來做add constraints的地方买置。
但是在這里有一個(gè)回答者說updateViewConstraints并不適合做添加Constraints的事情。
綜合我自己和評(píng)論區(qū)各位關(guān)心這個(gè)問題的兄弟們的各種測試和各種文檔强霎,我現(xiàn)在覺得還是在viewDidLoad里面開一個(gè)layoutPageSubviews的方法忿项,然后在這個(gè)里面創(chuàng)建Constraints并添加,會(huì)比較好。就是像下面這樣:
- (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];
}
最后轩触,要感謝評(píng)論區(qū)各位關(guān)心這個(gè)問題寞酿,并提出自己意見,甚至是自己親自測試然后告訴我結(jié)果的各位兄弟:@fly2never脱柱,@Wythe伐弹,@wtlucky,@lcddhr榨为,@李新星惨好,@Meigan Fang,@匿名柠逞,@Xiao Moch昧狮。
這個(gè)做法是目前我自己覺得可能比較合適的做法,當(dāng)然也歡迎其他同學(xué)繼續(xù)拿出自己的看法板壮,我們來討論逗鸣。
勘誤
我的前同事@ddaajing看了這篇文章之后,給我提出了以下兩個(gè)勘誤绰精,和很多行文上的問題撒璧。在這里我對(duì)他表示非常感謝:
勘誤1:其實(shí)在viewWillAppear這里改變UI元素不是很可靠,Autolayout發(fā)生在viewWillAppear之后笨使,嚴(yán)格來說這里通常不做視圖位置的修改卿樱,而用來更新Form數(shù)據(jù)。改變位置可以放在viewWilllayoutSubview或者didLayoutSubview里硫椰,而且在viewDidLayoutSubview確定UI位置關(guān)系之后設(shè)置autoLayout比較穩(wěn)妥繁调。另外,viewWillAppear在每次頁面即將顯示都會(huì)調(diào)用靶草,viewWillLayoutSubviews雖然在lifeCycle里調(diào)用順序在viewWillAppear之后蹄胰,但是只有在頁面元素需要調(diào)整時(shí)才會(huì)調(diào)用,避免了Constraints的重復(fù)添加奕翔。
勘誤2:MVVM要有ViewModel裕寨,以及ReactiveCocoa帶來的信號(hào)通知效果,在ReactiveCocoa里就是RAC等相關(guān)宏來實(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è)重要方面。
有任何問題建議直接在評(píng)論區(qū)提問绅络,這樣后來的人如果有相同的問題阅悍,就能直接找到答案了好渠。提問之前也可以先看看評(píng)論區(qū)有沒有人問過類似問題了。
所有評(píng)論和問題我都會(huì)在第一時(shí)間回復(fù)节视,QQ上我是不回答問題的哈拳锚。
評(píng)論系統(tǒng)我用的是Disqus,不定期被墻寻行。所以如果你看到文章下面沒有加載出評(píng)論列表霍掺,翻個(gè)墻就有了。
本文遵守CC-BY拌蜘。 請(qǐng)保持轉(zhuǎn)載后文章內(nèi)容的完整杆烁,以及文章出處。本人保留所有版權(quán)相關(guān)權(quán)利简卧。
我的博客拒絕掛任何廣告兔魂,如果您覺得文章有價(jià)值,可以通過支付寶掃描下面的二維碼捐助我举娩。