找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

搜索
查看: 2625|回復(fù): 0
打印 上一主題 下一主題
收起左側(cè)

iOS應(yīng)用架構(gòu)談 view層的組織和調(diào)用方案 (轉(zhuǎn))

[復(fù)制鏈接]
跳轉(zhuǎn)到指定樓層
樓主
ID:85764 發(fā)表于 2015-7-18 00:31 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
前言

《iOS應(yīng)用架構(gòu)談 開篇》出來之后,很多人來催我趕緊出第二篇。這一篇文章出得相當(dāng)艱難,因為公司里的破事兒特別多,我自己又有點私事兒,以至于能用來寫博客的時間不夠充分。
現(xiàn)在好啦,第二篇出來了。
當(dāng)我們開始設(shè)計View層的架構(gòu)時,往往是這個App還沒有開始開發(fā),或者這個App已經(jīng)發(fā)過幾個版本了,然后此時需要做非常徹底的重構(gòu)。
一般也就是這兩種時機會去做View層架構(gòu),基于這個時機的特殊性,我們在這時候必須清楚認(rèn)識到:View層的架構(gòu)一旦實現(xiàn)或定型,在App發(fā)版后可修改的余地就已經(jīng)非常之小了。因為它跟業(yè)務(wù)關(guān)聯(lián)最為緊密,所以哪怕稍微動一點點,它所引發(fā)的蝴蝶效應(yīng)都不見得是業(yè)務(wù)方能夠hold住的。這樣的情況,就要求我們在實現(xiàn)這個架構(gòu)時,代碼必須得改得勤快,不能偷懶。也必須抱著充分的自我懷疑態(tài)度,做決策時要拿捏好尺度。
View層的架構(gòu)非常之重要,在我看來,這部分架構(gòu)是這系列文章涉及4個方面最重要的一部分,沒有之一。為什么這么說?
View層架構(gòu)是影響業(yè)務(wù)方迭代周期的因素之一
產(chǎn)品經(jīng)理產(chǎn)生需求的速度會非?,尤其是公司此時仍處于創(chuàng)業(yè)初期,在規(guī)模稍大的公司里面,產(chǎn)品經(jīng)理也喜歡挖大坑來在leader面前刷存在感,比如阿里。這就導(dǎo)致業(yè)務(wù)工程師任務(wù)非常繁重。正常情況下讓產(chǎn)品經(jīng)理砍需求是不太可能的,因此作為架構(gòu)師,在架構(gòu)里有一些可做可不做的事情,最好還是能做就做掉,不要偷懶。這可以幫業(yè)務(wù)方減負(fù),編寫代碼的時候也能更加關(guān)注業(yè)務(wù)。
我跟一些朋友交流的時候,他們都會或多或少地抱怨自己的團(tuán)隊迭代速度不夠快,或者說,迭代速度不合理地慢。我認(rèn)為迭代速度不是想提就能提的,迭代速度的影響因素有很多,一期PRD里的任務(wù)量和任務(wù)復(fù)雜度都會影響迭代周期能達(dá)到什么樣的程度。拋開這些外在的不談,從內(nèi)在可能導(dǎo)致迭代周期達(dá)不到合理的速度的原因來看,其中有一個原因很有可能就是View層架構(gòu)沒有做好,讓業(yè)務(wù)工程師完成一個不算復(fù)雜的需求時,需要處理太多額外的事情。當(dāng)然,開會多,工程師水平爛也屬于迭代速度提不上去的內(nèi)部原因,但這個不屬于本文討論范圍。還有,加班不是優(yōu)化迭代周期的正確方式,嗯。
一般來說,一個不夠好的View層架構(gòu),主要原因有以下五種:
  • 代碼混亂不規(guī)范
  • 過多繼承導(dǎo)致的復(fù)雜依賴關(guān)系
  • 模塊化程度不夠高,組件粒度不夠細(xì)
  • 橫向依賴
  • 架構(gòu)設(shè)計失去傳承
這五個地方會影響業(yè)務(wù)工程師實現(xiàn)需求的效率,進(jìn)而拖慢迭代周期。View架構(gòu)的其他缺陷也會或多或少地產(chǎn)生影響,但在我看來這里五個是比較重要的影響因素。如果大家覺得還有什么因素比這四個更高的,可以在評論區(qū)提出來我補上去。
對于第五點我想做一下強調(diào):架構(gòu)的設(shè)計是一定需要有傳承的,有傳承的架構(gòu)從整體上看會非常協(xié)調(diào)。但實際情況有可能是一個人走了,另一個頂上,即便任務(wù)交接得再完整,都不可避免不同的人有不同的架構(gòu)思路,從而導(dǎo)致整個架構(gòu)的流暢程度受到影響。要解決這個問題,一方面要盡量避免單點問題,讓架構(gòu)師做架構(gòu)的時候再帶一個人。另一方面,架構(gòu)要設(shè)計得盡量簡單,平緩接手人的學(xué)習(xí)曲線。我離開安居客的時候,做過保證:凡是從我手里出來的代碼,終身保修。所以不要想著離職了就什么事兒都不管了,這不光是職業(yè)素養(yǎng)問題,還有一個是你對你的代碼是否足夠自信的問題。傳承性對于View層架構(gòu)非常重要,因為它距離業(yè)務(wù)最近,改動余地最小。
所以當(dāng)各位CTO、技術(shù)總監(jiān)、TeamLeader們覺得迭代周期不夠快時,你可以先不忙著急吼吼地去招新人,《人月神話》早就說過加人不能完全解決問題。這時候如果你可以回過頭來看一下是不是View層架構(gòu)不合理,把這個弄好也是優(yōu)化迭代周期的手段之一。
嗯,至于本系列其他三項的架構(gòu)方案對于迭代周期的影響程度,我認(rèn)為都不如View層架構(gòu)方案對迭代周期的影響高,所以這是我認(rèn)為View層架構(gòu)是最重要的其中一個理由。
View層架構(gòu)是最貼近業(yè)務(wù)的底層架構(gòu)
View層架構(gòu)雖然也算底層,但還沒那么底層,它跟業(yè)務(wù)的對接面最廣,影響業(yè)務(wù)層代碼的程度也最深。在所有的底層都牽一發(fā)的時候,在View架構(gòu)上牽一發(fā)導(dǎo)致業(yè)務(wù)層動全身的面積最大。
所以View架構(gòu)在所有架構(gòu)中一旦定型,可修改的空間就最小,我們在一開始考慮View相關(guān)架構(gòu)時,不光要實現(xiàn)功能,還要考慮更多規(guī)范上的東西。制定規(guī)范的目的一方面是防止業(yè)務(wù)工程師的代碼腐蝕View架構(gòu),另一方面也是為了能夠有所傳承。按照規(guī)范來,總還是不那么容易出差池的。
還有就是,架構(gòu)師一開始考慮的東西也會有很多,不可能在第一版就把它們?nèi)繉崿F(xiàn),對于一個尚未發(fā)版的App來說,第一版架構(gòu)往往是最小完整功能集,那么在第二版第三版的發(fā)展過程中,架構(gòu)的迭代任務(wù)就很有可能不只是你一個人的事情了,相信你一個人也不見得能搞定全部。所以你要跟你的合作者們有所約定。另外,第一版出去之后,業(yè)務(wù)工程師在使用過程中也會產(chǎn)生很多修改意見,哪些意見是合理的,哪些意見是不合理的,也要通過事先約定的規(guī)范來進(jìn)行篩選,最終決定如何采納。
規(guī)范也不是一成不變的,什么時候槍斃意見,什么時候改規(guī)范,這就要靠各位的技術(shù)和經(jīng)驗了。
以上就是前言。
這篇文章講什么?
  • View代碼結(jié)構(gòu)的規(guī)定
  • 關(guān)于view的布局
  • 何時使用storyboard,何時使用nib,何時使用代碼寫View
  • 是否有必要讓業(yè)務(wù)方統(tǒng)一派生ViewController?
  • 方便View布局的小工具
  • MVC、MVVM、MVCS、VIPER
  • 本門心法
  • 跨業(yè)務(wù)時View的處理
  • 留給評論區(qū)各種補
  • 總結(jié)
View代碼結(jié)構(gòu)的規(guī)定
架構(gòu)師不是寫SDK出來交付業(yè)務(wù)方使用就沒事兒了的,每家公司一定都有一套代碼規(guī)范,架構(gòu)師的職責(zé)也包括定義代碼規(guī)范。按照道理來講,定代碼規(guī)范應(yīng)該是屬于通識,放在這里講的原因只是因為我這邊需要為View添加一個規(guī)范。
制定代碼規(guī)范嚴(yán)格來講不屬于View層架構(gòu)的事情,但它對View層架構(gòu)未來的影響會比較大,也是屬于架構(gòu)師在設(shè)計View層架構(gòu)時需要考慮的事情。制定View層規(guī)范的重要性在于:
  • 提高業(yè)務(wù)方View層的可讀性可維護(hù)性
  • 防止業(yè)務(wù)代碼對架構(gòu)產(chǎn)生腐蝕
  • 確保傳承
  • 保持架構(gòu)發(fā)展的方向不輕易被不合理的意見所左右
在這一節(jié)里面我不打算從頭開始定義一套規(guī)范,蘋果有一套Coding Guidelines,當(dāng)我們定代碼結(jié)構(gòu)或規(guī)范的時候,首先一定要符合這個規(guī)范。
然后,相信大家各自公司里面也都有一套自己的規(guī)范,具體怎么個規(guī)范法其實也是根據(jù)各位架構(gòu)師的經(jīng)驗而定,我這邊只是建議各位在各自規(guī)范的基礎(chǔ)上再加上下面這一點。
viewController的代碼應(yīng)該差不多是這樣:

要點如下:
所有的屬性都使用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去做了?傊褪潜M量不要出現(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)于這個做法,在唐巧的技術(shù)博客里面有一篇文章和我所提倡的做法不同,這個我會放在后面詳細(xì)論述。
getter和setter全部都放在最后
因為一個ViewController很有可能會有非常多的view,就像上面給出的代碼樣例一樣,如果getter和setter寫在前面,就會把主要邏輯扯到后面去,其他人看的時候就要先劃過一長串getter和setter,這樣不太好。然后要求業(yè)務(wù)工程師寫代碼的時候按照順序來分配代碼塊的位置,先是life cycle,然后是Delegate方法實現(xiàn),然后是event response,然后才是getters and setters。這樣后來者閱讀代碼時就能省力很多。
每一個delegate都把對應(yīng)的protocol名字帶上,delegate方法不要到處亂寫,寫到一塊區(qū)域里面去
比如UITableViewDelegate的方法集就老老實實寫上#pragma mark - UITableViewDelegate。這樣有個好處就是,當(dāng)其他人閱讀一個他并不熟悉的Delegate實現(xiàn)方法時,他只要按住command然后去點這個protocol名字,Xcode就能夠立刻跳轉(zhuǎn)到對應(yīng)這個Delegate的protocol定義的那部分代碼去,就省得他到處找了。
event response專門開一個代碼區(qū)域
所有button、gestureRecognizer的響應(yīng)事件都放在這個區(qū)域里面,不要到處亂放。
關(guān)于private methods,正常情況下ViewController里面不應(yīng)該寫
不是delegate方法的,不是event response方法的,不是life cycle方法的,就是private method了。對的,正常情況下ViewController里面一般是不會存在private methods的,這個private methods一般是用于日期換算、圖片裁剪啥的這種小功能。這種小功能要么把它寫成一個category,要么把他做成一個模塊,哪怕這個模塊只有一個函數(shù)也行。
ViewController基本上是大部分業(yè)務(wù)的載體,本身代碼已經(jīng)相當(dāng)復(fù)雜,所以跟業(yè)務(wù)關(guān)聯(lián)不大的東西能不放在ViewController里面就不要放。另外一點,這個private method的功能這時候只是你用得到,但是將來說不定別的地方也會用到,一開始就獨立出來,有利于將來的代碼復(fù)用。
為什么要這樣要求?
我見過無數(shù)ViewController,代碼布局亂得一塌糊涂,這里一個delegate那里一個getter,然后ViewController的代碼一般都死長死長的,看了就讓人頭疼。
定義好這個規(guī)范,就能使得ViewController條理清晰,業(yè)務(wù)方程序員很能夠區(qū)分哪些放在ViewController里面比較合適,哪些不合適。另外,也可以提高代碼的可維護(hù)性和可讀性。
關(guān)于View的布局
業(yè)務(wù)工程師在寫View的時候一定逃不掉的就是這個命題。用Frame也好用Autolayout也好,如果沒有精心設(shè)計過,布局部分一定慘不忍睹。
直接使用CGRectMake的話可讀性很差,光看那幾個數(shù)字,也無法知道view和view之間的位置關(guān)系。用Autolayout可讀性稍微好點兒,但生成Constraint的長度實在太長,代碼觀感不太好。
Autolayout這邊可以考慮使用Masonry,代碼的可讀性就能好很多。如果還有使用Frame的,可以考慮一下使用這個項目。
這個項目里面提供了Frame相關(guān)的方便方法(UIView+LayoutMethods),里面的方法也基本涵蓋了所有布局的需求,可讀性非常好,使用它之后基本可以和CGRectMake說再見了。因為天貓在最近才切換到支持iOS6,所以之前天貓都是用Frame布局的,在天貓App中,首頁,范兒部分頁面的布局就使用了這些方法。使用這些方便方法能起到事半功倍的效果。
這個項目也提供了Autolayout方案下生產(chǎn)Constraints的方便方法(UIView+AEBHandyAutoLayout),可讀性比原生好很多。我當(dāng)時在寫這系列方法的時候還不知道有Masonry。知道有Masonry之后我特地去看了一下,發(fā)現(xiàn)Masonry功能果然強大。不過這系列方法雖然沒有Masonry那么強大,但是也夠用了。當(dāng)時安居客iPad版App全部都是Autolayout來做的View布局,就是使用的這個項目里面的方法?勺x性很好。
讓業(yè)務(wù)工程師使用良好的工具來做View的布局,能提高他們的工作效率,也能減少bug發(fā)生的幾率。架構(gòu)師不光要關(guān)心那些高大上的內(nèi)容,也要多給業(yè)務(wù)工程師提供方便易用的小工具,才能發(fā)揮架構(gòu)師的價值。
何時使用storyboard,何時使用nib,何時使用代碼寫View
這個問題唐巧的博客里這篇文章也提到過,我的意見和他是基本一致的。
在這里我還想補充一些內(nèi)容:
具有一定規(guī)模的團(tuán)隊化iOS開發(fā)(10人以上)有以下幾個特點:
  • 同一份代碼文件的作者會有很多,不同作者同時修改同一份代碼的情況也不少見。因此,使用Git進(jìn)行代碼版本管理時出現(xiàn)Conflict的幾率也比較大。
  • 需求變化非常頻繁,產(chǎn)品經(jīng)理一時一個主意,為了完成需求而針對現(xiàn)有代碼進(jìn)行微調(diào)的情況,以及針對現(xiàn)有代碼的部分復(fù)用的情況也比較多。
  • 復(fù)雜界面元素、復(fù)雜動畫場景的開發(fā)任務(wù)比較多。
如果這三個特點你一看就明白了,下面的解釋就可以不用看了。如果你針對我的傾向愿意進(jìn)一步討論的,可以先看我下面的解釋,看完再說。
同一份代碼文件的作者會有很多,不同作者同時修改同一份代碼的情況也不少見。因此,使用Git進(jìn)行代碼版本管理時出現(xiàn)Conflict的幾率也比較大。
iOS開發(fā)過程中,會遇到最蛋疼的兩種Conflict一個是project.pbxproj,另外一個就是StoryBoard或XIB。因為這些文件的內(nèi)容的可讀性非常差,雖然蘋果在XCode5(現(xiàn)在我有點不確定是不是這個版本了)中對StoryBoard的文件描述方式做了一定的優(yōu)化,但只是把可讀性從非常差提升為很差。
然而在StoryBoard中往往包含了多個頁面,這些頁面基本上不太可能都由一個人去完成,如果另一個人在做StoryBoard的操作的時候,出于某些目的動了一下不屬于他的那個頁面,比如為了美觀調(diào)整了一下位置。然后另外一個人也因為要添加一個頁面,而在Storyboard中調(diào)整了一下某個其他頁面的位置。那么針對這個情況我除了說個呵呵以外,我就只能說:祝你好運?辞宄,這還沒動具體的頁頁面內(nèi)容呢。
但如果使用代碼繪制View,Conflict一樣會發(fā)生,但是這種Conflict就好解很多了,你懂的。
需求變化非常頻繁,產(chǎn)品經(jīng)理一時一個主意,為了完成需求而針對現(xiàn)有代碼進(jìn)行微調(diào)的情況,以及針對現(xiàn)有代碼的部分復(fù)用的情況也比較多。
我覺得產(chǎn)品經(jīng)理一時一個主意不是他的錯,他說不定也是被逼的,比如誰都會來摻和一下產(chǎn)品的設(shè)計,公司里的所有人,上至CEO,下至基層員工都有可能對產(chǎn)品設(shè)計評頭論足,只要他個人有個地方用得不爽(極大可能是個人喜好)然后又正好跟產(chǎn)品經(jīng)理比較熟悉能夠搭得上話,都會提出各種意見。產(chǎn)品經(jīng)理躲不起也惹不起,有時也是沒辦法,嗯。
但落實到工程師這邊來,這種情況就很蛋疼。因為這種改變有時候不光是UI,UI所對應(yīng)的邏輯也有要改的可能,工程師就會兩邊文件都改,你原來link的那個view現(xiàn)在不link了,然后你的outlet對應(yīng)也要刪掉,這兩部分只要有一個沒做,編譯通過之后跑一下App,一會兒就crash了?雌饋磉@不是什么大事兒,但很影響心情。
另外,如果出現(xiàn)部分的代碼復(fù)用,比如說某頁面下某個View也希望放在另外一個頁面里,相關(guān)的操作就不是復(fù)制粘貼這么簡單了,你還得重新link一遍。也很影響心情。
復(fù)雜界面元素,復(fù)雜動畫交互場景的開發(fā)任務(wù)比較多。
要是想在基于StoryBoard的項目中做一個動畫,很煩。做幾個復(fù)雜界面元素,也很煩。有的時候我們掛Custom View上去,其實在StoryBoard里面看來就是一個空白View。然后另外一點就是,當(dāng)你的layout出現(xiàn)問題需要調(diào)整的時候,還是挺難找到問題所在的,尤其是在復(fù)雜界面元素的情況下。
所以在針對View層這邊的要求時,我也是建議不要用StoryBoard。實現(xiàn)簡單的東西,用Code一樣簡單,實現(xiàn)復(fù)雜的東西,Code比StoryBoard更簡單。所以我更加提倡用code去畫view而不是storyboard。
是否有必要讓業(yè)務(wù)方統(tǒng)一派生ViewController
有的時候我們出于記錄用戶操作行為數(shù)據(jù)的需要,或者統(tǒng)一配置頁面的目的,會從UIViewController里面派生一個自己的ViewController,來執(zhí)行一些通用邏輯。比如天貓客戶端要求所有的ViewController都要繼承自TMViewController。這個統(tǒng)一的父類里面針對一個ViewController的所有生命周期都做了一些設(shè)置,至于這里都有哪些設(shè)置對于本篇文章來說并不重要。在這里我想討論的是,在設(shè)計View架構(gòu)時,如果為了能夠達(dá)到統(tǒng)一設(shè)置或執(zhí)行統(tǒng)一邏輯的目的,使用派生的手段是有必要的嗎?
我覺得沒有必要,為什么沒有必要?
  • 使用派生比不使用派生更容易增加業(yè)務(wù)方的使用成本
  • 不使用派生手段一樣也能達(dá)到統(tǒng)一設(shè)置的目的
這兩條原因是我認(rèn)為沒有必要使用派生手段的理由,如果兩條理由你都心領(lǐng)神會,那么下面的就可以不用看了。如果你還有點疑惑,請看下面我來詳細(xì)講一下原因。
為什么使用了派生,業(yè)務(wù)方的使用成本會提升?
其實不光是業(yè)務(wù)方的使用成本,架構(gòu)的維護(hù)成本也會上升。那么具體的成本都來自于哪里呢?
  • 集成成本
