久久久久久久999_99精品久久精品一区二区爱城_成人欧美一区二区三区在线播放_国产精品日本一区二区不卡视频_国产午夜视频_欧美精品在线观看免费

標(biāo)題: iOS應(yīng)用架構(gòu)談 view層的組織和調(diào)用方案 (轉(zhuǎn)) [打印本頁]

作者: niujia    時(shí)間: 2015-7-18 00:31
標(biāo)題: iOS應(yīng)用架構(gòu)談 view層的組織和調(diào)用方案 (轉(zhuǎn))
前言

《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ī)的特殊性,我們在這時(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住的。這樣的情況,就要求我們在實(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),主要原因有以下五種:
這五個(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)中一旦定型,可修改的空間就最小,我們在一開始考慮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ī)定
架構(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ī)范的重要性在于:
在這一節(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)該差不多是這樣:

要點(diǎn)如下:
所有的屬性都使用getter和setter
不要在viewDidLoad里面初始化你的view然后再add,這樣代碼就很難看。在viewDidload里面只做addSubview的事情,然后在viewWillAppear里面做布局的事情(勘誤1),最后在viewDidAppear里面做Notification的監(jiān)聽之類的事情。至于屬性的初始化,則交給getter去做。
比如這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#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)以下的情況:

1
2
3
4
5
6
7
8
9
10
- (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)目里面的方法?勺x性很好。
讓業(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):
如果這三個(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)一邏輯的目的,使用派生的手段是有必要的嗎?
我覺得沒有必要,為什么沒有必要?
這兩條原因是我認(rèn)為沒有必要使用派生手段的理由,如果兩條理由你都心領(lǐng)神會(huì),那么下面的就可以不用看了。如果你還有點(diǎn)疑惑,請看下面我來詳細(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。
盡可能少地使用繼承能提高項(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è)問題?
這三個(gè)問題按照順序一一解答之后,具體方案就能出來了。
我們先看第一個(gè)問題:方案的效果,和最終要達(dá)到的目的是什么?
方案的效果應(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è)比較好的參考。
MVC
MVC(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的劃分問題?
關(guān)于這個(gè),每個(gè)人糾結(jié)的點(diǎn)可能不太一樣,我也不知道當(dāng)時(shí)座談會(huì)上大家的觀點(diǎn)。但請?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:

1
2
3
4
5
6
7
8
9
10
11
12
    /*

        ...

            數(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ì)變成這樣:

1
2
3
4
5
6
7
8
- (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是這樣劃分的:

但事實(shí)上,整套流程的MVC劃分是這樣:

由圖中可以看出,我們服務(wù)端開發(fā)在這個(gè)概念下,其實(shí)只涉及M和C的開發(fā)工作,瀏覽器作為View的容器,負(fù)責(zé)View的展示和事件的監(jiān)聽。那么對(duì)應(yīng)到iOS客戶端的MVC劃分上面來,就是這樣:

唯一區(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)該做的事:
C應(yīng)該做的事:
V應(yīng)該做的事:
我通過與服務(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包含了部分弱業(yè)務(wù)邏輯。胖Model要達(dá)到的目的是,Controller從胖Model這里拿到數(shù)據(jù)之后,不用額外做操作或者只要做非常少的操作,就能夠?qū)?shù)據(jù)直接應(yīng)用在View上。舉個(gè)例子:

1
2
3
4
5
6
7
8
9
Raw Data:

    timestamp:1234567

FatModel:

    @property (nonatomic, assign) CGFloat timestamp;

    - (NSString *)ymdDateString; // 2015-04-20 15:16

    - (NSString *)gapString; // 3分鐘前、1小時(shí)前、一天前、2015-3-13 12:34

Controller:

    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只負(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è)例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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的壓力。
MVVM
MVVM去年在業(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è)例子:

1
2
3
4
5
6
7
8
    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ù)流程就變成了這樣:

這么一來,原本復(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請求是能夠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)該是這樣:

(來源:http://www.sprynthesis.com/2014/ ... -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邏輯處理。
VIPER
VIPER(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 Taloyum
MVC其實(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í),做拆分的心法。
在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)。
根據(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ù)器申請上傳資源,獲得資源ID之后再上傳圖片,上傳圖片完成之后拿到圖片URL,后面帶著URL再把信息發(fā)送出去。
這時(shí)候我們拆模塊,可以拆成:數(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模塊,那這個(gè)事情就很蛋疼,因?yàn)檫壿嫃?fù)雜了,Controller要調(diào)度的東西要區(qū)分的情況就多了,Controller就膨脹了。
那么怎么處理呢?可以采用Strategy模式。我們再來分析一下,Controller要完成任務(wù),它初始情況下所具有的條件是什么?它有這條消息的所有數(shù)據(jù),也知道這個(gè)消息的類型。那么它最終需要的是什么呢?消息發(fā)送的結(jié)果:發(fā)送成功或失敗。

上面就是我們要實(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)度邏輯。

1
2
3
4
5
6
7
typedef NS_ENUM (NSUInteger, MessageSendStrategy)

{

    MessageSendStrategyText = 0,

    MessageSendStrategyImage = 1,

    MessageSendStrategyVoice = 2,

    MessageSendStrategyVideo = 3

}


然后在MessageSender里面的StrategyList是這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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) id<messagesenderdelegate> delegate;

@protocol MessageSenderDelegate<nsobject>

  @required

      - (void)messageSender:(MessageSender *)messageSender

      didSuccessSendMessage:(BaseMessage *)message

                   strategy:(MessageSendStrategy)strategy;

      - (void)messageSender:(MessageSender *)messageSender

         didFailSendMessage:(BaseMessage *)message

                   strategy:(MessageSendStrategy)strategy

                      error:(NSError *)error;

@end</nsobject></messagesenderdelegate>


Controller里面是這樣使用的:
1
[self.messageSender sendMessage:message withStrategy:MessageSendStrategyText];

MessageSender里面是這樣的:
1
[self.strategyList[strategy] invoke];

然后在某個(gè)Invocation里面,就是這樣的:
1
2
3
[A invoke];
[B invoke];
[C invoke];

這樣就好啦,即便拆分粒度因?yàn)榭陀^原因無法細(xì)化,那也能把復(fù)雜的判斷邏輯和調(diào)度邏輯從Controller中抽出來,真正為Controller做到了減負(fù)?傊軌蜃龅酱罅6染捅M量大粒度,實(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ì)注意到它。
繼承是罪惡,盡量不要繼承。就我目前了解到的情況看,除了安居客的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)師的要求也很高,請合理使用。而且蘋果也在很多場合使用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í)的第一直覺。
這主要是為了提高可維護(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會(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的膨脹。我們再細(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ī)?隙ㄉ俨涣说模还苣慵夹g(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使用的,F(xiàn)在看天貓的ViewControler,動(dòng)不動(dòng)就幾千行,看不了多久頭就暈了,問了一下,大家都表示很習(xí)慣這樣的代碼長度,攤手。
架構(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)問題。

可以看出,跨業(yè)務(wù)的頁面調(diào)用在多業(yè)務(wù)組成的App中會(huì)導(dǎo)致橫向依賴。那么像這樣的橫向依賴,如果不去設(shè)法解決,會(huì)導(dǎo)致什么樣的結(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)系下沉。

當(dāng)A業(yè)務(wù)需要調(diào)用B業(yè)務(wù)的某個(gè)頁面的時(shí)候,將請求交給Mediater,然后由Mediater通過某種手段獲取到B業(yè)務(wù)頁面的實(shí)例,交還給A就行了。在具體實(shí)現(xiàn)這個(gè)機(jī)制的過程中,有以下幾個(gè)問題需要解決:
這個(gè)看起來就非常像我們web開發(fā)時(shí)候的URL機(jī)制,發(fā)送一個(gè)Get或Post請求,CGI調(diào)用腳本把請求分發(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這樣的寫法的。就是這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@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),主要就是從以下三方面入手:
當(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)于AOP
AOP(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)方案就是這樣:

1
2
3
4
5
6
7
8
9
10
11
12
@protocol RTAPIManagerInterceptor <nsobject>
@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) id<rtapimanagerinterceptor> interceptor;
@end</rtapimanagerinterceptor></nsobject>

這么做對(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é)果了哈。

蘋果在文檔中指出,updateViewConstraints是用來做add constraints的地方。
但是在這里有一個(gè)回答者說updateViewConstraints并不適合做添加Constraints的事情。
綜合我自己和評(píng)論區(qū)各位關(guān)心這個(gè)問題的兄弟們的各種測試和各種文檔,我現(xiàn)在覺得還是在viewDidLoad里面開一個(gè)layoutPageSubviews的方法,然后在這個(gè)里面創(chuàng)建Constraints并添加,會(huì)比較好。就是像下面這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (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è)重要方面。
本文遵守CC-BY。 請保持轉(zhuǎn)載后文章內(nèi)容的完整,以及文章出處。本人保留所有版權(quán)相關(guān)權(quán)利。







歡迎光臨 (http://m.zg4o1577.cn/bbs/) Powered by Discuz! X3.1
主站蜘蛛池模板: 91久久国产综合久久91精品网站 | avtt久久| 国产日韩欧美精品 | 欧美视频在线一区 | 欧美精品一级片 | 91av视频在线 | 亚洲精品乱码久久久久 | 深夜福利视频网站 | av免费在线观看网站 | 羞羞的视频在线观看 | 美女黄色免费网站 | 国产精品一品二区三区的使用体验 | 欧美一级特黄视频 | a天堂在线视频 | 中文文字幕文字幕高清 | 秘密爱大尺度做爰呻吟 | 经典三级第一页 | 精品国产欧美 | 亚洲黄色在线 | 久久98| 久久999| 国产超碰在线观看 | 国产乡下妇女三片 | 亚洲黄色大片 | 欧美黄色一区二区 | 狠狠操综合| 成 人 黄 色 片 在线播放 | 国产精品二区一区二区aⅴ污介绍 | 色婷婷一区 | 午夜在线视频观看 | av在线免费观看网址 | 黄色一级大片在线免费看国产一 | 成人在线a | 日韩在线不卡视频 | 97国产在线| 黄色小视频免费观看 | 激情四射网站 | 欧美日韩亚洲一区二区三区 | 久久综合av | 在线观看网址你懂的 | 久久国产一区二区三区 |