深度重構 UIViewController

UIViewController是iOS應用的基礎業務單位,每個iOS程序員都寫過無數的Controller。今天和大家一起來深度解剖Controller,看看怎么來做一次深度的重構。

重構的前提

我們應該謹慎的去重構我們的代碼。iOS系統提供的UIViewController一定程度上可以很好的應付簡單的頁面單位,對于復雜的頁面,我們也可以采用市面上主流的MV(X)系列模式,比如MVP,MVVM等。但隨著單個Controller內業務進一步增長,我們需要更細粒度的重構,或者說對MV(X)做進一步的定制。

以下圖映客App兩個頁面為例。

左邊頁面元素少且靜態,一個TableView基本上就能應付,右邊的直播頁面則元素多且動態,傳統的MV(X)也會顯得粒度太粗,這類復雜頁面雖然不常遇到,但往往體現一個App的核心功能,合理的搭建或者重構這類Controller十分重要。

重構的本質

如何去定義重構,以我的理解可以歸納為兩個關鍵詞:分解,連接。

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

分解UIViewController

寫了那么多Controller,讓你來說下一個Controller都細分為哪些更小的功能單位,你能隨口說出來么?只有做過足夠多的業務,才能慢慢對Controller的構成有自己的理解。

當然可以回答說MVC或者MVP,但這個答案粒度太粗,一個Controller內部會發生哪些事可以說的更細,我們看下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,最后合成一個樹形結構。

Entity自然是代表Model。

MVC當中的C,MVP當中的P,被細分成了Interactor,Presenter,和Routing。這三個角色各自負責什么職責呢?

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

Interactor和Presenter初看起來很類似,似乎都是在處理業務邏輯。但業務邏輯其實是個大的歸類,可以描述任何一種業務場景和行為。Interactor當中有個很重要的術語:use case,這個術語很多技術文章中都會遇見,它代表的是一個完整的,獨立的,細分過后的業務流程,比如我們App當中的登錄模塊,它是一個業務單位,但它其實可以進一步的細分為很多的use case:

use case 1: 驗證郵箱長度

use case 2: 密碼強度檢驗

use case 3: 從Server查詢user name是否可用

user case N

定義use case有什么好處呢?

好處當然是分門別類,結構清晰。你把100本書堆一堆,或者放書架上按類別擺放,下次找書的時候那種方式你更舒服?獨立出一個個的use case還有一個好處是方便unit test,如果項目對每一個use case都有寫對應的unit test,每次遇到“前一發動全身“的業務更改,可以邊杯茶邊寫代碼。

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

所以VIPER當中interactor的說法是強化大家寫單獨的use case的意識,打開interactor.m,看到一個函數代表一個use case,同一類的use case再用#pragma mark 歸在一塊,別人看你代碼時能不賞心悅目嗎?

再說到Presenter,Presenter可以看做是上面一個個use case的使用者和響應者。使用者將各個use case串聯起來描述一個完整詳細的業務流程,比如我們的登錄模塊,每次用戶點擊按鈕注冊的時候,會觸發一系列的use case,從檢驗用戶輸入合法性,設備網絡狀態,服務器資源是否可用,到最后處理結果并展示,這就是一個完整的業務流程,這個流程由Presenter來描述。響應者表示Presenter在接收到服務器反饋之后進一步改變本地的狀態,比如view的展示,新的數據修改等,甚至會調用Routing發生頁面跳轉。

說到這里就比較明了了,interactor和routing都是服務的提供方,presenter是服務的使用和集成方。VIPER說白了不過是對傳統的MVC當中的C做了進一步細分。

能不能分的更細呢?

當然可以,VIPER的分法是一種通用的做法,我們還可以從業務的角度去做細分。拿映客的直播界面做例子,比如Presenter當中包含了很多完整的業務流程:

  • 收到用戶消息并展示
  • 收到禮品消息并展示
  • 收到彈幕消息并展示
  • 收到用戶進出房間的事件,處理并展示
  • 收到XXX,處理并展示

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

不要覺得上面幾個業務流程很簡單,一個presenter處理綽綽有余,我前段時間剛好做過一個直播項目,Presenter類超過1000行代碼很輕松。

