UIViewController是iOS應(yīng)用的基礎(chǔ)業(yè)務(wù)單位伙菜,每個iOS程序員都寫過無數(shù)的Controller氯葬。今天和大家一起來深度解剖Controller涣旨,看看怎么來做一次深度的重構(gòu)。
重構(gòu)的前提
我們應(yīng)該謹(jǐn)慎的去重構(gòu)我們的代碼浑测。iOS系統(tǒng)提供的UIViewController一定程度上可以很好的應(yīng)付簡單的頁面單位杠输,對于復(fù)雜的頁面,我們也可以采用市面上主流的MV(X)系列模式秕衙,比如MVP蠢甲,MVVM等。但隨著單個Controller內(nèi)業(yè)務(wù)進(jìn)一步增長据忘,我們需要更細(xì)粒度的重構(gòu)鹦牛,或者說對MV(X)做進(jìn)一步的定制。
以下圖映客App兩個頁面為例勇吊。
左邊頁面元素少且靜態(tài)曼追,一個TableView基本上就能應(yīng)付,右邊的直播頁面則元素多且動態(tài)汉规,傳統(tǒng)的MV(X)也會顯得粒度太粗礼殊,這類復(fù)雜頁面雖然不常遇到,但往往體現(xiàn)一個App的核心功能针史,合理的搭建或者重構(gòu)這類Controller十分重要晶伦。
重構(gòu)的本質(zhì)
如何去定義重構(gòu),以我的理解可以歸納為兩個關(guān)鍵詞:分解啄枕,連接婚陪。
重構(gòu)的前提是復(fù)雜,臃腫频祝,不直觀泌参,重構(gòu)的手段是分解之后再連接脆淹。以映客的直播界面為例,UI元素沽一,用戶事件盖溺,服務(wù)器交互等基礎(chǔ)元素都非常之多,以一個簡單的MVP去歸類代碼猶嫌不足锯玛,我們還需要進(jìn)一步的分解成view1,view2...viewN,presenter1,presenter2...presenterN,model1,model2...modelN咐柜,第二個問題是如何把這一個個的類文件或者說功能單位合理組織連接起來。完成上述兩步我們就完成了一次重構(gòu)攘残,每一次將代碼打散再串聯(lián)就是一次重構(gòu)拙友。
分解UIViewController
寫了那么多Controller,讓你來說下一個Controller都細(xì)分為哪些更小的功能單位歼郭,你能隨口說出來么遗契?只有做過足夠多的業(yè)務(wù),才能慢慢對Controller的構(gòu)成有自己的理解病曾。
當(dāng)然可以回答說MVC或者M(jìn)VP牍蜂,但這個答案粒度太粗,一個Controller內(nèi)部會發(fā)生哪些事可以說的更細(xì)泰涂,我們看下VIPER的答案:
- View: displays what it is told to by the Presenter and relays user input back to the Presenter.
- Interactor: contains the business logic as specified by a use case.
- Presenter: 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).
- Entity: contains basic model objects used by the Interactor.
- Routing: contains navigation logic for describing which screens are shown in which order
View不用多說鲫竞,可以分解成更多的子View,最后合成一個樹形結(jié)構(gòu)逼蒙。
Entity自然是代表Model从绘。
MVC當(dāng)中的C,MVP當(dāng)中的P是牢,被細(xì)分成了Interactor僵井,Presenter,和Routing驳棱。這三個角色各自負(fù)責(zé)什么職責(zé)呢批什?
Routing比較清楚,處理頁面之間的跳轉(zhuǎn)社搅。我見過的項目代碼里驻债,很少有把這一部分單獨拎出來的,但其實很有意義形葬,這部分代表的是不同Controller之間耦合依賴的方式却汉,無論是從類關(guān)系描述的角度還是Debug的角度,都能幫助我們快速定位代碼荷并。
Interactor和Presenter初看起來很類似合砂,似乎都是在處理業(yè)務(wù)邏輯。但業(yè)務(wù)邏輯其實是個大的歸類,可以描述任何一種業(yè)務(wù)場景和行為翩伪。Interactor當(dāng)中有個很重要的術(shù)語:use case微猖,這個術(shù)語很多技術(shù)文章中都會遇見,它代表的是一個完整的缘屹,獨立的凛剥,細(xì)分過后的業(yè)務(wù)流程,比如我們App當(dāng)中的登錄模塊轻姿,它是一個業(yè)務(wù)單位犁珠,但它其實可以進(jìn)一步的細(xì)分為很多的use case:
use case 1: 驗證郵箱長度
use case 2: 密碼強度檢驗
use case 3: 從Server查詢user name是否可用
...
user case N
定義use case有什么好處呢?
好處當(dāng)然是分門別類互亮,結(jié)構(gòu)清晰犁享。你把100本書堆一堆,或者放書架上按類別擺放豹休,下次找書的時候那種方式你更舒服炊昆?獨立出一個個的use case還有一個好處是方便unit test,如果項目對每一個use case都有寫對應(yīng)的unit test威根,每次遇到“前一發(fā)動全身“的業(yè)務(wù)更改凤巨,可以邊杯茶邊寫代碼。
我見過不少代碼都體現(xiàn)不出use case的分類洛搀,可以回頭看下自己當(dāng)前項目的登錄模塊敢茁,上面我提到的這些case有沒有在類文件當(dāng)中合理擺放,還是都攪在一起留美?
所以VIPER當(dāng)中interactor的說法是強化大家寫單獨的use case的意識卷要,打開interactor.m,看到一個函數(shù)代表一個use case独榴,同一類的use case再用#pragma mark 歸在一塊,別人看你代碼時能不賞心悅目嗎奕枝?
再說到Presenter棺榔,Presenter可以看做是上面一個個use case的使用者和響應(yīng)者。使用者將各個use case串聯(lián)起來描述一個完整詳細(xì)的業(yè)務(wù)流程隘道,比如我們的登錄模塊症歇,每次用戶點擊按鈕注冊的時候,會觸發(fā)一系列的use case谭梗,從檢驗用戶輸入合法性忘晤,設(shè)備網(wǎng)絡(luò)狀態(tài),服務(wù)器資源是否可用激捏,到最后處理結(jié)果并展示设塔,這就是一個完整的業(yè)務(wù)流程,這個流程由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說白了不過是對傳統(tǒng)的MVC當(dāng)中的C做了進(jìn)一步細(xì)分。
能不能分的更細(xì)呢荸型?
當(dāng)然可以繁涂,VIPER的分法是一種通用的做法拱她,我們還可以從業(yè)務(wù)的角度去做細(xì)分。拿映客的直播界面做例子爆土,比如Presenter當(dāng)中包含了很多完整的業(yè)務(wù)流程:
- 收到用戶消息并展示
- 收到禮品消息并展示
- 收到彈幕消息并展示
- 收到用戶進(jìn)出房間的事件椭懊,處理并展示
- 收到XXX,處理并展示
以O(shè)bjective C語言的特性步势,我們可以生成更多的Presenter Category來安置這些流程氧猬,比如LivePresenter+Message, LivePresenter+Gift, LivePresenter+Danmu, LivePresenter+Room, LivePresenter+XXX。
不要覺得上面幾個業(yè)務(wù)流程很簡單坏瘩,一個presenter處理綽綽有余盅抚,我前段時間剛好做過一個直播項目,Presenter類超過1000行代碼很輕松倔矾。
還可以進(jìn)一步細(xì)分妄均,一個功能復(fù)雜繁多的頁面基本上離不開UITableView,而tableview的代碼量主要在于delegate和datasource哪自。這兩個職責(zé)當(dāng)然可以放在presenter當(dāng)中丰包,或者我們向Android學(xué)習(xí),把它們也獨立出來放到單獨的類文件中去處理壤巷,比如叫做Adapter邑彪,用代碼來說就是:
_tableView.delegate = self.adapter;
_tableView.dataSource = self.adapter;
和tableView相關(guān)的這些代碼都搬到了adapter當(dāng)中:
@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
我們的Presenter就變得更加干凈了,看起來和剛大掃除過的房間一樣令人愉悅胧华。
好了寄症,到這里我們盤子里的牛排已經(jīng)被切成很多小塊了,可以開始享用這些美味的代碼了矩动,繼續(xù)我們的第二步工作:連接有巧。
連接
先看下我們分解之后有哪些元素:
view(1…N), model(1…N), interactor, presenter(1…N), routing, adapter”唬看著應(yīng)該粒度夠細(xì)了篮迎,對于復(fù)雜的Controller,我個人習(xí)慣的做法和VIPER相近,但略有不同柑潦,Interactor當(dāng)中的use case通過分層的架構(gòu)被我放到server layer享言,分層的架構(gòu)是另一個話題,這里不做細(xì)述渗鬼。其他元素基本一致览露。
至于怎么連接,手段無非就是OC的幾種類交互機制:
Delegate, Target-Action, Block, Notification, KVO譬胎。
這幾者之間的差異可以參考objc.io的一篇經(jīng)典文章差牛。選擇不同對耦合度,開發(fā)便捷性堰乔,調(diào)試是否方便等都會產(chǎn)生影響偏化,如何應(yīng)用不同的機制將各個單位串聯(lián)起來就看架構(gòu)師自己的積累和理解了,任何一個選擇都有其優(yōu)勢和局限性镐侯。
如果拿捏不準(zhǔn)選哪個好的時候侦讨,我個人建議就使用delegate,樸素可靠且直觀苟翻。delegate需要在不同的元素之間傳遞韵卤,代碼量會偏多一些,但優(yōu)點在protocol定義清晰崇猫,耦合在哪里一目了然沈条,記得要注意循環(huán)引用的問題。
我早些時候其他幾種機制都在實際項目中做過嘗試诅炉,最后綜合比較還是傾向于選擇delegate蜡歹,再后來經(jīng)過一番腦洞(主要是為了解決傳遞delegate所帶來的額外代碼量),利用runtime特性涕烧,做了一個CDD機制來自動串聯(lián)各個功能單位月而。CDD的詳細(xì)介紹在之前的博客中有,這里也不細(xì)述了议纯,其本質(zhì)或者說最終目的還是在于連接父款。
說完了分解和連接,Controller的重構(gòu)完成了大半痹扇,還剩下一個至關(guān)重要的概念:狀態(tài)分享。
盡量避免跨類溯香,跨模塊或跨層共享狀態(tài)
我之前在一篇博客中談到過對于程序狀態(tài)的維護鲫构。狀態(tài)是否維護得好對于程序的整體穩(wěn)定性很有影響,對于Controller當(dāng)中的狀態(tài)維護我有一個簡單的建議:
傳遞狀態(tài)的時候盡可能Copy
之前流行的函數(shù)式編程其實就很強調(diào)無狀態(tài)性玫坛,無狀態(tài)不是讓大家不定義狀態(tài)變量结笨,而是避免函數(shù)之間的狀態(tài)共享,具體到OC當(dāng)中,就是不要在不同的功能單位里使用指向同一塊內(nèi)存拷貝的地址炕吸,為什么共享狀態(tài)是一件危險的事伐憾,我在之前的文章中也介紹過。
一般來說赫模,我們從Model Layer或者說數(shù)據(jù)層拿到的model實例树肃,扔給Controller使用的時候應(yīng)該是一份新的拷貝,在不同的類單位里共享NSMutableString或者NSMutableArray瀑罗,NSMutableDictionary很容易讓你的代碼變得不穩(wěn)定胸嘴,而且這類不穩(wěn)定性一般很難調(diào)試,debug填坑的時候經(jīng)常按下葫蘆浮起瓢斩祭。
在controller內(nèi)部傳遞model或者state的時候劣像,我們應(yīng)該也盡量使用copy行為,任何state你一旦暴露出去就不再安全摧玫,自己創(chuàng)建耳奕,自己修改,自己銷毀才是正途诬像。說到
我之前介紹Facebook架構(gòu)的時候就提到過屋群,F(xiàn)acebook當(dāng)中的model layer是由一個單獨開發(fā)團隊維護的,應(yīng)用層開發(fā)人員(Controller開發(fā)人員)獲取到的都是新的拷貝颅停,要修改某個屬性不一定有接口谓晌,甚至要向model的維護團隊提交增加接口的申請,對于state維護的謹(jǐn)慎度可見一斑癞揉。
使用腳本生成原型代碼
說了這么多纸肉,Controller重構(gòu)的關(guān)鍵點都說完了。最后再提個小Tip喊熟,一旦Controller做深度細(xì)分之后柏肪,團隊成員需要對Controller的分法和構(gòu)成有一致的認(rèn)識,寫出來的代碼應(yīng)該保持一致芥牌,我的做法是通過腳本的方式生成Controller各個相關(guān)的類文件烦味,比如我的Controller是如下結(jié)構(gòu):
通過腳本將文件名和文件內(nèi)容當(dāng)中的Template全部替換成目標(biāo)Controller的名字,就省去了很多重復(fù)代碼的體力勞動壁拉,也達(dá)到了代碼風(fēng)格一致的目的谬俄。
歡迎關(guān)注公眾號: