深度重構(gòu)UIViewController

UIViewController是iOS應(yīng)用的基礎(chǔ)單位,每個(gè)iOS程序員都寫過無數(shù)的Controller,今天和大家一起來深度解剖Controller,看看怎么來做一次深度重構(gòu)。

重構(gòu)的前提

我們應(yīng)該謹(jǐn)慎的來重構(gòu)我們的代碼。iOS系統(tǒng)提供的UIViewController一定程度上可以很好的應(yīng)付簡單的頁面單位,對(duì)于復(fù)雜的頁面,我們也可以采用市上主流的MV(X)系列模式,比如MVP,MVVM等,但隨著單個(gè)Controller內(nèi)業(yè)務(wù)進(jìn)一步增長,我們需要更細(xì)粒度的重構(gòu),或者是對(duì)MV(X)做進(jìn)一步的定制。

以下圖映客APP兩個(gè)頁面為主:

左邊頁面元素少且靜態(tài),一個(gè)UITableView就可以應(yīng)付,右邊的直播頁面則元素多且動(dòng)態(tài),傳統(tǒng)的MV(X)也會(huì)顯得顆粒太粗,這類復(fù)雜頁面雖然不常遇到,但往往體現(xiàn)一個(gè)APP的核心功能,合理的搭建或者重構(gòu)這類界面非常重要。

重構(gòu)的本質(zhì)

如何去定義重構(gòu),以我的理解可以歸納為兩個(gè)關(guān)鍵詞:分解,鏈接。

重構(gòu)的前提是復(fù)雜,臃腫,不直觀,重構(gòu)的手段是分解之后再連接。以映客的直播界面為例,UI元素,用戶事件,服務(wù)器交互等基礎(chǔ)元素都非常之多。以一個(gè)簡單的MVP去歸類代碼猶嫌不足,我們需要進(jìn)一步的分解成view1,view2...viewN,presenter1,presenter2...presenterN,model1,model2...modelN,第二個(gè)問題是如何把這一個(gè)個(gè)的類文件或者說功能單位合理的組織連接起來。完成上述兩步我們就完成了一次重構(gòu),每一次將代碼打亂再連接就是一次重構(gòu)。

分解UIViewController

寫了那么多Controller,讓你來說一下Controller都細(xì)分為那些更小的功能單位,你能隨口說出來嗎?只有做了足夠多的業(yè)務(wù),才能慢慢對(duì)Controller的構(gòu)成有自己的理解。

當(dāng)然可以回答書MVP或者M(jìn)VC,但這個(gè)答案粒度太粗,一個(gè)Controller內(nèi)部會(huì)發(fā)生哪些事會(huì)說的更細(xì),我們看下VIPER的答案:

  • <strong>View:</strong> displays what it is told to by the Presenter and relays user input back to the Presenter.
  • <strong>Interactor:</strong> contains the business logic as specified by a use case.
  • <strong>Presenter:</strong>contains view logic for preparing content for display (as received from the Interactor) and for reacting to user inputs (by requesting new data from the Interactor).
  • <strong>Entity:</strong>contains basic model objects used by the Interactor.
  • <strong>Routing:</strong>contains navigation logic for describing which screens are shown in which order

view不用多說可以分解成更多的子View,最后形成一個(gè)樹形結(jié)構(gòu)。

Entity自然是代表的Model。

MVC中的C,MVP中的P,被細(xì)分成Interactor,Presenter,和Routing。這三個(gè)角色各自負(fù)責(zé)什么指責(zé)呢?

Routing比較清楚,處理頁面之間的跳轉(zhuǎn),我見過的項(xiàng)目代碼里,很少將這一部分單獨(dú)拎出來,但其實(shí)很有意義,這部分代表的是不同Controller之間耦合依賴的方式,無論是從類關(guān)系描述的角度還是Debug的角度,都能幫助我們快速定位代碼。

Interactor和Presenter初看起來很類似,似乎都是在處理業(yè)務(wù)邏輯。但業(yè)務(wù)邏輯其實(shí)是個(gè)大的歸類,可以描述任何一種場(chǎng)景和行為。Interactor當(dāng)中有個(gè)很重要的術(shù)語:use case,這個(gè)術(shù)語很多技術(shù)文章中都有遇見,它代表的是一個(gè)完整的,獨(dú)立的,細(xì)分過后的業(yè)務(wù)流程,比如我們APP當(dāng)中的登陸模塊,它是一個(gè)業(yè)務(wù)單元,但它其實(shí)可以進(jìn)一步的細(xì)分為很多use case:

use case1:驗(yàn)證郵箱長度

use case2:密碼長度檢驗(yàn)

use case3:從Server查詢use name是否可用