還可以進一步細分,一個功能復雜繁多的頁面基本上離不開UITableView,而tableview的代碼量主要在于delegate和datasource。這兩個職責當然可以放在presenter當中,或者我們向Android學習,把它們也獨立出來放到單獨的類文件中去處理,比如叫做Adapter,用代碼來說就是:

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

和tableView相關的這些代碼都搬到了adapter當中:

@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就變得更加干凈了,看起來和剛大掃除過的房間一樣令人愉悅。

好了,到這里我們盤子里的牛排已經被切成很多小塊了,可以開始享用這些美味的代碼了,繼續我們的第二步工作:連接。

連接

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

view(1…N), model(1…N), interactor, presenter(1…N), routing, adapter。看著應該粒度夠細了,對于復雜的Controller,我個人習慣的做法和VIPER相近,但略有不同,Interactor當中的use case通過分層的架構被我放到server layer,分層的架構是另一個話題,這里不做細述。其他元素基本一致。

至于怎么連接,手段無非就是OC的幾種類交互機制:

Delegate, Target-Action, Block, Notification, KVO。

這幾者之間的差異可以參考objc.io的一篇經典文章。選擇不同對耦合度,開發便捷性,調試是否方便等都會產生影響,如何應用不同的機制將各個單位串聯起來就看架構師自己的積累和理解了,任何一個選擇都有其優勢和局限性。

如果拿捏不準選哪個好的時候,我個人建議就使用delegate,樸素可靠且直觀。delegate需要在不同的元素之間傳遞,代碼量會偏多一些,但優點在protocol定義清晰,耦合在哪里一目了然,記得要注意循環引用的問題。

我早些時候其他幾種機制都在實際項目中做過嘗試,最后綜合比較還是傾向于選擇delegate,再后來經過一番腦洞(主要是為了解決傳遞delegate所帶來的額外代碼量),利用runtime特性,做了一個CDD機制來自動串聯各個功能單位。CDD的詳細介紹在之前的博客中有,這里也不細述了,其本質或者說最終目的還是在于連接。

說完了分解和連接,Controller的重構完成了大半,還剩下一個至關重要的概念:狀態分享。

盡量避免跨類,跨模塊或跨層共享狀態

我之前在一篇博客中談到過對于程序狀態的維護。狀態是否維護得好對于程序的整體穩定性很有影響,對于Controller當中的狀態維護我有一個簡單的建議:

傳遞狀態的時候盡可能Copy

之前流行的函數式編程其實就很強調無狀態性,無狀態不是讓大家不定義狀態變量,而是避免函數之間的狀態共享,具體到OC當中,就是不要在不同的功能單位里使用指向同一塊內存拷貝的地址,為什么共享狀態是一件危險的事,我在之前的文章中也介紹過。

一般來說,我們從Model Layer或者說數據層拿到的model實例,扔給Controller使用的時候應該是一份新的拷貝,在不同的類單位里共享NSMutableString或者NSMutableArray,NSMutableDictionary很容易讓你的代碼變得不穩定,而且這類不穩定性一般很難調試,debug填坑的時候經常按下葫蘆浮起瓢。

在controller內部傳遞model或者state的時候,我們應該也盡量使用copy行為,任何state你一旦暴露出去就不再安全,自己創建,自己修改,自己銷毀才是正途。說到

我之前介紹非死book架構的時候就提到過,非死book當中的model layer是由一個單獨開發團隊維護的,應用層開發人員(Controller開發人員)獲取到的都是新的拷貝,要修改某個屬性不一定有接口,甚至要向model的維護團隊提交增加接口的申請,對于state維護的謹慎度可見一斑。

使用腳本生成原型代碼

說了這么多,Controller重構的關鍵點都說完了。最后再提個小Tip,一旦Controller做深度細分之后,團隊成員需要對Controller的分法和構成有一致的認識,寫出來的代碼應該保持一致,我的做法是通過腳本的方式生成Controller各個相關的類文件,比如我的Controller是如下結構:

 

 

來自:http://mp.weixin.qq.com/s?__biz=MzI5MjEzNzA1MA==&mid=2650264176&idx=1&sn=6cf5d0f58dfacf93e84c2dc0f9ef4afc&chksm=f4068345c3710a537a05f2897f30179258d4eab36b7e29c33278ddd1b4277128090036599748

 

 本文由用戶 ti053473 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!