UIViewController是iOS應用的基礎單位踪危,每個iOS程序員都寫過無數(shù)的Controller步脓,今天和大家一起來深度解剖Controller,看看怎么來做一次深度重構(gòu)硝皂。
重構(gòu)的前提
我們應該謹慎的來重構(gòu)我們的代碼顷窒。iOS系統(tǒng)提供的UIViewController一定程度上可以很好的應付簡單的頁面單位蛙吏,對于復雜的頁面,我們也可以采用市上主流的MV(X)系列模式鞋吉,比如MVP鸦做,MVVM等,但隨著單個Controller內(nèi)業(yè)務進一步增長谓着,我們需要更細粒度的重構(gòu)泼诱,或者是對MV(X)做進一步的定制。
以下圖映客APP兩個頁面為主:
左邊頁面元素少且靜態(tài)赊锚,一個UITableView就可以應付治筒,右邊的直播頁面則元素多且動態(tài),傳統(tǒng)的MV(X)也會顯得顆粒太粗舷蒲,這類復雜頁面雖然不常遇到耸袜,但往往體現(xiàn)一個APP的核心功能,合理的搭建或者重構(gòu)這類界面非常重要牲平。
重構(gòu)的本質(zhì)
如何去定義重構(gòu)堤框,以我的理解可以歸納為兩個關(guān)鍵詞:分解,鏈接。
重構(gòu)的前提是復雜蜈抓,臃腫启绰,不直觀,重構(gòu)的手段是分解之后再連接资昧。以映客的直播界面為例酬土,UI元素,用戶事件格带,服務器交互等基礎元素都非常之多。以一個簡單的MVP去歸類代碼猶嫌不足刹枉,我們需要進一步的分解成view1叽唱,view2...viewN,presenter1,presenter2...presenterN,model1,model2...modelN,第二個問題是如何把這一個個的類文件或者說功能單位合理的組織連接起來。完成上述兩步我們就完成了一次重構(gòu)微宝,每一次將代碼打亂再連接就是一次重構(gòu)棺亭。
分解UIViewController
寫了那么多Controller,讓你來說一下Controller都細分為那些更小的功能單位蟋软,你能隨口說出來嗎镶摘?只有做了足夠多的業(yè)務,才能慢慢對Controller的構(gòu)成有自己的理解岳守。
當然可以回答書MVP或者MVC凄敢,但這個答案粒度太粗,一個Controller內(nèi)部會發(fā)生哪些事會說的更細湿痢,我們看下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涝缝,最后形成一個樹形結(jié)構(gòu)。
Entity自然是代表的Model譬重。
MVC中的C拒逮,MVP中的P,被細分成Interactor臀规,Presenter滩援,和Routing。這三個角色各自負責什么指責呢塔嬉?
Routing比較清楚玩徊,處理頁面之間的跳轉(zhuǎn),我見過的項目代碼里邑遏,很少將這一部分單獨拎出來佣赖,但其實很有意義,這部分代表的是不同Controller之間耦合依賴的方式记盒,無論是從類關(guān)系描述的角度還是Debug的角度憎蛤,都能幫助我們快速定位代碼。
Interactor和Presenter初看起來很類似,似乎都是在處理業(yè)務邏輯俩檬。但業(yè)務邏輯其實是個大的歸類萎胰,可以描述任何一種場景和行為。Interactor當中有個很重要的術(shù)語:use case棚辽,這個術(shù)語很多技術(shù)文章中都有遇見技竟,它代表的是一個完整的,獨立的屈藐,細分過后的業(yè)務流程榔组,比如我們APP當中的登陸模塊,它是一個業(yè)務單元联逻,但它其實可以進一步的細分為很多use case:
use case1:驗證郵箱長度
use case2:密碼長度檢驗
use case3:從Server查詢use name是否可用
...
use caseN
定義use case有什么好處呢?
好處當然是分門別類,結(jié)構(gòu)清晰搓扯。把100本書堆一堆,或者放書架上按類別擺放包归,下次找書的時候那種方式你更舒服锨推?獨立出一個個的use case還有一個好處是方便unit test,如果項目對每一個use case都寫有unit test公壤,每次遇到“牽一發(fā)動全身”的業(yè)務更改换可,可以邊喝茶邊寫代碼。
我見過不少代碼都體現(xiàn)不出use case 的分類厦幅,可以回頭看下自己當前項目的登陸模塊沾鳄,上面我提到的這些use case有沒有在類文件中合理擺放,還是都攪在一起慨削?
所以VIPER當中Interactor的說法是強化大家寫單獨的use case的意識洞渔,打開interactor.m,看一個函數(shù)代表一個use case缚态,同一類的use case再用#pragma mark歸在一塊磁椒,別人看你代碼時能不賞心悅目嗎?
再說到Presenter玫芦,Presenter時上面一個個use case的使用者和響應者浆熔。使用者將個個use case串聯(lián)起來描述一個完整詳細的業(yè)務流程,比如我們的登陸模塊桥帆,每次用戶點擊按鈕登陸的時候医增,會觸發(fā)一系列的use case,從驗證用戶輸入合法性老虫,設備網(wǎng)絡狀態(tài)叶骨,服務器資源是否可用,到最后處理結(jié)果并展示祈匙,這就是一個完整的業(yè)務流程忽刽,這個流程由Presenter來描述天揖。響應者表示Presenter在接受到服務器反饋之后進一步改變本地的狀態(tài),比如View的展示跪帝,新的數(shù)據(jù)修改等今膊,甚至會調(diào)用Routing發(fā)生界面跳轉(zhuǎn)。
說到這里就比較明了了伞剑,Interactor和Routing是服務的提供方斑唬,Presenter是服務的使用方和集成方。VIPER說白了不過是對傳統(tǒng)的MVC當中的C做了進一步細分黎泣。
能不能分的更細呢恕刘?
當然可以,VIPER的做法是一種通用的做法抒倚,我們還可以從業(yè)務的角度去細分雪营,那映客的直播頁面做例子,比如Presenter當中包含了很多業(yè)務流程:
收到用戶消息并展示
收到禮品消息并展示
收到彈幕消息并展示
收到用戶進出房間的時間衡便,并處理展示
收到XXX,處理并展示
以OC語言的特性洋访,我們可以生成更多的Presenter Category镣陕,來安置這些流程,比如LivePresenter+Message, LivePresenter+Gift, LivePresenter+Danmu, LivePresenter+Room, LivePresenter+XXX姻政。
不要覺的上面幾個業(yè)務流程很簡單呆抑,一個Presenter處理綽綽有余,我前段時間剛好看過別人的一個直播項目汁展,一個Persenter類超過1000行代碼很輕松鹊碍。
還可以進一步細分,一個功能復雜繁多的頁面基本上離不開UITableView食绿,而tableView代碼量基本在delegate和datasource侈咕。這兩個職責當然可以放在presenter當中,或者我們向Android學習器紧,把它們獨立出來放在單獨的類文件中來處理耀销,比如叫做Adapter 用代碼來說就是:
<pre>
<code>
_tableView.delegate = self.adapter;
_tableView.dataSource = self.adapter;
</code>
</pre>
和tableView相關(guān)的代碼都搬到adapter當中:
<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更加干凈了,看起來和剛大掃除過的房間一個干凈整潔,令人心情愉悅。
好了铲汪,到這里我們盤子里的牛排已經(jīng)被切成很多小塊了熊尉,可以開始享用這些美味的代碼了,繼續(xù)我們的第二部工作:鏈接掌腰。
鏈接
先看一下我們分解之后有哪些元素:
view(1…N), model(1…N), interactor, presenter(1…N), routing, adapter狰住。看著應該粒度夠細了齿梁,對于復雜的Controller催植,我個人習慣的做法和VIPER相近,但略有不同,Interactor當中的use case通過分層的架構(gòu)被我們放到server layer查邢,分層的架構(gòu)是另一個話題蔗崎,這里不做細述,其他元素基本一致扰藕。
至于怎樣鏈接缓苛,手段無非就是OC的幾種交互機制:
<strong>Delegate, Target-Action, Block, Notification, KVO</strong>
這幾者之間的差異可以參考objc.io的一篇經(jīng)典文章。選擇不同對耦合度邓深,開放便捷性未桥,調(diào)試是否方便等都會產(chǎn)生影響,如何應用不同的機制將各個單位串聯(lián)起來就看架構(gòu)師自己的積累和理解了芥备,任何一個選擇都有其優(yōu)勢和局限性冬耿。
如果拿捏不準選哪個好的時候,我個人建議使用delegate萌壳,樸素可靠且直觀亦镶。delegate需要在不同的元素之間傳遞,代碼量會偏多一些袱瓮,但優(yōu)點在protocol定義清晰缤骨,耦合在哪里一目了然,記得要注意循環(huán)引用的問題尺借。
我早些時候其他幾種機制都在實際項目中做過嘗試绊起,最后綜合比較還是傾向于選擇delegate,一位iOS大神MrPeak利用runtime機制燎斩,做個一個CDD機制來自動串聯(lián)各個功能單位虱歪。請看:CDD的詳細介紹,其本質(zhì)或者說最終目的還是在于鏈接。
說完了分解和鏈接栅表,Controller的重構(gòu)完成了一大半笋鄙,還剩下一個重要的概念:狀態(tài)分享。
盡量避免跨類,跨模塊跨層共享狀態(tài)
MrPeak博客里談到過對于程序狀態(tài)的維護.狀態(tài)是否維護的好對于程序的整體穩(wěn)定性很有影響谨读,對于Controller中狀態(tài)的維護局装,我有一個個簡單的建議:
傳遞狀態(tài)的時候盡可能copy
之前流行的函數(shù)式編程其實就很強調(diào)無狀態(tài)性,無狀態(tài)不是讓大家不定義狀態(tài)變量劳殖,而是避免函數(shù)之間的狀態(tài)共享铐尚,具體到OC當中,不要在不同的功能單位里使用指向同一塊內(nèi)存拷貝的地址哆姻,為什么共享狀態(tài)是一件危險的事宣增?
一般來說,我們從Model Layer 或者是數(shù)據(jù)層拿到model的實例矛缨,扔給Controller使用的時候應該是一份新的copy爹脾,在不同的類單位里共享NSMutableString或者NSMutableArray帖旨,NSMutableDictionary很容易讓你的代碼變得不穩(wěn)定,而且這類不穩(wěn)定性很難調(diào)試灵妨,debug填坑的時候經(jīng)常按下葫蘆漂起瓢解阅。
在Controller內(nèi)部傳遞model或者satate的時候股囊,我們應該也盡量使用copy行為簿透,任何satate你一旦暴露出去就不再安全乎莉,自己創(chuàng)建纺荧,自己修改,自己銷毀才是正途返吻。
FaceBook當中的model layer就是由一個單獨的開發(fā)團隊維護的挫剑,應用層(Controller層)開發(fā)人員獲取到的都是一個新的拷貝或悲,要修改某個屬性不一定有接口藤为,甚至要向model維護團隊提交增加接口的申請怪与,對于state維護的謹慎性可見一斑。
使用腳本生成原型代碼
說了這么多缅疟,Controller重構(gòu)的關(guān)鍵點就說完了分别。最后再提個小Tip,一旦Controller做深度細分之后存淫,團隊成員需要對Controller的分法和構(gòu)成有一定的認識茎杂,寫出來的代碼應該保持一致,我的做法是通過腳本的方式生成Controller各個相關(guān)的的類文件纫雁,比如我的Controller是如下結(jié)構(gòu):
通過腳本將文件名和文件內(nèi)容當中Template全部替換成目標Controller的名字,就省去了很多體力代碼的勞動倾哺,也達到了代碼風格一致的問題轧邪。