最近完成了我司iOS項目的重構,把總體的代碼架構都梳理了一遍,主要按照MVP的架構模式,並綜合考慮了重構的難度和效果。在這個過程當中也積累了一些代碼重構方面的經驗,在這裏總結一下。數據庫
首先簡單介紹一下項目狀況。咱們原有項目的架構是比較標準的MVC模式,也是蘋果官方推薦的架構模式。Model層用來表示實體類,View層負責界面展現和傳遞UI事件,Controller層負責大部分的業務邏輯。除此以外,對一部分公共的可複用的邏輯,咱們抽象出Service層,提供給Controller使用,另外網絡層也獨立出來。下圖比較清楚地展現了總體架構 編程
MVC架構做爲蘋果官方推薦的架構模式,把數據Model和展示View經過Controller層隔離開,在項目規模較小的時候是一個不錯的選擇。隨着項目複雜性的提升,咱們也漸漸感受到MVC模式的弊端,主要體如今下面幾個方面設計模式
Controller處理業務邏輯,處理UI更新,處理UI事件,同步Model層,咱們幾乎全部的代碼都寫在了Controller層。設計模式裏有單一模式原則,你看這裏的Controller層已經至少有四種職責了。bash
這一點一方面是由於Cocoa框架裏的Controller層,就是咱們最熟悉的UIViewController
和View是自然耦合的,不少view的生命週期方法如viewWillAppear
都存在於VC,另外一方面咱們不少時候也習慣於把UI操做甚至初始化操做放在VC裏,致使UI和業務邏輯混雜在一塊兒。當你想對業務邏輯編寫單元測試的時候,看着業務邏輯代碼裏混雜的UI操做,就知道什麼叫舉步維艱——數據能夠Mock,UI是不可能被Mock的。網絡
當一個界面功能比較複雜的時候,咱們全部的邏輯代碼都會堆積在Controller中,好比咱們原有的WebViewController
的代碼就多達5000行,在這種狀況下維護代碼簡直是如履薄冰。架構
對於Controller層過於臃腫的問題,MVP模式則能較好地解決這個問題——既然UIViewController
和UIView
是耦合的,索性把這二者都歸爲View層,業務邏輯則獨立存在於Presenter層,Model層保持不變。下圖比較清除得展現了MVP模式的結構 app
在MVP模式下,Controller層和View層已經合併爲View層,專門負責處理UI更新和事件傳遞,Model層仍是做爲實體類。本來寫在ViewController層的業務邏輯已經遷移到Presenter中。MVP模式較好地解決了Controller層職責過多的問題。框架
Presenter層主要處理業務邏輯,ViewController層實現Presenter提供的接口,Presenter經過接口去更新View,這樣就實現了業務邏輯和UI解耦。若是咱們要編寫單元測試的話,只須要Mock一個對象實現Presenter提供的接口就行了。MVP模式較好地解決了UI和邏輯的解耦。單元測試
經過把業務邏輯遷移到Presenter層,Controller層的困境彷佛獲得瞭解決,可是若是某個需求邏輯較爲複雜,單純的把業務邏輯遷移解決不了根本的問題,Presenter層也會存在大量業務邏輯代碼,維護困難。這個問題,咱們下面會討論如何解決。學習
這裏主要是考慮界面間跳轉的代碼如何重構,這一點我在以前的文章裏已經有提到了,這裏給個連接iOS重構之面向協議編程實踐,另外附圖一張
前面咱們提到,MVP模式雖然能解決許多MVC模式下存在的問題,但對於比較複雜的需求,仍是會存在邏輯過於複雜,Presenter層也出現難以維護的問題。下面咱們就經過一個實際的例子,來看看面對複雜的業務邏輯,咱們應該如何去設計和實現。
不少複雜的需求,在最初都是從一個簡單的場景,一步步往上增長功能。在這個過程當中,若是不持續的進行優化和重構,到最後就成了所謂的"只有上帝能看懂的代碼"。說了這麼多,進入正題,來看這個需求。
實現一個簡單的單文件上傳,文件的索引存儲在數據庫中,文件存儲在App的沙箱裏面。這個應該對於有經驗的客戶端開發者來講是小菜一碟,比較簡單也容易實現。咱們能夠把這個需求大體拆分紅如下幾個子需求
以上幾項若是使用傳統的MVC模式,實現起來以下圖所示
UploadViewController
中實現,目前需求仍是比較簡單的情形下面,仍是勉強可以接受,也不須要更多的思考。若是使用MVP的模式進行優化,以下圖所示
如今UploadPresenter
負責處理上傳邏輯了,而UploadViewController
專一於UI更新和事件傳遞,總體的結構更加清晰,之後維護代碼也會比較方便。
需求來了!須要在原來的基礎上支持多文件上傳,意味着咱們多了一個子需求
很顯然,咱們須要在UploadPresenter
中增長一個維護上傳隊列的功能,最初我也確實是這樣實現的,可是因爲文件上傳須要監聽的事件比較多,回調也比較頻繁,直接在Presenter中繼續寫這樣的邏輯代碼,已經成倍增長了代碼的複雜性。
因此通過一番思考,我考慮把文件上傳這部分的邏輯單獨提取出一層FileUploader
,而UploadPresenter
只負責維護FileUploader
的隊列以及檢查網絡狀態。具體的實現以下所示。
原來咱們的上傳文件的來源是存在於App沙箱裏的,咱們經過數據庫查詢能夠找到這個文件的索引和路徑,進而獲取到這個文件進行上傳。如今萬惡的需求又來了,須要支持上傳系統相冊中獲取的圖片/視頻。
到這裏可能有些讀者已經有點頭大了,若是沒有仔細思考,極可能從這裏就走向了代碼質量崩潰的道路。
這個時候,咱們就要思考,他們是多來源,可是對於FileUploader來講,它其實不關心模型的來源,它只須要獲取到模型的二進制流。因而,咱們能夠抽象出一個BaseModel
,提供一個stream
只讀屬性,兩種來源分別繼承BaseModel
,各自重載stream
只讀屬性,實現本身的構造文件stream
的方法。對於FileUploader
來講,它只持有BaseModel
便可,這就是繼承和多態的一個典型的使用場景。
若是後續還有更多來源的文件,好比網絡文件(先下載再上傳?),也只須要繼續繼承BaseModel
,重載stream
便可,對於FileUploader
和它的全部上層來講,一切都是透明的,無需進行修改。通過這樣的設計,咱們的代碼的可維護性和可擴展性又好了。下面是架構圖。
在HTTP文件上傳中,咱們能夠直接上傳文件的二進制流,這種就須要服務端作特定的支持。但更爲經常使用和支持普遍的作法是使用HTTP表單文件傳輸,即組裝HTTP請求的body時採用multipart/form-data
的標準組裝,傳輸數據。因而,咱們又多了一個需求:
思路和剛纔的多來源上傳差很少,咱們把上面的兩種來源的模型,即FSBaseM
和ABaseM
抽象爲父類,父類含有各自的文件二進制數據的抽象,子類分別實現二進制直接組裝流,和按multipart/form-data
格式組裝流,實現以下圖。
剛纔咱們的文件上傳,底層的協議是基於Http,此時咱們須要支持FTP/Socket協議的傳輸,應該怎麼辦?
通過上面的思考,相信你必定知道該怎麼作了。這裏留個思考,答案請戳這裏MVP_V5架構
最後,咱們把目前的需求全都整理一下
咱們看看,若是分別採用MVC、MVP_V一、MVP_V二、MVP_V三、MVP_V四、MVP_V5,來實現目前的十個需求,咱們的代碼大體會分佈在哪幾層。
孰優孰劣一目瞭然。若是採用最原始的MVC模式的話,保守估計ViewController
代碼量至少3K行以上。
在此次的項目重構中,我也總結了一些重構方面的技巧和貼士,但願能幫助到想開始進行代碼重構的同窗
大段重複的代碼出現了三次或以上 ——提取成一個公共的方法 這一點是最多見也最容易作到的,只要在平時的編碼過程當中養成這種習慣,對於出現過三次以上重複代碼段,提取成一個公共方法。
一個類的職責有三種或以上 ——經過合理分層的方式,減小職責 這一點在上面的例子中已經闡述地比較清楚了,經過職責的分層,上層持有下層,下層經過接口與上層通信。其實這也是MVP模式的本質。
同類的if/else出現了三次或以上 ——考慮使用抽象類和多態代替if/else 若是相同的if/else判斷在你的代碼中出現了不少次的話,則應該考慮設計一個抽象類去替代這些判斷。這裏可能有點難以理解,舉個例子就好懂不少 好比,如今咱們有一個水果類,有三種水果,水果有顏色、價錢和品種
class Fruit {
var name:String = ""
func getColor() -> UIColor? {
if name == "apple" {
return UIColor.red
} else if name == "banana" {
return UIColor.yellow
} else if name == "orange" {
return UIColor.orange
}
return nil
}
func getPrice() -> Float? {
if name == "apple" {
return 10
} else if name == "banana" {
return 20
} else if name == "orange" {
return 30
}
return nil
}
func getType() -> String? {
if name == "apple" {
return "紅富士"
} else if name == "banana" {
return "芭蕉"
} else if name == "orange" {
return "皇帝"
}
return nil
}
}
複製代碼
這裏的對名稱name
的相同的if/else判斷出現了三次,若是此時咱們多了一種水果梨,咱們得修改上述全部的if/else判斷,這樣就會很是難維護。
這種場景咱們能夠考慮抽象出一個Fruit的抽象類/接口/協議,經過實現水果類/接口/協議的方式,此時若是多了一種水果,讓這種水果繼續實現Fruit協議就行,這樣咱們就經過新增的方式替代修改,提升了代碼的可維護性。
protocol Fruit {
func getPrice() -> Float?
func getType() -> String?
func getColor() -> UIColor?
var name:String { get }
}
class Apple:Fruit {
var name:String = "apple"
func getColor() -> UIColor? {
return UIColor.red
}
func getPrice() -> Float? {
return 10
}
func getType() -> String? {
return "紅富士"
}
}
class Banana:Fruit {
var name:String = "banana"
func getColor() -> UIColor? {
return UIColor.yellow
}
func getPrice() -> Float? {
return 20
}
func getType() -> String? {
return "芭蕉"
}
}
class Orange:Fruit {
var name:String = "orange"
func getColor() -> UIColor? {
return UIColor.orange
}
func getPrice() -> Float? {
return 30
}
func getType() -> String? {
return "皇帝柑"
}
}
複製代碼
Notification
便可。最後我想談談設計模式。其實重構的過程其實也就是靈活運用設計模式對代碼進行優化和改進。不少人設計模式也看了不少,學習了不少,但真正在工做中能合理使用的卻不多。因此關鍵還在靈活運用四個字上,能作到這一點,你的水平就會上一個臺階。
因此在平時的工做中,咱們要有對代碼的Taste,知道什麼樣的是好代碼,什麼樣的是髒代碼,儘早發現可優化可改進的地方,持續產出高質量代碼,而不是實現功能就萬事大吉,不然早晚要爲你之前偷的懶買單。 以上就是我在我司項目重構過程當中的的一些總結和分享,水平有限,但願對你們有所幫助。