前言:
之前看了Casa大神的架構設計文章,醍醐灌頂,一直想開個系列文章記錄一下(這次就做個小小搬運工,別打臉),公司項目實在太忙,最近稍好些,就開始著手做這件事,這個系列共以下幾篇:
1. 搭建優質的App框架
2. view層的組織和調用方案(常用架構模式-MVC、MVCS、MVVM)
3. 網絡層設計方案
4. 數據持久化設計方案及動態部署
5. 組件化方案
文章主要內容:
- View代碼結構的規定
- 關于view的布局
- 何時使用storyboard,何時使用nib,何時使用代碼寫View
- 是否有必要讓業務方統一派生ViewController?
- 方便View布局的小工具
- MVC、MVVM、MVCS (后期專門開篇分析)
- 本門心法
- 總結
View代碼結構的規定
每個團隊或公司,都會有自己的編碼規范,當然蘋果也有一套蘋果有一套Coding Guidelines,制定代碼規范對于開發、尤其是團隊開發、迭代至關重要??梢蕴岣叽a的可讀性可維護性,防止業務代碼對架構產生腐蝕。
規范中確保以下幾點:
所有的屬性都使用getter和setter
不要在viewDidLoad里面初始化你的view然后再add,這樣代碼就很難看。在viewDidload里面只做addSubview的事情,然后在viewWillAppear里面做布局的事情(勘誤1),最后在viewDidAppear里面做Notification的監聽之類的事情。至于屬性的初始化,則交給getter去做。
PS:getter和setter全部都放在最后,寫代碼的時候按照順序來分配代碼塊的位置,先是life cycle,然后是Public、private方法,然后是Delegate方法實現,然后是event response,然后才是getters and setters。
每一個delegate都把對應的protocol名字帶上,delegate方法不要到處亂寫,寫到一塊區域里面去
View的布局
布局時用Frame也好用Autolayout也好,如果沒有精心設計過,布局部分一定慘不忍睹。
直接使用CGRectMake的話可讀性很差,光看那幾個數字,也無法知道view和view之間的位置關系。用Autolayout可讀性稍微好點兒,但生成Constraint的長度實在太長,代碼觀感不太好。
Autolayout這邊可以考慮使用Masonry,代碼的可讀性就能好很多。如果還有使用Frame的,可以考慮一下使用這個項目。這個項目里面提供了Frame相關的方便方法(UIView+LayoutMethods),里面的方法也基本涵蓋了所有布局的需求,可讀性非常好,使用它之后基本可以和CGRectMake說再見了。
對于 UI 界面的編寫工作,到底應該用 xib/storyboard 完成,還是用手寫代碼來完成?
- 對于復雜的、動態生成的界面,建議使用手工編寫界面。
- 對于需要統一風格的按鈕或UI控件,建議使用手工用代碼來構造。方便之后的修改和復用。
- 對于需要有繼承或組合關系的 UIView 類或 UIViewController 類,建議用代碼手工編寫界面。
- 對于那些簡單的、靜態的、非核心功能界面,可以考慮使用 xib 或 storyboard 來完成。
是否有必要讓業務方統一派生ViewController
有的時候我們出于記錄用戶操作行為數據的需要,或者統一配置頁面的目的,會從UIViewController里面派生一個自己的ViewController,來執行一些通用邏輯。比如天貓客戶端要求所有的ViewController都要繼承自TMViewController(其實我們同花順也是這樣的所有ViewController都要繼承自HXViewController)。這個統一的父類里面針對一個ViewController的所有生命周期都做了一些設置,至于這里都有哪些設置對于本篇文章來說并不重要。在這里我想討論的是,在設計View架構時,如果為了能夠達到統一設置或執行統一邏輯的目的,使用派生的手段是有必要的嗎?
我覺得沒有必要,為什么沒有必要?
- 使用派生比不使用派生更容易增加業務方的使用成本
- 不使用派生手段一樣也能達到統一設置的目的
為什么使用了派生,業務方的使用成本會提升?
其實不光是業務方的使用成本,架構的維護成本也會上升。那么具體的成本都來自于哪里呢?
-
集成成本
這里講的集成成本是這樣的:如果業務方自己開了一個獨立demo,快速完成了某個獨立流程,現在他想把這個現有流程集合進去。那么問題就來了,他需要把所有獨立的UIViewController改變成TMViewController。那為什么不是一開始就立刻使用TMViewController呢?因為要想引入TMViewController,就要引入整個天貓App所有的業務線,所有的基礎庫,因為這個父類里面涉及很多天貓環境才有的內容,所謂拔出蘿卜帶出泥,你要是想簡單繼承一下就能搞定的事情,搭環境就要搞半天,然后這個小Demo才能跑得起來。
對于業務層存在的所有父類來說,它們是很容易跟項目中的其他代碼糾纏不清的,這使得業務方開發時遇到一個兩難問題:要么把所有依賴全部搞定,然后基于App環境(比如天貓)下開發Demo,要么就是自己Demo寫好之后,按照環境要求改代碼。這里的兩難問題都會帶來成本,都會影響業務方的迭代進度。 -
上手接受成本
新來的業務工程師有的時候不見得都記得每一個ViewController都必須要派生自TMViewController而不是直接的UIViewController。新來的工程師他不能直接按照蘋果原生的做法去做事情,他需要額外學習,比如說:所有的ViewController都必須繼承自TMViewController。 -
架構的維護難度
盡可能少地使用繼承能提高項目的可維護性,具體內容見Casa《跳出面向對象思想(一) 繼承》
那么如果不使用派生,我們應該使用什么手段?
我的建議是使用AOP。
在架構師實現具體的方案之前,必須要想清楚幾個問題,然后才能決定采用哪種方案。是哪幾個問題?
- 方案的效果,和最終要達到的目的是什么?
- 在自己的知識體系里面,是否具備實現這個方案的能力?
- 在業界已有的開源組件里面,是否有可以直接拿來用的輪子?
這三個問題按照順序一一解答之后,具體方案就能出來了。
我們先看第一個問題:方案的效果,和最終要達到的目的是什么?
其實就是要實現不通過業務代碼上對框架的主動迎合,使得業務能夠被框架感知
這樣的功能。細化下來就是兩個問題,框架要能夠攔截到ViewController的生命周期,另一個問題就是,攔截的定義時機。
對于方法攔截,很容易想到Method Swizzling,那么我們可以寫一個實例,在App啟動的時候添加針對UIViewController的方法攔截,這是一種做法。還有另一種做法就是,使用NSObject的load函數,在應用啟動時自動監聽。使用后者的好處在于,這個模塊只要被項目包含,就能夠發揮作用,不需要在項目里面添加任何代碼。
然后另外一個要考慮的事情就是,原有的TMViewController(所謂的父類)也是會提供額外方法方便子類使用的,Method Swizzling只支持針對現有方法的操作,拓展方法的話,嗯,當然是用Category啦。
我本人不贊成Category的過度使用,但鑒于Category是最典型的化繼承為組合的手段,在這個場景下還是適合使用的。還有的就是,關于Method Swizzling
手段實現方法攔截,業界也已經有了現成的開源庫:Aspects,我們可以直接拿來使用。
那么什么是AOP?
AOP(Aspect Oriented Programming),面向切片編程,但它跟我們熟知的面向對象編程沒什么關系。
那切片又是什么?
程序要完成一件事情,一定會有一些步驟,1,2,3,4這樣。這里分解出來的每一個步驟我們可以認為是一個切片。
為什么會出現面向切片編程?
你要想做到在每一個步驟中間做你自己的事情,不用AOP也一樣可以達到目的,直接往步驟之間塞代碼就好了。但是事實情況往往很復雜,直接把代碼塞進去,主要問題就在于:塞進去的代碼很有可能是跟原業務無關的代碼,在同一份代碼文件里面摻雜多種業務,這會帶來業務間耦合。為了降低這種耦合度,我們引入了AOP。
如何實現AOP?
AOP一般都是需要有一個攔截器,然后在每一個切片運行之前和運行之后(或者任何你希望的地方),通過調用攔截器的方法來把這個jointpoint扔到外面,在外面獲得這個jointpoint的時候,執行相應的代碼。
在iOS開發領域,objective-C的runtime有提供了一系列的方法,能夠讓我們攔截到某個方法的調用,來實現攔截器的功能,這種手段我們稱為Method Swizzling。Aspects通過這個手段實現了針對某個類和某個實例中方法的攔截。
另外,也可以使用protocol的方式來實現攔截器的功能
設計心法
針對View層的架構不光是看重如何合理地拆分MVC來給UIViewController減負,另外一點也要照顧到業務方的使用成本。最好的情況是業務方什么都不知道,然后他把代碼放進去就能跑,同時還能獲得框架提供的種種功能。
第一心法:盡可能減少繼承層級,涉及蘋果原生對象的盡量不要繼承
繼承是罪惡,盡量不要繼承。就我目前了解到的情況看,除了安居客的Pad App沒有在框架級針對UIViewController有繼承的設計以外,其它公司或多或少都針對UIViewController有繼承,包括安居客iPhone app(那時候我已經對此無能為力,可見View的架構在一開始就設計好有多么重要)。甚至有的還對UITableView有繼承,這是一件多么令人發指,多么慘絕人寰,多么喪心病狂的事情啊。雖然不可避免的是有些情況我們不得不從蘋果原生對象中繼承,比如UITableViewCell。但我還是建議盡量不要通過繼承的方案來給原生對象添加功能,前面提到的Aspect方案和Category方案都可以使用。用Aspect+load來實現重載函數,用Category來實現添加函數,當然,耍點手段用Category來添加property也是沒問題的。這些方案已經覆蓋了繼承的全部功能,而且非常好維護,對于業務方也更加透明,何樂而不為呢。
不用繼承可能在思路上不會那么直觀,但是對于不使用繼承帶來的好處是足夠頂得上使用繼承的壞處的。順便在此我要給Category正一下名:業界對于Category的態度比較曖昧,在多種場合(講座、資料文檔)都宣揚過盡可能不要使用Category。它們說的都有一定道理,但我認為Category是蘋果提供的最好的使用集合代替繼承的方案,但針對Category的設計對架構師的要求也很高,請合理使用。而且蘋果也在很多場合使用Category,來把一個原本可能很大的對象,根據不同場景拆分成不同的Category,從而提高可維護性。
不使用繼承的好處我在這里已經說了,放到iOS應用架構來看,還能再多額外兩個好處:1. 在業務方做業務開發或者做Demo時,可以脫離App環境,或花更少的時間搭建環境。2. 對業務方來說功能更加透明,也符合業務方在開發時的第一直覺。
第二心法:做好代碼規范,規定好代碼在文件中的布局,尤其是ViewController
這主要是為了提高可維護性。在一個文件非常大的對象中,尤其要限制好不同類型的代碼在文件中的布局。比如在寫ViewController時,我之前給團隊制定的規范就是前面一段全部是getter setter,然后接下來一段是life cycle,viewDidLoad之類的方法都在這里。然后下面一段是各種要實現的Delegate,再下面一段就是event response,Button的或者GestureRecognizer的都在這里。然后后面是private method。一般情況下,如果做好拆分,ViewController的private method那一段是沒有方法的。后來隨著時間的推移,我發現開頭放getter和setter太影響閱讀了,所以后面改成全放在ViewController的最后。
第三心法:能不放在Controller做的事情就盡量不要放在Controller里面去做
Controller會變得龐大的原因,一方面是因為Controller承載了業務邏輯,MVC的總結者(在正式提出MVC之前,或多或少都有人這么設計,所以說MVC的設計者不太準確)對Controller下的定義也是承載業務邏輯,所以Controller就是用來干這事兒的,天經地義。另一方面是因為在MVC中,關于Model和View的定義都非常明確,很少有人會把一個屬于M或V的東西放到其他地方。然后除了Model和View以外,還會剩下很多模棱兩可的東西,這些東西從概念上講都算Controller,而且由于M和V定義得那么明確,所以直覺上看,這些東西放在M或V是不合適的,于是就往Controller里面塞咯。
正是由于上述兩方面原因導致了Controller的膨脹。我們再細細思考一下,Model膨脹和View膨脹,要針對它們來做拆分其實都是相對容易的,Controller膨脹之后,拆分就顯得艱難無比。所以如果能夠在一開始就盡量把能不放在Controller做的事情放到別的地方去做,這樣在第一時間就可以讓你的那部分將來可能會被拆分的代碼遠離業務邏輯。所以我們要稍微轉變一下思路:模棱兩可的模塊,就不要塞到Controller去了,塞到V或者塞到M或者其他什么地方都比塞進Controller好,便于將來拆分
。
所以關于前面我按下不表的關于胖Model和瘦Model的選擇,我的態度是更傾向于胖Model??陀^地說,業務膨脹之后,代碼規??隙ㄉ俨涣说模还苣慵夹g再好,經驗再豐富,代碼量最多只能優化,該膨脹還是要膨脹的,而且優化之后代碼往往也比較難看,使用各種奇技淫巧也是有代價的。所以,針對代碼量優化的結果,往往要么就是犧牲可讀性,要么就是犧牲可移植性(通用性),Every magic always needs a pay, you have to make a trade-off.
。
那么既然膨脹出來的代碼,或者將來有可能膨脹的代碼,不管放在MVC中的哪一個部分,最后都是要拆分的,既然遲早要拆分,那不如放Model里面,這樣將來拆分胖Model也能比拆分胖Cotroller更加容易。在我還在安居客的時候,安居客Pad app承載最復雜業務的ViewController才不到600行,其他多數Controller都是在300-400行之間,這就為后面接手的人降低了非常多的上手難度和維護復雜度。拆分出來的東西都是可以直接遷移給iPhone app使用的?,F在看天貓的ViewControler,動不動就幾千行,看不了多久頭就暈了,問了一下,大家都表示很習慣這樣的代碼長度,攤手。
第四心法:架構師是為業務工程師服務的,而不是去使喚業務工程師的
架構師在公司里的職級和地位往往都是要高于業務工程師的,架構師的技術實力和經驗往往也都是高于業務工程師的。所以你值得在公司里獲得較高的地位,但是在公司里的地位高不代表在軟件工程里面的角色地位也高
。架構師是要為業務工程師服務的,是他們使喚你而不是你使喚他們
。另外,制定規范一方面是起到約束業務工程師的代碼,但更重要的一點是,這其實是利用你的能力幫助業務工程師避免他無法預見的危機,所以地位高有一定的好處,畢竟夏蟲不可語冰,有的時候不見得能夠解釋得通,因此高地位隨之而來的就是說服力會比較強。但在軟件工程里,一定要保持謙卑,一定要多為業務工程師考慮。
一個不懂這個道理的架構師,設計出來的東西往往復雜難用,因為他只愿意做核心的東西,周邊不愿意做的都期望交給業務工程師去做,甚至有的時候就只做了個Demo,然后就交給業務工程師了,業務工程師變成給他打工的了。但是一個懂得這個道理的架構師,設計出來的東西會非常好用,業務方只需要扔很少的參數然后拿結果就好了,這樣的架構才叫好的架構。
舉一個保存圖片到本地的例子,一種做法是提供這樣的接口:
- (NSString *)saveImageWithData:(NSData *)imageData
另一種是
- (NSString *)saveImage:(UIImage *)image
后者更好,原因自己想。
你的態度越謙卑,就越能設計出好的架構,這是我設計心法里的最后一條,也是最重要的一條。即使你現在技術實力不是業界大牛級別的,但只要保持這個心態去做架構,去做設計,就已經是合格的架構師了,要成為業界大牛也會非???。
小結
其實針對View層的架構設計,還是要做好三點:代碼規范,架構模式,工具集。
代碼規范對于View層來說意義重大,畢竟View層非常重業務,如果代碼布局混亂,后來者很難接手,也很難維護。
架構模式具體如何選擇,完全取決于業務復雜度。如果業務相當相當復雜,那就可以使用VIPER,如果相對簡單,那就直接MVC稍微改改就好了。每一種已經成為定式的架構模式不見得都適合各自公司對應的業務,所以需要各位架構師根據情況去做一些拆分或者改變。拆分一般都不會出現問題,改變的時候,只要別把MVC三個角色搞混就好了,M該做啥做啥,C該做啥做啥,V該做啥做啥,不要亂來。關于大部分的架構模式應該是什么樣子,這篇文章里都已經說過了,不過我認為最重要的還是后面的心法,模式只是招術,熟悉了心法才能大巧不工。
View層的工具集主要還是集中在如何對View進行布局,以及一些特定的View,比如帶搜索提示的搜索框這種。這篇文章只提到了View布局的工具集,其它的工具集相對而言是更加取決于各自公司的業務的,各自實現或者使用CocoaPods里現成的都不是很難。
對于小規?;蛘咧械纫幠OS開發團隊來說,做好以上三點就足夠了。在大規模團隊中,有一個額外問題要考慮,就是跨業務頁面調用方案的設計(這就涉及到組件化,后期專門講這塊)。
參考搬運自Casa大神的iOS應用架構談 view層的組織和調用方案