授權轉自:MrPeak技術分享(公眾號:MrPeakTech)
1.應用層架構定義
其實嚴格來說,MVC和其他類似概念還算不上一個完整的架構。一個頗具規模的app必然會涉及到分層的設計,還有模塊化,hybrid機制,熱補丁等等。MVC這種更像是個設計模式,解決的只是app分層設計當中的應用層(Applicaiton Layer)組織方式。對於一些簡單app來說,可能應用層一層就完成了整個app的架構,不用擔心業務膨脹對後期開發的壓力。這裡我介紹一種新的應用層架構方式,名之為CDD:Context Driven Design。
先明確下我們討論的范疇,什麼是一個app的應用層呢?現在不少app都會做一個分層的設計,一般為三層:應用層,service層,data access層。每一層再通過面向接口的方式產生依賴。
應用層是直接和用戶打交道的部分,也就是我們常常用到的UIViewController,負責數據的展示,用戶交互的處理,數據的采集等等。
service層位於應用層的下面,為應用層提供公共的服務接口,對應用層來說就像是一個server,不過api調用的延遲為0ms,service層裡放哪些代碼做法沒有統一的規范,一般來說會包含業務數據的處理,網絡接口的調用,公共系統服務api封裝(比如gps定位,相冊,權限控制)等等。
data access層顧名思義是負責處理我們app的基礎數據,api設計規范一般遵循CRUD。這一層位於service層的下方,提供數據庫交互所需的api。
這是基礎部分,不同的團隊具體做法又會有一些差異。比如有些把data access層又叫做model層,有些把網絡模塊放在service層,有些則放在data acess層,有些把部分的業務數據放到model裡面做成胖model,有些則堅持使用瘦model,把業務代碼放在獨立的地方統一管理,等等差異不一而足。除了分層還有一些公共模塊的設計,比如數據庫,網絡,安全,熱補丁,hybrid機制,性能監測模塊,日志模塊等等如何配合分層設計,這裡就不一一展開了。我們今天討論的重點在應用層。
首先聲明下,這個CDD其實是我很久之前看Android代碼腦洞出來的,剛好解決了我之前組織應用層代碼的一個痛點。做過Android的朋友應該都知道,在很多類裡都可以通過getContext方法獲取到一個context,再通過這個context可以獲取到其他系統資源。當時我第一次了解完這個context概念的時候,瞬間產生了一個這樣的腦洞:
我知道這靈光一閃的腦洞有點大,容我慢慢道來。前面提到應用層其實是在管理一堆UIViewController。拿微信做例子(我真的很喜歡拿微信舉個栗子),首頁4個tab,4個界面,4個controller,每個controller都有很多UI元素,點擊又可以進入二級的controller,各controller可以看成一個獨立的模塊,有些簡單,有些復雜。比如聊天界面這個controller就非常非常的復雜。先來看下聊天界面。
這個界面展示的UI元素非常之多,頂部導航欄,消息tableView,輸入框部分,功能入口部分,可點擊交互的部分也很多。如果我們把所有這些UI元素和交互的處理都放倒Controller裡面,我們將得到一個著名的MVC(Massive View Controller),我曾經就有幸維護過一個這樣controller,一個類文件一萬多行代碼,修起bug來十分的酸爽。很顯然,我們的目標是拆分代碼,所謂的架構不就是“以低耦合的方式分散業務復雜度”嘛。如果我們能把這些UI元素放倒不同的xxxView.m裡面,交互的處理也有單獨的類,目標達成。但新的問題是這些分散的各個類文件之間怎麼交互,怎麼耦合,怎麼合體。MVC,MVVM,MVP等等都是在解決這個問題。這裡我們團結各個類文件的方式是Context!建議再回看下上面的腦洞圖。
在近一步深入討論CDD之前,我們再重點強調下一個概念,data flow(還有其他別名,info flow,數據流等)。data flow是架構優劣的測量標准,好的架構一定有清晰的data flow,你說你架構怎麼好,但data flow說不清楚,No,No,我們不約。什麼是data flow,就是數據在你的app裡流動的路線,就像人體血管裡的血液,滋養著各個器官的運作。上面的聊天界面裡,用戶在輸入框輸入一個“hello”文本,文本被你包裝成message model,再保存到db,再發送到服務器,最後在界面上展示給用戶,這就是一個完整的data flow。實際的data經歷的模塊會更多,大部分的bug都是data除了問題,修bug時就是在順著這個flow順籐摸瓜,把脈診斷。
再問個問題,什麼是data?你可以說data是model,是上面的“hello”文本。但我們還可以站在更高的角度來看待data,data是程序世界的基本元素,另一個基本元素是verb(動作),程序的世界裡的所有存在都可以由這兩個元素來描述,此處應該雙手合十,進入冥想三分鐘。推薦一篇大神吐槽java的文章。
2.CDD架構詳解
接下來進入正題,剖析CDD。我們先把應用層分解成三塊任務:
UI的展示,UI的展示通過分解UIView可以實現復雜度的分散,UI的變化則可以參考MVVM的方式,通過觀察者模式(KVO)來實現。
業務的處理,業務處理為了方便管理不能分散到不同的類,反而需要放到統一的地方管理,業務代碼太分散會給調試帶來很大的麻煩。
data flow,所有數據的變化可以在統一的地方被追蹤。數據的流向單一清晰。
在這三塊劃分的前提下我們再來制定CDD要達成的目標:
view的展示可以被分解成若干個子view.m文件,各自管理。
業務處理代碼放到統一的BusinessObject.m文件來管理。
model的變化有統一的類DataHandler.m文件來管理。
UIViewController裡面只存放Controller生命周期相關的代碼,做成輕量級的Controller。
所有子view可以處理只和自己相關的邏輯,如果屬於整體的業務邏輯,則需要通過context傳輸到BusinessObject來處理。
子view展示所需的數據可以通過context獲取到,並通過KVO的方式直接進行綁定。
根據這些目標,我把腦洞圖完善下就得到了下面一個更清晰的方案圖:
到這裡context的作用就很明顯了,context可以把所有的子view都連接起來,可以把業務邏輯都導向同一個地方,可以把數據的管理都集中在一個類。所有的類都可以訪問到context,但各部分只通過接口產生依賴,將耦合降至最低。至此CDD的大致結構就完成了,但還有一個問題需要解決。view的更新需要跟數據直接綁定,需要做成數據驅動的模式,類似MVVM。
但是我們怎麼定義數據的變化呢?
做數據驅動的設計就一定要有一套規范去定義數據的變化,在應用層數據的變化我們可以主要分為兩類。一是model本身property的變化,這種變化可以用KVO來監聽,很方便。另一種是集合類的變化,比如array,set,dictionary等,這類變化又包括元素的增刪替換,Objective-C沒有提供原生的支持來監聽這類變化,所以我們需要自己定一個子類,再通過重載增刪替換方法來實現,在Demo中我就定義了一個CDDMutableArray。定義數據的變化十分關鍵,直接關系到我們怎麼去設計data flow。data access層也需要定義一套規范,這裡就不展開了。
CDD的data flow是怎樣的呢?
前面提到了data flow是架構是否清晰的評判標准,是我們debug時的主要依賴。基於上面的討論CDD的data flow是這樣的:
我之前提到說CDD解決了我之前的一個痛點,其實就是分散復雜度時,需要大量的delegate傳遞來連接各個類,很多地方都需要引用protocol,比如輸入框view產生的“hello”文本要通過delegate傳遞給superview,superview可能還有superview,再到controller,controller再傳遞給業務處理的類,最後可能還要通過delegate做回傳。但我們看下CDD的整個flow,Controller就像是一個旁觀者,根本不需要參與到任何數據的傳遞,僅僅作為各個對象的持有者,只處理controller本身相關的業務,比如view appear,disappear,rotate等,controller也是context的持有者,也可以在viewWillAppear的時候把事件傳遞到BusinessObject進行處理。
輸入框view產生的“hello”文本,直接通過context傳遞到BusinessObject進行處理,生成的新消息message通過DataHandler插入到message array之後,直接通知到message tableview進行刷新。方法調用的路徑變短了,意味著調試的時候step over的次數減少了。
這裡有一點需要討論下,view和context之間耦合的方式。view產生的數據要交給BusinessObject進行處理,那麼這二者之間必然要產生耦合。耦合的方式有很多種:
只通過model進行耦合,二者只需要引用相同的model就可以進行交互,MVVM裡view通過KVO的方式監聽model變化就是這種耦合,這種耦合最弱,但調試會麻煩一些。
通過model+protocol進行耦合。耦合的雙方需要引用相同的model和protocol文件。這種方式屬於面向接口編程的范疇,耦合也比較弱,但比上面的方式強。優點是調試方便,delegate的調試可以單步step into。
通過model+class進行耦合。這是我們最不願意看到的耦合方式,類與類之間直接相互引用,任何一方變化都有可能引起潛在的bug。
view與context之間耦合的方式采用的是第二種,方便調試且耦合清晰。view會引用business object和data handler實現的相關協議,進而調用。
3.CDD架構Demo實戰
No Code,No BB。接下來我們用這套CDD的方案來實現一個類似微信的聊天界面。附上github DEMO地址,與朋友們一起學習研究。
我們會通過demo實現這樣一個Controller:
這個demo主要實現兩個功能來幫助大家了解CDD的workflow。一個是上面提到過的發送消息流程,二是點擊頭像之後可以進入用戶詳情的Controller,詳情Controller裡面可以改變用戶的名字,改變之後聊天界面的MrPeak的名字也會以數據驅動的方式自動更新。
Demo實現細節:
CDD實現並不復雜,關鍵類如下:
CDDContext就是之前提到的核心類context,還包含CDDDataHandler, CDDBusinessObject基類的定義。
NSObject+CDD給每個NSObject對象添加一個CDDContext指針。
UIView+CDD則通過swizzling的方式給每個UIView自動添加CDD。
CDDMutableArray實現對Array的觀察者模型。
針對某個Controller實現規范如下:
CDDContext,CDDDataHandler,CDDBusinessObject均在Controller當中生成。protocol定義接口部分的耦合。ViewModel是應用層當中的model,和View的展示通過KVO直接綁定。View部分則是我們拆分過後的子view。
4.CDD架構後續工作
CDD還處於初級階段,是很久之前腦洞的產物,最近空一點才找機會把他變成代碼。後面我會嘗試在成熟的項目裡去進一步完善並應證其合理性,也歡迎朋友們一起研究討論。
後期可能進行的工作有:
完善對更多集合類的支持,比如Dictionary, Set等。
BusinessObject在業務龐大的時候還是有可能膨脹,變得難以維護,可以嘗試做進一步分解。
現在Context的賦值是通過didAddSubview去hack實現的,應該還有更多的場景需要去完善。
現在每個UIView包括系統(比如導航欄)控件都會去賦值Context,可能需要一種機制只對定制的UIView進行賦值。
給Demo添加更多的功能場景。
待補充……
總結
從2010年開始接觸iOS開發到現在,折騰過不少app的架構。從MVC到MVVM,VIPER,MVP,以及最新的ReactiveCocoa都做過實戰嘗試,還有其他變種,諸如猿題庫iOS客戶端架構設計,也做過一些學習研究。這些技術概念如果不熟悉,建議每個鏈接都點開好好研讀下,不要對你的大腦太溫柔。
最後推薦一些其他非常值得一讀的文章:
唐巧-被誤解的 MVC 和被神化的 MVVM
Casa Taloyum iOS架構系列文章
objc.io架構系列文章