本文從移動端構架設計、類設計、方法設計以及最佳實踐等方面簡單討論瞭如何開發出高質量的代碼。git
本文同時發表於個人我的博客github
高質量代碼、架構設計以及重構等都是充滿智慧且須要深厚功底和實戰經驗的話題,本不敢隨意拿來討論。只是最近在項目中對兩個較大的模塊作了一次重構,再加上補習了一下《代碼大全》以及《重構》,所以嘗試着作一次這方面的分享。代碼設計自己也是一個仁者見仁、智者見智的話題,下述討論若有不正確之處,請指正。數據庫
首先須要回答的問題是什麼樣的代碼是高質量代碼,其次纔是如何設計出高質量代碼。編程
從宏觀上說,高質量代碼無外乎於:可擴展性強、可維護性高、可讀性好。 從實現的層面說,主要包含:劃分層次合理、數據流向簡潔明瞭、模塊間通訊合法、類有良好的封裝和一致的抽象、實體內部高內聚、實體間低耦合等。json
對於終端開發來講,架構設計最經典莫過於 MVC,在此基礎上又衍生出了像 MVVM 之類的架構。 以前看過一篇文章討論是先分層再分模塊,仍是先分模塊再分層的問題。雖然,這兩種方式各有道理,但我的仍是建議先劃分模塊,再在模塊內部按 MVC 或其餘架構分層,由於對於終端來講模塊有更強的獨立性,一個模塊基本上也是由M、V、C 三部分構成。數組
關於 MVC、MVVM 等構架的文章已有不少,本文再也不贅述。安全
以 MVC 爲例,須要強調的是 M、V、C三者間的通訊規則: 微信
項目中,每每一個模塊由一我的單獨負責,這就致使不一樣的模塊可能採用不一樣的架構,MVC、MVP、MVVM 以及其餘變體構架可能在一個項目中同時存在,顯得有些混亂。最好是能統一一下,一個項目只用一種構架。多線程
在面向對象編程中,類的好壞直接影響代碼的質量,那麼什麼樣的類是設計良好的類? 一樣,從宏觀上說設計良好的類具備:良好的抽象與封裝。架構
抽象與封裝是兩個關係很是緊密的概念。
代碼大全對抽象的定義:抽象是一種能讓你在關注某一律唸的同時能夠放心地忽略其中一些細節的能力。
具體到類上,抽象主要指接口的抽象,即對外宣稱該類具備什麼樣的能力、能完成哪些工做。而類的具體實現對外界是個黑盒子,無需關心。 封裝更強調的是隱藏細節,迫使外界沒法瞭解類內部的細節信息。
經過抽象接口能夠簡化外界對類的使用,使其無需關心類內部複雜的實現細節。 在設計接口時,須要注意的問題:
接口須要展示一致的抽象層次
接口應隱藏細節 接口應該隱藏業務層無需關心的細節問題。在咱們項目中,支持遊客登陸和 QQ 登陸,而登陸模塊將這兩種登陸方式區分開來以不一樣的 notification 通知業務層相關的登陸信息。然而絕大多數業務邏輯並不關心具體的登陸方式,若在此基礎上添加微信等其餘登陸方式,接口將更加難以維護。 所以,在登陸模塊重構後將這些細節信息都隱藏在登陸模塊內部,對外提供統一的回調接口。
高內聚 不管是在類層次仍是方法層次,高內聚一直是咱們追求的目標之一。對類來講,抽象與內聚關係十分緊密,類具備良好抽象的接口通常也意味着有很高的內聚性。
在咱們 QQ 閱讀項目中,有一個很是重要的類:QRBookInfo
,從名字看應該是一個用於表示書籍信息的類。其對外的接口應該有:id(惟一編號)、name(名稱)、author(做者)以及 format(格式)等。但在現實中 QRBookInfo
類卻包含了不少不屬於它的信息,如:閱讀進度、閱讀時間、添加到書架時間以及在書架上分組 id 等,最終該類暴露給外界的屬性有將近40個,淪落爲一個大雜燴,很明顯已經不具有高內聚特徵了。
若是要對該類進行重構,則能夠經過 Extract Class(提煉類)的手法進行,將不屬於該類的職責提煉到新的類中。從 QRBookInfo
至少能夠提煉出3個類:
QRBook
——用於描述書籍自己,抽象接口有:id(惟一編號)、name(名稱)、author(做者)以及 format(格式)等;QRBookShelfItem
——用於描述書架上每一個項的信息,抽象接口有:id(書籍編號)、addTime(添加到書架時間)以及 categoryId(該書在書架上的分組編號)等;QRReadingItem
——用於描述閱讀信息,抽象接口有:id(書籍編號)、readTime(閱讀時間)以及 readProgress(閱讀進度)等。項目中,還有個書架類:BookShelfViewController
,有5000多行代碼,註冊了20多個通知,已經到了幾乎沒法維護的地步,其中不只包含了書架相關的邏輯,打開書的邏輯也所有扔在裏面。後來在一個新項目中須要使用這個類時,稍微作一點小改動都沒法正常工做。這次對書架進行重構時將打開書的邏輯所有移到其餘類中,QRBookShelfViewController
這個類只專一於處理書架相關的邏輯。
高內聚做爲管理類複雜度的一個重要原則,咱們應該時刻把握這一利器。
儘可能讓接口可編程,拒絕隱藏的語義
來自代碼大全:每一個接口都由一個可編程(programmatic)部分和一個語義(semantic)部分組成。其中,可編程部分由接口中的數據類型和其餘屬性構成,編譯器能強制性地要求它們(在編譯時檢查錯誤),而語義部分則由『本接口將會被怎樣使用』的假定組成,而這些是沒法經過編譯器來強制實施的。
好比:在調用 methodA 前必須先調用方法 methodB,這一要求則屬於 methodA 的語義部分。因爲沒有編譯器的強制檢查,這一隱藏語義極可能被調用者忽略,引發錯誤。
所以,接口設計時儘可能不要包含語義部分,能夠經過抽取新接口或添加 Asserts 等方法將語義接口轉換爲編程接口,確實沒法避免也應在接口中經過註釋說明其中的語義。如在上述例子中,能夠添加新的接口methodC,在該接口中調用 methodB、methodA,從而消除 methodA 的語義。
謹防在修改時破壞接口的抽象 從前面討論可知,一個類應該圍繞一箇中心職責,處理一個任務。在類設計之初可能有較高的內聚性,但在實際開發中,類會被不斷擴展,不斷加入新的功能和數據。類的內聚性和抽象性極可能在這個過程當中被破壞。 常常在代碼中能夠看到一個類有大相徑庭風格、不一樣抽象的接口,這每每是被"篡改"的結果。另外還會看到有2個、3個、4個甚至更多個相同功能,只是參數不同的接口。有的接口多出個 bool 型的參數,有的帶個 block 類型的參數,有的帶個 delegate 形式的參數,在這種粗暴式的背後每每隱藏着重複代碼的危機。 在咱們的登陸模塊中,登陸接口有10個之多,登陸回調也是五花八門,有 block、delegate 以及 notification。所以在業務層的類中常常看到這3種回調方式同時存在,維護成本極高。我相信,在設計之初絕非如此混亂,而是在往後開發過程當中慢慢引入的。 所以在修改已有類接口時必定要三思,切不可圖一時之便隨意添加修改,不然長此以往極可能致使類失控。若因業務須要必須修改,最好將需求提給類的做者,由他來修改。
設計接口時儘可能不要給調用者留坑 咱們有一個基類 controller:QRBaseViewController
,其定義了一個接口,做用是讓子類自定義 NavigationBar 上的item:
self
調用其餘子方法或屬性,這樣就出現了cycle retain。通過一番排查發現有六、7個類存在這樣的問題。 (ps: 上面的 actionBlock
屬性應該使用 copy
而不是 strong
) 抽象更可能是強調這個類是什麼,能作什麼,而封裝則是強制外界沒法瞭解實現的細節。 良好的封裝通常須要注意:
儘量地限制類中各成員的可訪問性 使可訪問性儘量低是促成封裝的原則之一,在 Objective-C 中則是儘量將類的數據成員、屬性以及方法放到類的匿名分類中,使類的接口(.h文件)文件儘可能簡潔。
不要暴露類的數據成員 在 Objective-C 中,屬性做爲特殊的數據成員,能夠暴露給外界,但在這麼作以前請認真思考是否真的須要這麼作。 可是,做爲容器類的數據成員(Array、Dictionary、Set 等)必定不要輕意暴露給外界,由於這屬於很底層的實現細節。一旦暴露了這些細節,封裝將被嚴重破壞。
試想一下,如有一天須要將 Array 改成 Dictionary,影響範圍有多大? 若是暴露的是容器的mutable版本,那麼外界能夠任意對該容器進行操做,你已失去各類控制、錯誤檢查的能力,同時還可能有多線程問題。
私有實現細節必定不要暴露在頭文件中 頭文件應該是簡潔明瞭的,僅用於向外界表達該類能作什麼。簡潔的頭文件也能減小類的使用者在使用該類時的成本。
要格外警戒從語義上破壞封裝性 語法上的封裝能夠經過 private、匿名 category 等方式實現,然而語義上的封裝性更難以控制。如下是代碼大全中列舉的從語義上破壞封裝性的例子:
上述例子的問題在於,其調用方不是依賴類的抽象接口,而是依賴於類的內部實現。當經過查看類的內部實現得知能夠如何使用該類時,就不是針對接口編程,而是透過接口針對內部實現編程。這是一種十分危險的舉動,類的封裝性已被破壞,一旦類內部實現改變了,可能引發嚴重錯誤。
低耦合 兩個類之間的關聯程度稱爲耦合,低耦合一直是咱們的追求。類的封裝性直接影響到耦合程度,若類過多的將細節信息暴露出來,無疑會增長類與使用方間的耦合度。 理想狀況下,類對於調用者來講是個黑盒子,調用者經過類的接口就能完成對該類的使用,而無需深刻類內部瞭解實現細節,固然這首先須要該類有很好的抽象與封裝。反之,若在使用一個類時須要瞭解其內部實現,則必然在二者之間造成很高的耦合。 抽象性與耦合間的關係也十分緊密,良好的抽象與封裝通常也會有較低的耦合度。
項目中,有個用於表示書架分組的類:QRCategoryViewController
,該類不只用於處理分組相關的業務邏輯,連用戶分組數據的讀取、存儲也全在這個類中。而書架在決定顯示哪些書時也須要知道當前分組信息,所以,在書架初始化時必須也初始化一個QRCategoryViewController
實例,用於獲取當前分組信息。這裏,正是因爲QRCategoryViewController
類在抽象與封裝上沒有處理好,致使本來幾乎沒有耦合關係的兩個類QRCategoryViewController
與BookShelfViewController
間高度耦合。在重構時,將分組數據的管理移到一個新類QRCategoryManager
中,使QRCategoryViewController
與BookShelfViewController
間完全解耦。
做爲面向對象三大特性之一的繼承,重要性不言而喻,用好了能簡化程序,反之會增長程序複雜性。 在決定使用繼承以前,須要認真思考基類與派生類之間是不是"is...a"的關係,如若不是,那繼承就不是正確的選擇,此時可考慮"has...a"(包含)關係是否更合適。 代碼大會中關於繼承的幾個經典描述:
項目中的書架類BookShelfViewController
因爲要同時適配 iPhone 和 iPad 兩個版本,所以在代碼中隨處可見:if(IS_IPHOEN)...else
這樣的語句。 在重構的時候,將 iPhone 與 iPad 的UI邏輯抽取到兩個子類,而它們共有的數據相關邏輯(如:雲書架、章節更新等)放在基類。
QRAuthenticatorDelegate
:
QRQQAuthenticator
、
QRWeChatAuthenticator
以及
QRGuestAuthenticator
實現了
QRAuthenticatorDelegate
。
因爲 Objective-C 語言的動態性,其成員函數天生就具備虛函數的特徵,當繼承體系過於複雜,函數重載將進一步加大問題的複雜性。 繼承在使用前必定要三思,或許包含、接口是更好的選擇,使用不當會增長程序複雜性。
船體外殼裝有隔離艙、建築物都有防火牆,其做用都是隔離危險。 在防護式編程中一樣須要隔離危險,在系統層面能夠有專門的類用於處理錯誤、隔離危險(來自代碼大全):
在程序中錯誤又可分爲2類:
對於上述2種錯誤應該分別使用斷言(Assertions)和錯誤處理。回到前面那個問題,在類的公開接口中能夠視狀況使用錯誤處理或斷言,而在類的私有方法中能夠直接使用斷言。 (ps: 斷言主要用於在開發期間快速檢測代碼錯誤)
『代碼首先是寫給人看的,其次纔是讓機器執行的』,在方法設計時更應考慮這一點。
命名是個技術活,對代碼維護性、可讀性相當重要! 須要注意的是,方法名應該強調方法是作什麼的,而不是怎麼作的。 除此以外,方法名在內存管理上也有約束: 任何如下列名稱爲前綴的方法,若其返回值爲 object,則方法調用者持有該 object:alloc、new、copy以及mutableCopy。 還有一個更爲嚴格的規則:任何以 init 爲前綴的方法必須遵照下列規則:
id
或其所屬class、superclass、subclass 的對象;尤爲是在 ARC 與 MRC 混用的項目中,必須遵照上述規則,詳情可參看我以前的文章:Inside Memory Management for iOS。
一個方法只作一件事!並經過方法名很優雅的表達出來! 但在實際中不少方法每每作了更多,有的方法長達100多行,甚至幾百上千行。 低內聚的方法帶來的後果有:
將 if...else...
、for
、switch
等語句的 body 抽取爲子方法,並經過好的方法名提升可讀性。
這兩段代碼作了一樣的事情,都是根據用戶選擇加載 grid 或 line 模式的書架,但其可讀性差別仍是很大的。
常常會看到這樣的代碼:
另外,關於 if 語句的格式規範問題,雖各有各的習慣,但強烈不建議作成下面這樣(事實上大量 if 被寫成這樣,由於 Xcode默認代碼補全就是這樣的):
若是條件表達式過於複雜爲了提升可讀性,若該表達式重複出現應該抽取爲布爾方法,不然能夠將其賦值給一個良好命名的變量。 咱們書架有個規則,在 iPhone 上對於自定義的分組,若該分組下有書籍,則須要在書籍列表的最後面添加一個導入書的按鈕。
前面講到複雜的布爾表達式能夠轉換爲命名良好的中間變量,但並不意味着能夠隨意添加。多一箇中間變量就多一份複雜,多一種狀態,對於使用次數少的中間變量能夠直接將表達式內聯到語句中。
indexPath.row
含義清楚明確,不必再定義中間變量
row
。
對於類來講也是如此,爲了解決某些問題每每會引入一些狀態成員變量,這種作法無可厚非,但仍是要謹慎。每每狀態變量涉及何時 set、何時 reset,增長了類內部的複雜性,也增長了類內部方法間的耦合度。
若方法須要檢查參數或其餘條件,把檢查操做放在方法開始部分,條件不知足時當即返回。
尤爲是利用 switch 處理 enum 時,輕意不要寫 default 分支,這樣在漏掉哪一個枚舉值沒有處理時,編譯器會發出警告。但在有 default 分支時,是不會有警告的。
該小節主要討論在開發過程一些值得注意的小點,不必定與設計有關。
LLVM 7.0從編譯器層面支持泛型,系統中經常使用的容器都增長了對泛型的支持,無疑是一個重大利好。
同泛型,nullability 也是編譯器 LLVM 支持的一個特性,用於描述接口中的值是否能夠爲 nil。 對於 nonnull 的接口,若傳入的是 nil,編譯器會發出 warning。一樣,nullability對接口的自我說明能力比 warning 更有意義。
Objective-C 語言的動態性給咱們帶來了無限的想象空間,『只有想不到,沒有作不到』。 典型的利用到 Objective-C 動態性的例子有:JSPatch、MJExtension、Swizzling、KVO 以及 AOP等等。
viewWillAppear:
方法以及 UIButton 的 sendAction:
方法,記錄用戶的操做路徑,在 crash 時隨 crash log 一塊兒上報,經過該方法解決了很多疑難雜症的 crash;GCD做爲實現多線程的方式之一,結合 block 使用時簡單方便。所以,在代碼中常常存在大量dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
這樣的用法。 雖然系統會控制 GCD 使用的線程數,但當線程被鎖住時,仍是會建立新的線程以供其餘 block 使用,所以某一時段系統可能會建立出大量線程,最終會搶佔主線程的資源,影響到主線程的執行。 所以,必要時能夠統一管理這些隊列,YYDispatchQueuePool 就是一個很不錯的實現。
雖然一直追求高內聚的類,但 UI 交互的類尤爲是 UIViewController 通常功能都比較複雜,此時最好按功能排列方法,同一功能的方法放在一塊兒。 如UIViewController的這些方法應該固定排在最前面:
#pragma mark -
那就更好了。還能夠經過 category 將不一樣的功能分到不一樣的 category 中。 另外,也不要將多個類放到同一個文件中。
經過 category 能夠擴展示有類的功能,在使用 category 時有幾個點須要注意:
dealloc
方法,則主類中的 dealloc
方法將被覆蓋。尤爲是在沒有源碼的三方庫中,更加不能這樣作。首先須要代表的是,可以使用宏而不是硬編碼,是一件值得鼓勵的事情。 但宏的缺點也經常遭到詬病,主要有:非強類型、只是預編譯期的文本替換,由於宏定義沒有加括號而引起的錯誤家常便飯。 絕大多數狀況下,宏均可以用 const 或者子方法代替。看個有意思的問題,下面這個宏定義有問題嗎:
warning 表明程序存在病態,雖然有些 warning 看上去可有可無,但 warning 一旦多起來,一些重要的 warning 可能也隱藏其中,難以發現。
對於那些真的以爲可有可無的 warning,能夠經過#pragma clang diagnostic ignored
將其隱藏(固然並不推薦這麼作)。
在現在的移動互聯網時代,不少項目都是被需求推着往前走,都在追求敏捷開發、快速迭代、小步快跑,對代碼質量有所忽視。 俗話說:『磨刀不誤砍柴工』,在動手以前多一點思考,在開發過程當中少一點任性,就能寫出質量更高的代碼。 總之,高質量說來容易,作到難。不只要有豐富的實戰經驗,紮實的功底,更要有一顆執著的心!