這里講的集成成本是這樣的:如果業(yè)務(wù)方自己開了一個獨立demo,快速完成了某個獨立流程,現(xiàn)在他想把這個現(xiàn)有流程集合進(jìn)去。那么問題就來了,他需要把所有獨立的UIViewController改變成TMViewController。那為什么不是一開始就立刻使用TMViewController呢?因為要想引入TMViewController,就要引入整個天貓App所有的業(yè)務(wù)線,所有的基礎(chǔ)庫,因為這個父類里面涉及很多天貓環(huán)境才有的內(nèi)容,所謂拔出蘿卜帶出泥,你要是想簡單繼承一下就能搞定的事情,搭環(huán)境就要搞半天,然后這個小Demo才能跑得起來。
對于業(yè)務(wù)層存在的所有父類來說,它們是很容易跟項目中的其他代碼糾纏不清的,這使得業(yè)務(wù)方開發(fā)時遇到一個兩難問題:要么把所有依賴全部搞定,然后基于App環(huán)境(比如天貓)下開發(fā)Demo,要么就是自己Demo寫好之后,按照環(huán)境要求改代碼。這里的兩難問題都會帶來成本,都會影響業(yè)務(wù)方的迭代進(jìn)度。
我不確定各位所在公司是否會有這樣的情況,但我可以在這里給大家舉一個我在阿里的真實的例子:我最近在開發(fā)某濾鏡Demo和相關(guān)頁面流程,最終是要合并到天貓這個App里面去的。使用天貓環(huán)境進(jìn)行開發(fā)的話,pod install完所有依賴差不多需要10分鐘,然后打開workspace之后,差不多要再等待1分鐘讓xcode做好索引,然后才能正式開始工作。在這里要感謝一下則平,因為他在此基礎(chǔ)上做了很多優(yōu)化,使得這個1分鐘已經(jīng)比原來的時間短很多了。但如果天貓環(huán)境有更新,你就要再重復(fù)一次上面的流程,否則 就很有可能編譯不過。
拜托,我只是想做個Demo而已,不想搞那么復(fù)雜。
  • 上手接受成本
新來的業(yè)務(wù)工程師有的時候不見得都記得每一個ViewController都必須要派生自TMViewController而不是直接的UIViewController。新來的工程師他不能直接按照蘋果原生的做法去做事情,他需要額外學(xué)習(xí),比如說:所有的ViewController都必須繼承自TMViewController。
  • 架構(gòu)的維護(hù)難度
盡可能少地使用繼承能提高項目的可維護(hù)性,具體內(nèi)容我在《跳出面向?qū)ο笏枷耄ㄒ唬?繼承》里面說了,在這里我想偷懶不想把那篇文章里說過的東西再說一遍。
其實對于業(yè)務(wù)方來說,主要還是第一個集成成本比較蛋疼,因為這是長痛,每次要做點什么事情都會遇到。第二點倒還好,短痛。第三點跟業(yè)務(wù)工程師沒啥關(guān)系。
那么如果不使用派生,我們應(yīng)該使用什么手段?
我的建議是使用AOP。
在架構(gòu)師實現(xiàn)具體的方案之前,必須要想清楚幾個問題,然后才能決定采用哪種方案。是哪幾個問題?
  • 方案的效果,和最終要達(dá)到的目的是什么?
  • 在自己的知識體系里面,是否具備實現(xiàn)這個方案的能力?
  • 在業(yè)界已有的開源組件里面,是否有可以直接拿來用的輪子?
這三個問題按照順序一一解答之后,具體方案就能出來了。
我們先看第一個問題:方案的效果,和最終要達(dá)到的目的是什么?
方案的效果應(yīng)該是:
  • 業(yè)務(wù)方可以不用通過繼承的方法,然后框架能夠做到對ViewController的統(tǒng)一配置。
  • 業(yè)務(wù)方即使脫離框架環(huán)境,不需要修改任何代碼也能夠跑完代碼。業(yè)務(wù)方的ViewController一旦丟入框架環(huán)境,不需要修改任何代碼,框架就能夠起到它應(yīng)該起的作用。
其實就是要實現(xiàn)不通過業(yè)務(wù)代碼上對框架的主動迎合,使得業(yè)務(wù)能夠被框架感知這樣的功能。細(xì)化下來就是兩個問題,框架要能夠攔截到ViewController的生命周期,另一個問題就是,攔截的定義時機。
對于方法攔截,很容易想到Method Swizzling,那么我們可以寫一個實例,在App啟動的時候添加針對UIViewController的方法攔截,這是一種做法。還有另一種做法就是,使用NSObject的load函數(shù),在應(yīng)用啟動時自動監(jiān)聽。使用后者的好處在于,這個模塊只要被項目包含,就能夠發(fā)揮作用,不需要在項目里面添加任何代碼。
然后另外一個要考慮的事情就是,原有的TMViewController(所謂的父類)也是會提供額外方法方便子類使用的,Method Swizzling只支持針對現(xiàn)有方法的操作,拓展方法的話,嗯,當(dāng)然是用Category啦。
我本人不贊成Category的過度使用,但鑒于Category是最典型的化繼承為組合的手段,在這個場景下還是適合使用的。還有的就是,關(guān)于Method Swizzling手段實現(xiàn)方法攔截,業(yè)界也已經(jīng)有了現(xiàn)成的開源庫:Aspects,我們可以直接拿來使用。
我這邊有個非常非常小的Demo可以放出來給大家,這個Demo只是一個點睛之筆,有一些話我也寫在這個Demo里面了,各位架構(gòu)師們你們可以基于各自公司App的需求去拓展。
這個Demo不包含Category,畢竟Category還是得你們自己去寫啊~然后這套方案能夠完成原來通過派生手段所有可以完成的任務(wù),但同時又允許業(yè)務(wù)方不必添加任何代碼,直接使用原生的UIViewController。
然后另外要提醒的是,這方案的目的是消除不必要的繼承,雖然不限定于UIViewController,但它也是有適用范圍的,在適用繼承的地方,還是要老老實實使用繼承。比如你有一個數(shù)據(jù)模型,是由基本模型派生出的一整套模型,那么這個時候還是老老實實使用繼承。至于拿捏何時使用繼承,相信各位架構(gòu)師一定能夠處理好,或者你也可以參考我前面提到的那篇文章來控制拿捏的尺度。
關(guān)于MVC、MVVM等一大堆思想
其實這些都是相對通用的思想,萬變不離其宗的還是在開篇里面我提到的那三個角色:數(shù)據(jù)管理者,數(shù)據(jù)加工者,數(shù)據(jù)展示者。這些五花八門的思想,不外乎就是制訂了一個規(guī)范,規(guī)定了這三個角色應(yīng)當(dāng)如何進(jìn)行數(shù)據(jù)交換。但同時這些也是爭議最多的話題,所以我在這里來把幾個主流思想做一個梳理,當(dāng)你在做View層架構(gòu)時,能夠有個比較好的參考。
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)了一個數(shù)據(jù)流調(diào)配的功能。正在我寫這篇文章的時候,我看到InfoQ發(fā)了這篇文章,里面提到了一個移動開發(fā)中的痛點是:對MVC架構(gòu)劃分的理解。我當(dāng)時沒能夠去參加這個座談會,也沒辦法發(fā)表個人意見,所以就只能在這里寫寫了。
在iOS開發(fā)領(lǐng)域,我們應(yīng)當(dāng)如何進(jìn)行MVC的劃分?
這里面其實有兩個問題:
  • 為什么我們會糾結(jié)于iOS開發(fā)領(lǐng)域中MVC的劃分問題?
  • 在iOS開發(fā)領(lǐng)域中,怎樣才算是劃分的正確姿勢?
為什么我們會糾結(jié)于iOS開發(fā)領(lǐng)域中MVC的劃分問題?
關(guān)于這個,每個人糾結(jié)的點可能不太一樣,我也不知道當(dāng)時座談會上大家的觀點。但請允許我猜一下:是不是因為UIViewController中自帶了一個View,且控制了View的整個生命周期(viewDidLoad,viewWillAppear...),而在常識中我們都知道Controller不應(yīng)該和View有如此緊密的聯(lián)系,所以才導(dǎo)致大家對劃分產(chǎn)生困惑?,下面我會針對這個猜測來給出我的意見。
在服務(wù)端開發(fā)領(lǐng)域,Controller和View的交互方式一般都是這樣,比如Yii:

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不會去做任何關(guān)于View的事情,包括生成View,這些都由渲染引擎代勞了。這是一個區(qū)別,但其實服務(wù)端View的概念和Native應(yīng)用View的概念,真正的區(qū)別在于:從概念上嚴(yán)格劃分的話,服務(wù)端其實根本沒有View,拜HTTP協(xié)議所賜,我們平時所討論的View只是用于描述View的字符串(更實質(zhì)的應(yīng)該稱之為數(shù)據(jù)),真正的View是瀏覽器。。
所以服務(wù)端只管生成對View的描述,至于對View的長相,UI事件監(jiān)聽和處理,都是瀏覽器負(fù)責(zé)生成和維護(hù)的。但是在Native這邊來看,原本屬于瀏覽器的任務(wù)也逃不掉要自己做。那么這件事情由誰來做最合適?蘋果給出的答案是:UIViewController。
鑒于蘋果在這一層做了很多艱苦卓絕的努力,讓iOS工程師們不必親自去實現(xiàn)這些內(nèi)容。而且,它把所有的功能都放在了UIView上,并且把UIView做成不光可以展示UI,還可以作為容器的一個對象。
看到這兒你明白了嗎?UIView的另一個身份其實是容器!UIViewController中自帶的那個view,它的主要任務(wù)就是作為一個容器。如果它所有的相關(guān)命名都改成ViewContainer,那么代碼就會變成這樣:

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];

}

... ...


僅僅改了個名字,現(xiàn)在是不是感覺清晰了很多?如果再要說詳細(xì)一點,我們平常所認(rèn)為的服務(wù)端MVC是這樣劃分的:

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

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