...

use caseN

定義use case有什么好處呢?

好處當(dāng)然是分門別類,結(jié)構(gòu)清晰。把100本書堆一堆,或者放書架上按類別擺放,下次找書的時(shí)候那種方式你更舒服?獨(dú)立出一個(gè)個(gè)的use case還有一個(gè)好處是方便unit test,如果項(xiàng)目對(duì)每一個(gè)use case都寫有unit test,每次遇到“牽一發(fā)動(dòng)全身”的業(yè)務(wù)更改,可以邊喝茶邊寫代碼。

我見過不少代碼都體現(xiàn)不出use case 的分類,可以回頭看下自己當(dāng)前項(xiàng)目的登陸模塊,上面我提到的這些use case有沒有在類文件中合理擺放,還是都攪在一起?

所以VIPER當(dāng)中Interactor的說法是強(qiáng)化大家寫單獨(dú)的use case的意識(shí),打開interactor.m,看一個(gè)函數(shù)代表一個(gè)use case,同一類的use case再用#pragma mark歸在一塊,別人看你代碼時(shí)能不賞心悅目嗎?

再說到Presenter,Presenter時(shí)上面一個(gè)個(gè)use case的使用者和響應(yīng)者。使用者將個(gè)個(gè)use case串聯(lián)起來描述一個(gè)完整詳細(xì)的業(yè)務(wù)流程,比如我們的登陸模塊,每次用戶點(diǎn)擊按鈕登陸的時(shí)候,會(huì)觸發(fā)一系列的use case,從驗(yàn)證用戶輸入合法性,設(shè)備網(wǎng)絡(luò)狀態(tài),服務(wù)器資源是否可用,到最后處理結(jié)果并展示,這就是一個(gè)完整的業(yè)務(wù)流程,這個(gè)流程由Presenter來描述。響應(yīng)者表示Presenter在接受到服務(wù)器反饋之后進(jìn)一步改變本地的狀態(tài),比如View的展示,新的數(shù)據(jù)修改等,甚至?xí){(diào)用Routing發(fā)生界面跳轉(zhuǎn)。

說到這里就比較明了了,Interactor和Routing是服務(wù)的提供方,Presenter是服務(wù)的使用方和集成方。VIPER說白了不過是對(duì)傳統(tǒng)的MVC當(dāng)中的C做了進(jìn)一步細(xì)分。

能不能分的更細(xì)呢?

當(dāng)然可以,VIPER的做法是一種通用的做法,我們還可以從業(yè)務(wù)的角度去細(xì)分,那映客的直播頁面做例子,比如Presenter當(dāng)中包含了很多業(yè)務(wù)流程:

  • 收到用戶消息并展示

  • 收到禮品消息并展示

  • 收到彈幕消息并展示

  • 收到用戶進(jìn)出房間的時(shí)間,并處理展示

  • 收到XXX,處理并展示

以O(shè)C語言的特性,我們可以生成更多的Presenter Category,來安置這些流程,比如LivePresenter+Message, LivePresenter+Gift, LivePresenter+Danmu, LivePresenter+Room, LivePresenter+XXX。

不要覺的上面幾個(gè)業(yè)務(wù)流程很簡單,一個(gè)Presenter處理綽綽有余,我前段時(shí)間剛好看過別人的一個(gè)直播項(xiàng)目,一個(gè)Persenter類超過1000行代碼很輕松。

還可以進(jìn)一步細(xì)分,一個(gè)功能復(fù)雜繁多的頁面基本上離不開UITableView,而tableView代碼量基本在delegate和datasource。這兩個(gè)職責(zé)當(dāng)然可以放在presenter當(dāng)中,或者我們向Android學(xué)習(xí),把它們獨(dú)立出來放在單獨(dú)的類文件中來處理,比如叫做Adapter 用代碼來說就是:

<pre>
<code>

_tableView.delegate = self.adapter;  
_tableView.dataSource = self.adapter; 

</code>
</pre>

和tableView相關(guān)的代碼都搬到adapter當(dāng)中:

<pre><code>

@protocol UITableViewDelegate

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section;
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section;

@end

@protocol UITableViewDataSource<NSObject>

@required
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

@end

</code></pre>

我們的Presenter更加干凈了,看起來和剛大掃除過的房間一個(gè)干凈整潔,令人心情愉悅。

好了,到這里我們盤子里的牛排已經(jīng)被切成很多小塊了,可以開始享用這些美味的代碼了,繼續(xù)我們的第二部工作:鏈接。

鏈接

先看一下我們分解之后有哪些元素:

view(1…N), model(1…N), interactor, presenter(1…N), routing, adapter??粗鴳?yīng)該粒度夠細(xì)了,對(duì)于復(fù)雜的Controller,我個(gè)人習(xí)慣的做法和VIPER相近,但略有不同,Interactor當(dāng)中的use case通過分層的架構(gòu)被我們放到server layer,分層的架構(gòu)是另一個(gè)話題,這里不做細(xì)述,其他元素基本一致。