唯一區(qū)別在于,View的容器在服務(wù)端,是由Browser負(fù)責(zé),在整個網(wǎng)站的流程中,這個容器放在Browser是非常合理的。在iOS客戶端,View的容器是由UIViewController中的view負(fù)責(zé),我也覺得蘋果做的這個選擇是非常正確明智的。
因為瀏覽器和服務(wù)端之間的關(guān)系非常松散,而且他們分屬于兩個不同陣營,服務(wù)端將對View的描述生成之后,交給瀏覽器去負(fù)責(zé)展示,然而一旦view上有什么事件產(chǎn)生,基本上是很少傳遞到服務(wù)器(也就是所謂的Controller)的(要傳也可以:AJAX),都是在瀏覽器這邊把事情都做掉,所以在這種情況下,View容器就適合放在瀏覽器(V)這邊。
但是在iOS開發(fā)領(lǐng)域,雖然也有讓View去監(jiān)聽事件的做法,但這種做法非常少,都是把事件回傳給Controller,然后Controller再另行調(diào)度。所以這時候,View的容器放在Controller就非常合適。Controller可以因為不同事件的產(chǎn)生去很方便地更改容器內(nèi)容,比如加載失敗時,把容器內(nèi)容換成失敗頁面的View,無網(wǎng)絡(luò)時,把容器頁面換成無網(wǎng)絡(luò)的View等等。
在iOS開發(fā)領(lǐng)域中,怎樣才算是MVC劃分的正確姿勢?
這個問題其實在上面已經(jīng)解答掉一部分了,那么這個問題的答案就當(dāng)是對上面問題的一個總結(jié)吧。
M應(yīng)該做的事:
  • 給ViewController提供數(shù)據(jù)
  • 給ViewController存儲數(shù)據(jù)提供接口
  • 提供經(jīng)過抽象的業(yè)務(wù)基本組件,供Controller調(diào)度
C應(yīng)該做的事:
  • 管理View Container的生命周期
  • 負(fù)責(zé)生成所有的View實例,并放入View Container
  • 監(jiān)聽來自View與業(yè)務(wù)有關(guān)的事件,通過與Model的合作,來完成對應(yīng)事件的業(yè)務(wù)。
V應(yīng)該做的事:
  • 響應(yīng)與業(yè)務(wù)無關(guān)的事件,并因此引發(fā)動畫效果,點擊反饋(如果合適的話,盡量還是放在View去做)等。
  • 界面元素表達(dá)
我通過與服務(wù)端MVC劃分的對比來回答了這兩個問題,之所以這么做,是因為我知道有很多iOS工程師之前是從服務(wù)端轉(zhuǎn)過來的。我也是這樣,在進(jìn)安居客之前,我也是做服務(wù)端開發(fā)的,在學(xué)習(xí)iOS的過程中,我也曾經(jīng)對iOS領(lǐng)域的MVC劃分問題產(chǎn)生過疑惑,我疑惑的點就是前面開篇我猜測的點。如果有人問我iOS中應(yīng)該怎么做MVC的劃分,我就會像上面這么回答。
MVCS
蘋果自身就采用的是這種架構(gòu)思路,從名字也能看出,也是基于MVC衍生出來的一套架構(gòu)。從概念上來說,它拆分的部分是Model部分,拆出來一個Store。這個Store專門負(fù)責(zé)數(shù)據(jù)存取。但從實際操作的角度上講,它拆開的是Controller。
這算是瘦Model的一種方案,瘦Model只是專門用于表達(dá)數(shù)據(jù),然后存儲、數(shù)據(jù)處理都交給外面的來做。MVCS使用的前提是,它假設(shè)了你是瘦Model,同時數(shù)據(jù)的存儲和處理都在Controller去做。所以對應(yīng)到MVCS,它在一開始就是拆分的Controller。因為Controller做了數(shù)據(jù)存儲的事情,就會變得非常龐大,那么就把Controller專門負(fù)責(zé)存取數(shù)據(jù)的那部分抽離出來,交給另一個對象去做,這個對象就是Store。這么調(diào)整之后,整個結(jié)構(gòu)也就變成了真正意義上的MVCS。
關(guān)于胖Model和瘦Model
我在面試和跟別人聊天時,發(fā)現(xiàn)知道胖Model和瘦Model的概念的人不是很多。大約兩三年前國外業(yè)界曾經(jīng)對此有過非常激烈的討論,主題就是Fat model, skinny controller,F(xiàn)在關(guān)于這方面的討論已經(jīng)不多了,然而直到今天胖Model和瘦Model哪個更好,業(yè)界也還沒有定論,所以這算是目前業(yè)界懸而未解的一個爭議。我很少看到國內(nèi)有討論這個的資料,所以在這里我打算補充一下什么叫胖Model什么叫瘦Model。以及他們的爭論來源于何處。
  • 什么叫胖Model?
胖Model包含了部分弱業(yè)務(wù)邏輯。胖Model要達(dá)到的目的是,Controller從胖Model這里拿到數(shù)據(jù)之后,不用額外做操作或者只要做非常少的操作,就能夠?qū)?shù)據(jù)直接應(yīng)用在View上。舉個例子:

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小時前、一天前、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)注強業(yè)務(wù)代碼就行了。眾所周知,強業(yè)務(wù)變動的可能性要比弱業(yè)務(wù)大得多,弱業(yè)務(wù)相對穩(wěn)定,所以弱業(yè)務(wù)塞進(jìn)Model里面是沒問題的。另一方面,弱業(yè)務(wù)重復(fù)出現(xiàn)的頻率要大于強業(yè)務(wù),對復(fù)用性的要求更高,如果這部分業(yè)務(wù)寫在Controller,類似的代碼會灑得到處都是,一旦弱業(yè)務(wù)有修改(弱業(yè)務(wù)修改頻率低不代表就沒有修改),這個事情就是一個災(zāi)難。如果塞到Model里面去,改一處很多地方就能跟著改,就能避免這場災(zāi)難。
然而其缺點就在于,胖Model相對比較難移植,雖然只是包含弱業(yè)務(wù),但好歹也是業(yè)務(wù),遷移的時候很容易拔出蘿卜帶出泥。另外一點,MVC的架構(gòu)思想更加傾向于Model是一個Layer,而不是一個Object,不應(yīng)該把一個Layer應(yīng)該做的事情交給一個Object去做。最后一點,軟件是會成長的,F(xiàn)atModel很有可能隨著軟件的成長越來越Fat,最終難以維護(hù)。
  • 什么叫瘦Model?
瘦Model只負(fù)責(zé)業(yè)務(wù)數(shù)據(jù)的表達(dá),所有業(yè)務(wù)無論強弱一律扔到Controller。瘦Model要達(dá)到的目的是,盡一切可能去編寫細(xì)粒度Model,然后配套各種helper類或方法來對弱業(yè)務(wù)做抽象,強業(yè)務(wù)依舊交給Controller。舉個例子:

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ù)可以交給任何一個能處理它數(shù)據(jù)的Helper或其他的對象,來完成業(yè)務(wù)。在代碼遷移的時候獨立性很強,很少會出現(xiàn)拔出蘿卜帶出泥的情況。另外,由于SlimModel只是數(shù)據(jù)表達(dá),對它進(jìn)行維護(hù)基本上是0成本,軟件膨脹得再厲害,SlimModel也不會大到哪兒去。
缺點就在于,Helper這種做法也不見得很好,這里有一篇文章批判了這個事情。另外,由于Model的操作會出現(xiàn)在各種地方,SlimModel在一定程度上違背了DRY(Don't Repeat Yourself)的思路,Controller仍然不可避免在一定程度上出現(xiàn)代碼膨脹。
我的態(tài)度?嗯,我會在本門心法這一節(jié)里面說。
說回來,MVCS是基于瘦Model的一種架構(gòu)思路,把原本Model要做的很多事情中的其中一部分關(guān)于數(shù)據(jù)存儲的代碼抽象成了Store,在一定程度上降低了Controller的壓力。
MVVM
MVVM去年在業(yè)界討論得非常多,無論國內(nèi)還是國外都討論得非常熱烈,尤其是在ReactiveCocoa這個庫成熟之后,ViewModel和View的信號機制在iOS下終于有了一個相對優(yōu)雅的實現(xiàn)。MVVM本質(zhì)上也是從MVC中派生出來的思想,MVVM著重想要解決的問題是盡可能地減少Controller的任務(wù)。不管MVVM也好,MVCS也好,他們的共識都是Controller會隨著軟件的成長,變很大很難維護(hù)很難測試。只不過兩種架構(gòu)思路的前提不同,MVCS是認(rèn)為Controller做了一部分Model的事情,要把它拆出來變成Store,MVVM是認(rèn)為Controller做了太多數(shù)據(jù)加工的事情,所以MVVM把數(shù)據(jù)加工的任務(wù)從Controller中解放了出來,使得Controller只需要專注于數(shù)據(jù)調(diào)配的工作,ViewModel則去負(fù)責(zé)數(shù)據(jù)加工并通過通知機制讓View響應(yīng)ViewModel的改變。
MVVM是基于胖Model的架構(gòu)思路建立的,然后在胖Model中拆出兩部分:Model和ViewModel。關(guān)于這個觀點我要做一個額外解釋:胖Model做的事情是先為Controller減負(fù),然后由于Model變胖,再在此基礎(chǔ)上拆出ViewModel,跟業(yè)界普遍認(rèn)知的MVVM本質(zhì)上是為Controller減負(fù)這個說法并不矛盾,因為胖Model做的事情也是為Controller減負(fù)。
另外,我前面說MVVM把數(shù)據(jù)加工的任務(wù)從Controller中解放出來,跟MVVM拆分的是胖Model也不矛盾。要做到解放Controller,首先你得有個胖Model,然后再把這個胖Model拆成Model和ViewModel。
那么MVVM究竟應(yīng)該如何實現(xiàn)?
這很有可能是大多數(shù)人糾結(jié)的問題,我打算憑我的個人經(jīng)驗試圖在這里回答這個問題,歡迎大家在評論區(qū)交流。
在iOS領(lǐng)域大部分MVVM架構(gòu)都會使用ReactiveCocoa,但是使用ReactiveCocoa的iOS應(yīng)用就是基于MVVM架構(gòu)的嗎?那當(dāng)然不是,我覺得很多人都存在這個誤區(qū),我面試過的一些人提到了ReactiveCocoa也提到了MVVM,但他們對此的理解膚淺得讓我忍俊不禁。嗯,在網(wǎng)絡(luò)層架構(gòu)我會舉出不使用ReactiveCocoa的例子,現(xiàn)在舉我感覺有點兒早。
MVVM的關(guān)鍵是要有View Model!而不是ReactiveCocoa(勘誤2)
ViewModel做什么事情?就是把RawData變成直接能被View使用的對象的一種Model。舉個例子:

1
2
3
4
5
6
7
8
    Raw Data:

        {

            (

                (123, 456),

                (234, 567),

                (345, 678)

            )

        }


這里的RawData我們假設(shè)是經(jīng)緯度,數(shù)字我隨便寫的不要太在意。然后你有一個模塊是地圖模塊,把經(jīng)緯度數(shù)組全部都轉(zhuǎn)變成MKAnnotation或其派生類對于Controller來說是弱業(yè)務(wù),(記住,胖Model就是用來做弱業(yè)務(wù)的),因此我們用ViewModel直接把它轉(zhuǎn)變成MKAnnotation的NSArray,交給Controller之后Controller直接就可以用了。
嗯,這就是ViewModel要做的事情,是不是覺得很簡單,看不出優(yōu)越性?
安居客Pad應(yīng)用也有一個地圖模塊,在這里我設(shè)計了一個對象叫做reformer(其實就是ViewModel),專門用來干這個事情。那么這么做的優(yōu)越性體現(xiàn)在哪兒呢?
安居客分三大業(yè)務(wù):租房、二手房、新房。這三個業(yè)務(wù)對應(yīng)移動開發(fā)團(tuán)隊有三個API開發(fā)團(tuán)隊,他們各自為政,這就造成了一個結(jié)果:三個API團(tuán)隊回饋給移動客戶端的數(shù)據(jù)內(nèi)容雖然一致,但是數(shù)據(jù)格式是不一致的,也就是相同value對應(yīng)的key是不一致的。但展示地圖的ViewController不可能寫三個,所以肯定少不了要有一個API數(shù)據(jù)兼容的邏輯,這個邏輯我就放在reformer里面去做了,于是業(yè)務(wù)流程就變成了這樣:

這么一來,原本復(fù)雜的MKAnnotation組裝邏輯就從Controller里面拆分了出來,Controller可以直接拿著Reformer返回的數(shù)據(jù)進(jìn)行展示。APIManager就屬于Model,reformer就屬于ViewModel。具體關(guān)于reformer的東西我會放在網(wǎng)絡(luò)層架構(gòu)來詳細(xì)解釋。Reformer此時扮演的ViewModel角色能夠很好地給Controller減負(fù),同時,維護(hù)成本也大大降低,經(jīng)過reformer產(chǎn)出的永遠(yuǎn)都是MKAnnotation,Controller可以直接拿來使用。
然后另外一點,還有一個業(yè)務(wù)需求是取附近的房源,地圖API請求是能夠hold住這個需求的,那么其他地方都不用變,在fetchDataWithReformer的時候換一個reformer就可以了,其他的事情都交給reformer。
那么ReactiveCocoa應(yīng)該扮演什么角色?
不用ReactiveCocoa也能MVVM,用ReactiveCocoa能更好地體現(xiàn)MVVM的精髓。前面我舉到的例子只是數(shù)據(jù)從API到View的方向,View的操作也會產(chǎn)生"數(shù)據(jù)",只不過這里的"數(shù)據(jù)"更多的是體現(xiàn)在表達(dá)用戶的操作上,比如輸入了什么內(nèi)容,那么數(shù)據(jù)就是text、選擇了哪個cell,那么數(shù)據(jù)就是indexPath。那么在數(shù)據(jù)從view走向API或者Controller的方向上,就是ReactiveCocoa發(fā)揮的地方。
我們知道,ViewModel本質(zhì)上算是Model層(因為是胖Model里面分出來的一部分),所以View并不適合直接持有ViewModel,那么View一旦產(chǎn)生數(shù)據(jù)了怎么辦?扔信號扔給ViewModel,用誰扔?ReactiveCocoa。
在MVVM中使用ReactiveCocoa的第一個目的就是如上所說,View并不適合直接持有ViewModel。第二個目的就在于,ViewModel有可能并不是只服務(wù)于特定的一個View,使用更加松散的綁定關(guān)系能夠降低ViewModel和View之間的耦合度。
那么在MVVM中,Controller扮演什么角色?
大部分國內(nèi)外資料闡述MVVM的時候都是這樣排布的:View <-> ViewModel <-> Model,造成了MVVM不需要Controller的錯覺,現(xiàn)在似乎發(fā)展成業(yè)界開始出現(xiàn)MVVM是不需要Controller的。的聲音了。其實MVVM是一定需要Controller的參與的,雖然MVVM在一定程度上弱化了Controller的存在感,并且給Controller做了減負(fù)瘦身(這也是MVVM的主要目的)。但是,這并不代表MVVM中不需要Controller,MMVC和MVVM他們之間的關(guān)系應(yīng)該是這樣:

(來源:http://www.sprynthesis.com/2014/ ... -mvvm-introduction/)

View <-> C <-> ViewModel <-> Model,所以使用MVVM之后,就不需要Controller的說法是不正確的。嚴(yán)格來說MVVM其實是MVCVM。從圖中可以得知,Controller夾在View和ViewModel之間做的其中一個主要事情就是將View和ViewModel進(jìn)行綁定。在邏輯上,Controller知道應(yīng)當(dāng)展示哪個View,Controller也知道應(yīng)當(dāng)使用哪個ViewModel,然而View和ViewModel它們之間是互相不知道的,所以Controller就負(fù)責(zé)控制他們的綁定關(guān)系,所以叫Controller/控制器就是這個原因。
前面扯了那么多,其實歸根結(jié)底就是一句話:在MVC的基礎(chǔ)上,把C拆出一個ViewModel專門負(fù)責(zé)數(shù)據(jù)處理的事情,就是MVVM。然后,為了讓View和ViewModel之間能夠有比較松散的綁定關(guān)系,于是我們使用ReactiveCocoa,因為蘋果本身并沒有提供一個比較適合這種情況的綁定方法。iOS領(lǐng)域里KVO,Notification,block,delegate和target-action都可以用來做數(shù)據(jù)通信,從而來實現(xiàn)綁定,但都不如ReactiveCocoa提供的RACSignal來的優(yōu)雅,如果不用ReactiveCocoa,綁定關(guān)系可能就做不到那么松散那么好,但并不影響它還是MVVM。
在實際iOS應(yīng)用架構(gòu)中,MVVM應(yīng)該出現(xiàn)在了大部分創(chuàng)業(yè)公司或者老牌公司新App的iOS應(yīng)用架構(gòu)圖中,據(jù)我所知易寶支付旗下的某個iOS應(yīng)用就整體采用了MVVM架構(gòu),他們抽出了一個Action層來裝各種ViewModel,也是屬于相對合理的結(jié)構(gòu)。
所以Controller在MVVM中,一方面負(fù)責(zé)View和ViewModel之間的綁定,另一方面也負(fù)責(zé)常規(guī)的UI邏輯處理。
VIPER
VIPER(View,Interactor,Presenter,Entity,Routing)。VIPER我并沒有實際使用過,我是在objc.io上第13期看到的。
但凡出現(xiàn)一個新架構(gòu)或者我之前并不熟悉的新架構(gòu),有一點我能夠非常肯定,這貨一定又是把MVC的哪個部分給拆開了(壞笑,做這種判斷的理論依據(jù)在第一篇文章里面我已經(jīng)講過了)。事實情況是VIPER確實拆了很多很多,除了View沒拆,其它的都拆了。
我提到的這兩篇文章關(guān)于VIPER都講得很詳細(xì),一看就懂。但具體在使用VIPER的時候會有什么坑或者會有哪些爭議我不是很清楚,硬要寫這一節(jié)的話我只能靠YY,所以我想想還是算了。如果各位讀者有誰在實際App中采用VIPER架構(gòu)的或者對VIPER很有興趣的,可以評論區(qū)里面提出來,我們交流一下。
本門心法
重劍無鋒,大巧不工。 ---- 《神雕俠侶》
這是楊過在挑劍時,玄鐵重劍旁邊寫的一段話。對此我深表認(rèn)同。提到這段話的目的是想告訴大家,在具體做View層架構(gòu)的設(shè)計時,不需要拘泥于MVC、MVVM、VIPER等規(guī)矩。這些都是招式,告訴你你就知道了,然后怎么玩都可以。但是心法不是這樣的,心法是大巧,說出來很簡單,但是能不能在實際架構(gòu)設(shè)計時牢記心法,并且按照規(guī)矩辦事,就都看個人了。
拆分的心法
天下功夫出少林,天下架構(gòu)出MVC。 ---- Casa Taloyum
MVC其實是非常高Level的抽象,意思也就是,在MVC體系下還可以再衍生無數(shù)的架構(gòu)方式,但萬變不離其宗的是,它一定符合MVC的規(guī)范。這句話不是我說的,是我在某個英文資料上看到的,但時過境遷,我已經(jīng)找不到出處了,我很贊同這句話。我采用的架構(gòu)嚴(yán)格來說也是MVC,但也做了很多的拆分。根據(jù)前面幾節(jié)的洗禮,相信各位也明白了這樣的道理:拆分方式的不同誕生了各種不同的衍生架構(gòu)方案(MVCS拆胖Controller,MVVM拆胖Model,VIPER什么都拆),但即便拆分方式再怎么多樣,那都只是招式。而拆分的規(guī)范,就是心法。這一節(jié)我就講講我在做View架構(gòu)時,做拆分的心法。
  • 第一心法:保留最重要的任務(wù),拆分其它不重要的任務(wù)
在iOS開發(fā)領(lǐng)域內(nèi),UIViewController承載了非常多的事情,比如View的初始化,業(yè)務(wù)邏輯,事件響應(yīng),數(shù)據(jù)加工等等,當(dāng)然還有更多我現(xiàn)在也列舉不出來,但是我們知道有一件事情Controller肯定逃不掉要做:協(xié)調(diào)V和M。也就是說,不管怎么拆,協(xié)調(diào)工作是拆不掉的。
那么剩下的事情我們就可以拆了,比如UITableView的DataSource。唐巧的博客有一篇文章提到他和另一個工程師關(guān)于是否要拆分DataSource爭論了好久。拆分DataSource這個做法應(yīng)該也算是通用做法,在不復(fù)雜的應(yīng)用里面,它可能確實看上去只是一個數(shù)組而已,但在復(fù)雜的情況下,它背后可能涉及了文件內(nèi)容讀取,數(shù)據(jù)同步等等復(fù)雜邏輯,這篇文章的第一節(jié)就提倡了這個做法,我其實也蠻提倡的。
前面的文章里面也提了很多能拆的東西,我就不搬運了,大家可以進(jìn)去看看。除了這篇文章提到的內(nèi)容以外,任何比較大的,放在ViewController里面比較臟的,只要不是Controller的核心邏輯,都可以考慮拆出去,然后在架構(gòu)的時候作為一個獨立模塊去定義,以及設(shè)計實現(xiàn)。
  • 第二心法:拆分后的模塊要盡可能提高可復(fù)用性,盡量做到DRY
根據(jù)第一心法拆開來的東西,很有可能還是強業(yè)務(wù)相關(guān)的,這種情況有的時候無法避免。但我們拆也要拆得好看,拆出來的部分最好能夠歸成某一類對象,然后最好能夠抽象出一個通用邏輯出來,使他能夠復(fù)用。即使不能抽出通用邏輯,那也盡量抽象出一個protocol,來實現(xiàn)IOP。這里有篇關(guān)于IOP的文章,大家看了就明白優(yōu)越性了。
第三心法:要盡可能提高拆分模塊后的抽象度
也就是說,拆分的粒度要盡可能大一點,封裝得要透明一些。唐巧說一切隱藏都是對代碼復(fù)雜性的增加,除非它帶來了好處,這在一定程度上有點道理,沒有好處的隱藏確實都不好(笑)。提高抽象度事實上就是增加封裝的力度,將一個負(fù)責(zé)的業(yè)務(wù)抽象成只需要很少的輸入就能完成,就是高度抽象。嗯,繼承很多層,這種做法雖然也提高了抽象程度,但我不建議這么玩。我不確定唐巧在這里說的隱藏跟我說的封裝是不是同一個概念,但我在這里想提倡的是盡可能提高抽象程度。
提高抽象程度的好處在于,對于業(yè)務(wù)方來說,他只需要收集很少的信息(最小充要條件),做很少的調(diào)度(Controller負(fù)責(zé)大模塊調(diào)度,大模塊里面再去做小模塊的調(diào)度),就能夠完成任務(wù),這才是給Controller減負(fù)的正確姿勢。
如果拆分出來的模塊抽象程度不夠,模塊對外界要求的參數(shù)比較多,那么在Controller里面,關(guān)于收集參數(shù)的代碼就會多了很多。如果一部分參數(shù)的收集邏輯能夠由模塊來完成,那也可以做到幫Controller減輕負(fù)擔(dān)。否則就感覺拆得不太干凈,因為Controller里面還是多了一些不必要的參數(shù)收集邏輯。
如果拆分出來的粒度太小,Controller在完成任務(wù)的時候調(diào)度代碼要寫很多,那也不太好。導(dǎo)致拆分粒度小的首要因素就是業(yè)務(wù)可能本身就比較復(fù)雜,拆分粒度小并不是不好,能大就大一點,如果小了,那也沒問題。針對這種情況的處理,就需要采用strategy模式。
針對拆分粒度小的情況,我來舉個實際例子,這個例子來源于我的一個朋友他在做聊天應(yīng)用的消息發(fā)送模塊。當(dāng)消息是文字時,直接發(fā)送。當(dāng)消息是圖片時,需要先向服務(wù)器申請上傳資源,獲得資源ID之后再上傳圖片,上傳圖片完成之后拿到圖片URL,后面帶著URL再把信息發(fā)送出去。
這時候我們拆模塊,可以拆成:數(shù)據(jù)發(fā)送(叫A模塊),上傳資源申請(叫B模塊),內(nèi)容上傳(叫C模塊)。那么要發(fā)送文字消息,Controller調(diào)度A就可以了。如果要發(fā)送圖片消息,Controller調(diào)度B->C->A,假設(shè)將來還有上傳別的類型消息的任務(wù),他們又要依賴D/E/F模塊,那這個事情就很蛋疼,因為邏輯復(fù)雜了,Controller要調(diào)度的東西要區(qū)分的情況就多了,Controller就膨脹了。
那么怎么處理呢?可以采用Strategy模式。我們再來分析一下,Controller要完成任務(wù),它初始情況下所具有的條件是什么?它有這條消息的所有數(shù)據(jù),也知道這個消息的類型。那么它最終需要的是什么呢?消息發(fā)送的結(jié)果:發(fā)送成功或失敗。

上面就是我們要實現(xiàn)的最終結(jié)果,Controller只要把消息丟給MessageSender,然后讓MessageSender去做事情,做完了告訴Controller就好了。那么MessageSender里面怎么去調(diào)度邏輯?MessageSender里面可以有一個StrategyList,里面存放了表達(dá)各種邏輯的Block或者Invocation(Target-Action)。那么我們先定義一個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];

// 然后對外提供一個這樣的接口,同時有一個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];

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

這樣就好啦,即便拆分粒度因為客觀原因無法細(xì)化,那也能把復(fù)雜的判斷邏輯和調(diào)度邏輯從Controller中抽出來,真正為Controller做到了減負(fù)?傊軌蜃龅酱罅6染捅M量大粒度,實在做不到那也行,用Strategy把它hold住。這個例子是小粒度的情況,大粒度的情況太簡單,我就不舉了。
設(shè)計心法
針對View層的架構(gòu)不光是看重如何合理地拆分MVC來給UIViewController減負(fù),另外一點也要照顧到業(yè)務(wù)方的使用成本。最好的情況是業(yè)務(wù)方什么都不知道,然后他把代碼放進(jìn)去就能跑,同時還能獲得框架提供的種種功能。
比如觀眾看臺,就是我覺得最好的設(shè)計,因為沒人會注意到它。
  • 第一心法:盡可能減少繼承層級,涉及蘋果原生對象的盡量不要繼承
繼承是罪惡,盡量不要繼承。就我目前了解到的情況看,除了安居客的Pad App沒有在框架級針對UIViewController有繼承的設(shè)計以外,其它公司或多或少都針對UIViewController有繼承,包括安居客iPhone app(那時候我已經(jīng)對此無能為力,可見View的架構(gòu)在一開始就設(shè)計好有多么重要)。甚至有的還對UITableView有繼承,這是一件多么令人發(fā)指,多么慘絕人寰,多么喪心病狂的事情啊。雖然不可避免的是有些情況我們不得不從蘋果原生對象中繼承,比如UITableViewCell。但我還是建議盡量不要通過繼承的方案來給原生對象添加功能,前面提到的Aspect方案和Category方案都可以使用。用Aspect+load來實現(xiàn)重載函數(shù),用Category來實現(xiàn)添加函數(shù),當(dāng)然,耍點手段用Category來添加property也是沒問題的。這些方案已經(jīng)覆蓋了繼承的全部功能,而且非常好維護(hù),對于業(yè)務(wù)方也更加透明,何樂而不為呢。
不用繼承可能在思路上不會那么直觀,但是對于不使用繼承帶來的好處是足夠頂?shù)蒙鲜褂美^承的壞處的。順便在此我要給Category正一下名:業(yè)界對于Category的態(tài)度比較曖昧,在多種場合(講座、資料文檔)都宣揚過盡可能不要使用Category。它們說的都有一定道理,但我認(rèn)為Category是蘋果提供的最好的使用集合代替繼承的方案,但針對Category的設(shè)計對架構(gòu)師的要求也很高,請合理使用。而且蘋果也在很多場合使用Category,來把一個原本可能很大的對象,根據(jù)不同場景拆分成不同的Category,從而提高可維護(hù)性。
不使用繼承的好處我在這里已經(jīng)說了,放到iOS應(yīng)用架構(gòu)來看,還能再多額外兩個好處:1. 在業(yè)務(wù)方做業(yè)務(wù)開發(fā)或者做Demo時,可以脫離App環(huán)境,或花更少的時間搭建環(huán)境。2. 對業(yè)務(wù)方來說功能更加透明,也符合業(yè)務(wù)方在開發(fā)時的第一直覺。
  • 第二心法:做好代碼規(guī)范,規(guī)定好代碼在文件中的布局,尤其是ViewController