至于怎樣鏈接,手段無非就是OC的幾種交互機(jī)制:

<strong>Delegate, Target-Action, Block, Notification, KVO</strong>

這幾者之間的差異可以參考objc.io的一篇經(jīng)典文章。選擇不同對(duì)耦合度,開放便捷性,調(diào)試是否方便等都會(huì)產(chǎn)生影響,如何應(yīng)用不同的機(jī)制將各個(gè)單位串聯(lián)起來就看架構(gòu)師自己的積累和理解了,任何一個(gè)選擇都有其優(yōu)勢(shì)和局限性。

如果拿捏不準(zhǔn)選哪個(gè)好的時(shí)候,我個(gè)人建議使用delegate,樸素可靠且直觀。delegate需要在不同的元素之間傳遞,代碼量會(huì)偏多一些,但優(yōu)點(diǎn)在protocol定義清晰,耦合在哪里一目了然,記得要注意循環(huán)引用的問題。

我早些時(shí)候其他幾種機(jī)制都在實(shí)際項(xiàng)目中做過嘗試,最后綜合比較還是傾向于選擇delegate,一位iOS大神MrPeak利用runtime機(jī)制,做個(gè)一個(gè)CDD機(jī)制來自動(dòng)串聯(lián)各個(gè)功能單位。請(qǐng)看:CDD的詳細(xì)介紹,其本質(zhì)或者說最終目的還是在于鏈接。

說完了分解和鏈接,Controller的重構(gòu)完成了一大半,還剩下一個(gè)重要的概念:狀態(tài)分享。

盡量避免跨類,跨模塊跨層共享狀態(tài)

MrPeak博客里談到過對(duì)于程序狀態(tài)的維護(hù).狀態(tài)是否維護(hù)的好對(duì)于程序的整體穩(wěn)定性很有影響,對(duì)于Controller中狀態(tài)的維護(hù),我有一個(gè)個(gè)簡單的建議:

傳遞狀態(tài)的時(shí)候盡可能copy

之前流行的函數(shù)式編程其實(shí)就很強(qiáng)調(diào)無狀態(tài)性,無狀態(tài)不是讓大家不定義狀態(tài)變量,而是避免函數(shù)之間的狀態(tài)共享,具體到OC當(dāng)中,不要在不同的功能單位里使用指向同一塊內(nèi)存拷貝的地址,為什么共享狀態(tài)是一件危險(xiǎn)的事?

一般來說,我們從Model Layer 或者是數(shù)據(jù)層拿到model的實(shí)例,扔給Controller使用的時(shí)候應(yīng)該是一份新的copy,在不同的類單位里共享NSMutableString或者NSMutableArray,NSMutableDictionary很容易讓你的代碼變得不穩(wěn)定,而且這類不穩(wěn)定性很難調(diào)試,debug填坑的時(shí)候經(jīng)常按下葫蘆漂起瓢。

在Controller內(nèi)部傳遞model或者satate的時(shí)候,我們應(yīng)該也盡量使用copy行為,任何satate你一旦暴露出去就不再安全,自己創(chuàng)建,自己修改,自己銷毀才是正途。

FaceBook當(dāng)中的model layer就是由一個(gè)單獨(dú)的開發(fā)團(tuán)隊(duì)維護(hù)的,應(yīng)用層(Controller層)開發(fā)人員獲取到的都是一個(gè)新的拷貝,要修改某個(gè)屬性不一定有接口,甚至要向model維護(hù)團(tuán)隊(duì)提交增加接口的申請(qǐng),對(duì)于state維護(hù)的謹(jǐn)慎性可見一斑。

使用腳本生成原型代碼

說了這么多,Controller重構(gòu)的關(guān)鍵點(diǎn)就說完了。最后再提個(gè)小Tip,一旦Controller做深度細(xì)分之后,團(tuán)隊(duì)成員需要對(duì)Controller的分法和構(gòu)成有一定的認(rèn)識(shí),寫出來的代碼應(yīng)該保持一致,我的做法是通過腳本的方式生成Controller各個(gè)相關(guān)的的類文件,比如我的Controller是如下結(jié)構(gòu):

通過腳本將文件名和文件內(nèi)容當(dāng)中Template全部替換成目標(biāo)Controller的名字,就省去了很多體力代碼的勞動(dòng),也達(dá)到了代碼風(fēng)格一致的問題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容