這主要是為了提高可維護(hù)性。在一個文件非常大的對象中,尤其要限制好不同類型的代碼在文件中的布局。比如在寫ViewController時,我之前給團(tuán)隊制定的規(guī)范就是前面一段全部是getter setter,然后接下來一段是life cycle,viewDidLoad之類的方法都在這里。然后下面一段是各種要實現(xiàn)的Delegate,再下面一段就是event response,Button的或者GestureRecognizer的都在這里。然后后面是private method。一般情況下,如果做好拆分,ViewController的private method那一段是沒有方法的。后來隨著時間的推移,我發(fā)現(xiàn)開頭放getter和setter太影響閱讀了,所以后面改成全放在ViewController的最后。
  • 第三心法:能不放在Controller做的事情就盡量不要放在Controller里面去做
Controller會變得龐大的原因,一方面是因為Controller承載了業(yè)務(wù)邏輯,MVC的總結(jié)者(在正式提出MVC之前,或多或少都有人這么設(shè)計,所以說MVC的設(shè)計者不太準(zhǔn)確)對Controller下的定義也是承載業(yè)務(wù)邏輯,所以Controller就是用來干這事兒的,天經(jīng)地義。另一方面是因為在MVC中,關(guān)于Model和View的定義都非常明確,很少有人會把一個屬于M或V的東西放到其他地方。然后除了Model和View以外,還會剩下很多模棱兩可的東西,這些東西從概念上講都算Controller,而且由于M和V定義得那么明確,所以直覺上看,這些東西放在M或V是不合適的,于是就往Controller里面塞咯。
正是由于上述兩方面原因?qū)е铝薈ontroller的膨脹。我們再細(xì)細(xì)思考一下,Model膨脹和View膨脹,要針對它們來做拆分其實都是相對容易的,Controller膨脹之后,拆分就顯得艱難無比。所以如果能夠在一開始就盡量把能不放在Controller做的事情放到別的地方去做,這樣在第一時間就可以讓你的那部分將來可能會被拆分的代碼遠(yuǎn)離業(yè)務(wù)邏輯。所以我們要稍微轉(zhuǎn)變一下思路:模棱兩可的模塊,就不要塞到Controller去了,塞到V或者塞到M或者其他什么地方都比塞進(jìn)Controller好,便于將來拆分。
所以關(guān)于前面我按下不表的關(guān)于胖Model和瘦Model的選擇,我的態(tài)度是更傾向于胖Model。客觀地說,業(yè)務(wù)膨脹之后,代碼規(guī)?隙ㄉ俨涣说,不管你技術(shù)再好,經(jīng)驗再豐富,代碼量最多只能優(yōu)化,該膨脹還是要膨脹的,而且優(yōu)化之后代碼往往也比較難看,使用各種奇技淫巧也是有代價的。所以,針對代碼量優(yōu)化的結(jié)果,往往要么就是犧牲可讀性,要么就是犧牲可移植性(通用性),Every magic always needs a pay, you have to make a trade-off.。
那么既然膨脹出來的代碼,或者將來有可能膨脹的代碼,不管放在MVC中的哪一個部分,最后都是要拆分的,既然遲早要拆分,那不如放Model里面,這樣將來拆分胖Model也能比拆分胖Cotroller更加容易。在我還在安居客的時候,安居客Pad app承載最復(fù)雜業(yè)務(wù)的ViewController才不到600行,其他多數(shù)Controller都是在300-400行之間,這就為后面接手的人降低了非常多的上手難度和維護(hù)復(fù)雜度。拆分出來的東西都是可以直接遷移給iPhone app使用的,F(xiàn)在看天貓的ViewControler,動不動就幾千行,看不了多久頭就暈了,問了一下,大家都表示很習(xí)慣這樣的代碼長度,攤手。
  • 第四心法:架構(gòu)師是為業(yè)務(wù)工程師服務(wù)的,而不是去使喚業(yè)務(wù)工程師的
架構(gòu)師在公司里的職級和地位往往都是要高于業(yè)務(wù)工程師的,架構(gòu)師的技術(shù)實力和經(jīng)驗往往也都是高于業(yè)務(wù)工程師的。所以你值得在公司里獲得較高的地位,但是在公司里的地位高不代表在軟件工程里面的角色地位也高。架構(gòu)師是要為業(yè)務(wù)工程師服務(wù)的,是他們使喚你而不是你使喚他們。另外,制定規(guī)范一方面是起到約束業(yè)務(wù)工程師的代碼,但更重要的一點是,這其實是利用你的能力幫助業(yè)務(wù)工程師避免他無法預(yù)見的危機,所以地位高有一定的好處,畢竟夏蟲不可語冰,有的時候不見得能夠解釋得通,因此高地位隨之而來的就是說服力會比較強。但在軟件工程里,一定要保持謙卑,一定要多為業(yè)務(wù)工程師考慮。
一個不懂這個道理的架構(gòu)師,設(shè)計出來的東西往往復(fù)雜難用,因為他只愿意做核心的東西,周邊不愿意做的都期望交給業(yè)務(wù)工程師去做,甚至有的時候就只做了個Demo,然后就交給業(yè)務(wù)工程師了,業(yè)務(wù)工程師變成給他打工的了。但是一個懂得這個道理的架構(gòu)師,設(shè)計出來的東西會非常好用,業(yè)務(wù)方只需要扔很少的參數(shù)然后拿結(jié)果就好了,這樣的架構(gòu)才叫好的架構(gòu)。
舉一個保存圖片到本地的例子,一種做法是提供這樣的接口:- (NSString *)saveImageWithData:(NSData *)imageData,另一種是- (NSString *)saveImage:(UIImage *)image。后者更好,原因自己想。
你的態(tài)度越謙卑,就越能設(shè)計出好的架構(gòu),這是我設(shè)計心法里的最后一條,也是最重要的一條。即使你現(xiàn)在技術(shù)實力不是業(yè)界大牛級別的,但只要保持這個心態(tài)去做架構(gòu),去做設(shè)計,就已經(jīng)是合格的架構(gòu)師了,要成為業(yè)界大牛也會非?臁
小總結(jié)
其實針對View層的架構(gòu)設(shè)計,還是要做好三點:代碼規(guī)范,架構(gòu)模式,工具集。
代碼規(guī)范對于View層來說意義重大,畢竟View層非常重業(yè)務(wù),如果代碼布局混亂,后來者很難接手,也很難維護(hù)。
架構(gòu)模式具體如何選擇,完全取決于業(yè)務(wù)復(fù)雜度。如果業(yè)務(wù)相當(dāng)相當(dāng)復(fù)雜,那就可以使用VIPER,如果相對簡單,那就直接MVC稍微改改就好了。每一種已經(jīng)成為定式的架構(gòu)模式不見得都適合各自公司對應(yīng)的業(yè)務(wù),所以需要各位架構(gòu)師根據(jù)情況去做一些拆分或者改變。拆分一般都不會出現(xiàn)問題,改變的時候,只要別把MVC三個角色搞混就好了,M該做啥做啥,C該做啥做啥,V該做啥做啥,不要亂來。關(guān)于大部分的架構(gòu)模式應(yīng)該是什么樣子,這篇文章里都已經(jīng)說過了,不過我認(rèn)為最重要的還是后面的心法,模式只是招術(shù),熟悉了心法才能大巧不工。
View層的工具集主要還是集中在如何對View進(jìn)行布局,以及一些特定的View,比如帶搜索提示的搜索框這種。這篇文章只提到了View布局的工具集,其它的工具集相對而言是更加取決于各自公司的業(yè)務(wù)的,各自實現(xiàn)或者使用CocoaPods里現(xiàn)成的都不是很難。
對于小規(guī)模或者中等規(guī)模iOS開發(fā)團(tuán)隊來說,做好以上三點就足夠了。在大規(guī)模團(tuán)隊中,有一個額外問題要考慮,就是跨業(yè)務(wù)頁面調(diào)用方案的設(shè)計。
跨業(yè)務(wù)頁面調(diào)用方案的設(shè)計
跨業(yè)務(wù)頁面調(diào)用是指,當(dāng)一個App中存在A業(yè)務(wù),B業(yè)務(wù)等多個業(yè)務(wù)時,B業(yè)務(wù)有可能會需要展示A業(yè)務(wù)的某個頁面,A業(yè)務(wù)也有可能會調(diào)用其他業(yè)務(wù)的某個頁面。在小規(guī)模的App中,我們直接import其他業(yè)務(wù)的某個ViewController然后或者push或者present,是不會產(chǎn)生特別大的問題的。但是如果App的規(guī)模非常大,涉及業(yè)務(wù)數(shù)量非常多,再這么直接import就會出現(xiàn)問題。

可以看出,跨業(yè)務(wù)的頁面調(diào)用在多業(yè)務(wù)組成的App中會導(dǎo)致橫向依賴。那么像這樣的橫向依賴,如果不去設(shè)法解決,會導(dǎo)致什么樣的結(jié)果?
  • 當(dāng)一個需求需要多業(yè)務(wù)合作開發(fā)時,如果直接依賴,會導(dǎo)致某些依賴層上端的業(yè)務(wù)工程師在前期空轉(zhuǎn),依賴層下端的工程師任務(wù)繁重,而整個需求完成的速度會變慢,影響的是團(tuán)隊開發(fā)迭代速度。
  • 當(dāng)要開辟一個新業(yè)務(wù)時,如果已有各業(yè)務(wù)間直接依賴,新業(yè)務(wù)又依賴某個舊業(yè)務(wù),就導(dǎo)致新業(yè)務(wù)的開發(fā)環(huán)境搭建困難,因為必須要把所有相關(guān)業(yè)務(wù)都塞入開發(fā)環(huán)境,新業(yè)務(wù)才能進(jìn)行開發(fā)。影響的是新業(yè)務(wù)的響應(yīng)速度。
  • 當(dāng)某一個被其他業(yè)務(wù)依賴的頁面有所修改時,比如改名,涉及到的修改面就會特別大。影響的是造成任務(wù)量和維護(hù)成本都上升的結(jié)果。
當(dāng)然,如果App規(guī)模特別小,這三點帶來的影響也會特別小,但是在阿里這樣大規(guī)模的團(tuán)隊中,像天貓/淘寶這樣大規(guī)模的App,一旦遇上這里面哪怕其中一件事情,就特么很坑爹。
那么應(yīng)該怎樣處理這個問題?
讓依賴關(guān)系下沉。
怎么讓依賴關(guān)系下沉?引入Mediator模式。
所謂引入Mediator模式來讓依賴關(guān)系下沉,實質(zhì)上就是每次呼喚頁面的時候,通過一個中間人來召喚另外一個頁面,這樣只要每個業(yè)務(wù)依賴這個中間人就可以了,中間人的角色就可以放在業(yè)務(wù)層的下面一層,這就是依賴關(guān)系下沉。

當(dāng)A業(yè)務(wù)需要調(diào)用B業(yè)務(wù)的某個頁面的時候,將請求交給Mediater,然后由Mediater通過某種手段獲取到B業(yè)務(wù)頁面的實例,交還給A就行了。在具體實現(xiàn)這個機制的過程中,有以下幾個問題需要解決:
  • 設(shè)計一套通用的請求機制,請求機制需要跟業(yè)務(wù)剝離,使得不同業(yè)務(wù)的頁面請求都能夠被Mediater處理
  • 設(shè)計Mediater根據(jù)請求如何獲取其他業(yè)務(wù)的機制,Mediater需要知道如何處理請求,上哪兒去找到需要的頁面
這個看起來就非常像我們web開發(fā)時候的URL機制,發(fā)送一個Get或Post請求,CGI調(diào)用腳本把請求分發(fā)給某個Controller下的某個Action,然后返回HTML字符串到瀏覽器去解析。蘋果本身也實現(xiàn)了一套跨App調(diào)用機制,它也是基于URL機制來運轉(zhuǎn)的,只不過它想要解決的問題是跨App的數(shù)據(jù)交流和頁面調(diào)用,我們想要解決的問題是降低各業(yè)務(wù)的耦合度。
不過我們還不能直接使用蘋果原生的這套機制,因為這套機制不能夠返回對象實例。而我們希望能夠拿到對象實例,這樣不光可以做跨業(yè)務(wù)頁面調(diào)用,也可以做跨業(yè)務(wù)的功能調(diào)用。另外,我們又希望我們的Mediater也能夠跟蘋果原生的跨App調(diào)用兼容,這樣就又能幫業(yè)務(wù)方省掉一部分開發(fā)量。
就我目前所知道的情況,AutoCad旗下某款iOS應(yīng)用(時間有點久我不記得是哪款應(yīng)用了,如果你是AutoCad的iOS開發(fā),可以在評論區(qū)補充一下。)就采用了這種頁面調(diào)用方式。天貓里面目前也在使用這套機制,只是這一塊由于歷史原因存在新老版本混用的情況,因此暫時還沒能夠很好地發(fā)揮應(yīng)有的作用。
嗯,想問我要Demo的同學(xué),我可以很大方地告訴你,沒有。不過我打算抽時間寫一個出來,現(xiàn)在除了已經(jīng)想好名字叫Summon以外,其它什么都沒做,哈哈。
關(guān)于Getter和Setter?
我比較習(xí)慣一個對象的"私有"屬性寫在extension里面,然后這些屬性的初始化全部放在getter里面做,在init和dealloc之外,是不會出現(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)用一個setup方法而已,然后我推薦的方法就是不用多調(diào)一個setup方法,直接走getter。
嗯,怎么說呢,其實兩種做法都能完成需求。但是從另一個角度看,蘋果之所以選擇讓[self getProperty]和self.property可以互相通用,這種做法已經(jīng)很明顯地表達(dá)了蘋果的傾向:希望每個property都是通過getter方法來獲得。
早在2003年,Allen Holub就發(fā)了篇文章《Why getter and setter methods are evil》,自此之后,業(yè)界就對此產(chǎn)生了各種爭議,雖然是從Java開始說的,但是發(fā)展到后面各種語言也參與了進(jìn)來。然后雖然現(xiàn)在關(guān)于這個問題討論得少了,但是依舊屬于沒有定論的狀態(tài)。setter的情況比較復(fù)雜,也不是我這一節(jié)的重點,我這邊還是主要說getter。我們從objc的設(shè)計來看,蘋果的設(shè)計者更加傾向于getter is not evil。
認(rèn)為getter is evil的原因有非常之多,或大或小,隨著爭論的進(jìn)行,大家慢慢就聚焦到這樣的一個原因:Getter和Setter提供了一個能讓外部修改對象內(nèi)部數(shù)據(jù)的方式,這是evil的,正常情況下,一個對象自己私有的變量應(yīng)該是只有自己關(guān)心。
然后我們回到iOS領(lǐng)域來,objc也同樣面臨了這樣的問題,甚至更加嚴(yán)重:objc并沒有像Java那么嚴(yán)格的私有概念。但在實際工作中,我們不太會去操作頭文件里面沒有的變量,這是從規(guī)范上就被禁止的。
認(rèn)為getter is not evil的原因也可以聚焦到一個:高度的封裝性。getter事實上是工廠方法,有了getter之后,業(yè)務(wù)邏輯可以更加專注于調(diào)用,而不必?fù)?dān)心當(dāng)前變量是否可用。我們可以想一下,假設(shè)一個ViewController有20個subview要加入view中,這20個subview的初始化代碼是肯定逃不掉的,放在哪里比較好?放在哪里都比放在addsubview的地方好,我個人認(rèn)為最好的地方還是放在getter里面,結(jié)合單例模式之后,代碼會非常整齊,生產(chǎn)的地方和使用的地方得到了很好的區(qū)分。
所以放到iOS來說,我還是覺得使用getter會比較好,因為evil的地方在iOS這邊基本都避免了,not evil的地方都能享受到,還是不錯的。
總結(jié)
要做一個View層架構(gòu),主要就是從以下三方面入手:
  • 制定良好的規(guī)范
  • 選擇好合適的模式(MVC、MVCS、MVVM、VIPER)
  • 根據(jù)業(yè)務(wù)情況針對ViewController做好拆分,提供一些小工具方便開發(fā)
當(dāng)然,你還會遇到其他的很多問題,這時候你可以參考這篇文章里提出的心法,在后面提到的跨業(yè)務(wù)頁面調(diào)用方案的設(shè)計中,你也能夠看到我的一些心法的影子。
對于iOS客戶端來說,它并不像其他語言諸如Python、PHP他們有那么多的非官方通用框架?陀^原因在于,蘋果已經(jīng)為我們做了非常多的事情,做了很多的努力。在蘋果已經(jīng)做了這么多事情的基礎(chǔ)上,架構(gòu)師要做針對View層的方案時,最好還是盡量遵守蘋果已有的規(guī)范和設(shè)計思想,然后根據(jù)自己過去開發(fā)iOS時的經(jīng)驗,盡可能給業(yè)務(wù)方在開發(fā)業(yè)務(wù)時減負(fù),提高業(yè)務(wù)代碼的可維護(hù)性,就是View層架構(gòu)方案的最大目標(biāo)。
2015-04-28 09:28補:關(guān)于AOP
AOP(Aspect Oriented Programming),面向切片編程,這也是面向XX編程系列術(shù)語之一哈,但它跟我們熟知的面向?qū)ο缶幊虥]什么關(guān)系。
什么是切片?
程序要完成一件事情,一定會有一些步驟,1,2,3,4這樣。這里分解出來的每一個步驟我們可以認(rèn)為是一個切片。
什么是面向切片編程?
你針對每一個切片的間隙,塞一些代碼進(jìn)去,在程序正常進(jìn)行1,2,3,4步的間隙可以跑到你塞進(jìn)去的代碼,那么你寫這些代碼就是面向切片編程。
為什么會出現(xiàn)面向切片編程?
你要想做到在每一個步驟中間做你自己的事情,不用AOP也一樣可以達(dá)到目的,直接往步驟之間塞代碼就好了。但是事實情況往往很復(fù)雜,直接把代碼塞進(jìn)去,主要問題就在于:塞進(jìn)去的代碼很有可能是跟原業(yè)務(wù)無關(guān)的代碼,在同一份代碼文件里面摻雜多種業(yè)務(wù),這會帶來業(yè)務(wù)間耦合。為了降低這種耦合度,我們引入了AOP。
如何實現(xiàn)AOP?
AOP一般都是需要有一個攔截器,然后在每一個切片運行之前和運行之后(或者任何你希望的地方),通過調(diào)用攔截器的方法來把這個jointpoint扔到外面,在外面獲得這個jointpoint的時候,執(zhí)行相應(yīng)的代碼。
在iOS開發(fā)領(lǐng)域,objective-C的runtime有提供了一系列的方法,能夠讓我們攔截到某個方法的調(diào)用,來實現(xiàn)攔截器的功能,這種手段我們稱為Method Swizzling。Aspects通過這個手段實現(xiàn)了針對某個類和某個實例中方法的攔截。
另外,也可以使用protocol的方式來實現(xiàn)攔截器的功能,具體實現(xiàn)方案就是這樣:

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>

這么做對比Method Swizzling有個額外好處就是,你可以通過攔截器來給攔截器的實現(xiàn)者提供更多的信息,便于外部實現(xiàn)更加了解當(dāng)前切片的情況。另外,你還可以更精細(xì)地對切片進(jìn)行劃分。Method Swizzling的切片粒度是函數(shù)粒度的,自己實現(xiàn)的攔截器的切片粒度可以比函數(shù)更小,更加精細(xì)。
缺點就是,你得自己在每一個插入點把調(diào)用攔截器方法的代碼寫上(笑),通過Aspects(本質(zhì)上就是Mehtod Swizzling)來實現(xiàn)的AOP,就能輕松一些。
2015-4-29 14:25 補:關(guān)于在哪兒寫Constraints?
文章發(fā)出來之后,很多人針對勘誤1有很多看法,以至于我覺得很有必要在這里做一份補。期間過程很多很復(fù)雜,這篇文章也已經(jīng)很長了,我就直接說結(jié)果了哈。

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

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];

}


最后,要感謝評論區(qū)各位關(guān)心這個問題,并提出自己意見,甚至是自己親自測試然后告訴我結(jié)果的各位兄弟:@fly2never,@Wythe,@wtlucky,@lcddhr,@李新星,@Meigan Fang,@匿名,@Xiao Moch。
這個做法是目前我自己覺得可能比較合適的做法,當(dāng)然也歡迎其他同學(xué)繼續(xù)拿出自己的看法,我們來討論。
勘誤
我的前同事@ddaajing看了這篇文章之后,給我提出了以下兩個勘誤,和很多行文上的問題。在這里我對他表示非常感謝:
勘誤1:其實在viewWillAppear這里改變UI元素不是很可靠,Autolayout發(fā)生在viewWillAppear之后,嚴(yán)格來說這里通常不做視圖位置的修改,而用來更新Form數(shù)據(jù)。改變位置可以放在viewWilllayoutSubview或者didLayoutSubview里,而且在viewDidLayoutSubview確定UI位置關(guān)系之后設(shè)置autoLayout比較穩(wěn)妥。另外,viewWillAppear在每次頁面即將顯示都會調(diào)用,viewWillLayoutSubviews雖然在lifeCycle里調(diào)用順序在viewWillAppear之后,但是只有在頁面元素需要調(diào)整時才會調(diào)用,避免了Constraints的重復(fù)添加。
勘誤2:MVVM要有ViewModel,以及ReactiveCocoa帶來的信號通知效果,在ReactiveCocoa里就是RAC等相關(guān)宏來實現(xiàn)。另外,使用ReactiveCocoa能夠比較優(yōu)雅地實現(xiàn)MVVM模式,就是因為有RAC等相關(guān)宏的存在。就像它的名字一樣Reactive-響應(yīng)式,這也是區(qū)分MVVM的VM和MVC的C和MVP的P的一個重要方面。
本文遵守CC-BY。 請保持轉(zhuǎn)載后文章內(nèi)容的完整,以及文章出處。本人保留所有版權(quán)相關(guān)權(quán)利。


分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享淘帖 頂 踩
回復(fù)

使用道具 舉報

您需要登錄后才可以回帖 登錄 | 立即注冊

本版積分規(guī)則

手機版|小黑屋|51黑電子論壇 |51黑電子論壇6群 QQ 管理員QQ:125739409;技術(shù)交流QQ群281945664

Powered by 單片機教程網(wǎng)

快速回復(fù) 返回頂部 返回列表