項(xiàng)目簡介和MVP模式重構(gòu)
項(xiàng)目簡介
首先簡單介紹一下項(xiàng)目情況。我們原有項(xiàng)目的架構(gòu)是比較標(biāo)準(zhǔn)的MVC模式,也是蘋果官方推薦的架構(gòu)模式。Model層用來表示實(shí)體類,View層負(fù)責(zé)界面展示和傳遞UI事件,Controller層負(fù)責(zé)大部分的業(yè)務(wù)邏輯。除此之外,對一部分公共的可復(fù)用的邏輯,我們抽象出Service層,提供給Controller使用,另外網(wǎng)絡(luò)層也獨(dú)立出來。下圖比較清楚地展示了整體架構(gòu)

整體架構(gòu)
MVC模式的問題
MVC架構(gòu)作為蘋果官方推薦的架構(gòu)模式,把數(shù)據(jù)Model和展現(xiàn)View通過Controller層隔離開,在項(xiàng)目規(guī)模較小的時(shí)候是一個(gè)不錯(cuò)的選擇。隨著項(xiàng)目復(fù)雜性的提高,我們也漸漸感覺到MVC模式的弊端,主要體現(xiàn)在下面幾個(gè)方面
Controller層職責(zé)過多,Model和View層太簡單Controller處理業(yè)務(wù)邏輯,處理UI更新,處理UI事件,同步Model層,我們幾乎所有的代碼都寫在了Controller層。設(shè)計(jì)模式里有單一模式原則,你看這里的Controller層已經(jīng)至少有四種職責(zé)了。
業(yè)務(wù)邏輯和UI混雜在一起,難以編寫單元測試這一點(diǎn)一方面是因?yàn)镃ocoa框架里的Controller層,就是我們最熟悉的UIViewController和View是天然耦合的,很多view的生命周期方法如viewWillAppear都存在于VC,另一方面我們很多時(shí)候也習(xí)慣于把UI操作甚至初始化操作放在VC里,導(dǎo)致UI和業(yè)務(wù)邏輯混雜在一起。當(dāng)你想對業(yè)務(wù)邏輯編寫單元測試的時(shí)候,看著業(yè)務(wù)邏輯代碼里混雜的UI操作,就知道什么叫舉步維艱——數(shù)據(jù)可以Mock,UI是不可能被Mock的。
業(yè)務(wù)邏輯代碼大量存在于Controller層,維護(hù)困難當(dāng)一個(gè)界面功能比較復(fù)雜的時(shí)候,我們所有的邏輯代碼都會(huì)堆積在Controller中,比如我們原有的WebViewController的代碼就多達(dá)5000行,在這種情況下維護(hù)代碼簡直是如履薄冰。
MVP模式的重構(gòu)
對于Controller層過于臃腫的問題,MVP模式則能較好地解決這個(gè)問題——既然UIViewController和UIView是耦合的,索性把這兩者都?xì)w為View層,業(yè)務(wù)邏輯則獨(dú)立存在于Presenter層,Model層保持不變。下圖比較清除得展示了MVP模式的結(jié)構(gòu)

MVP模式簡介
我們來看一下MVP模式能否解決MVC模式存在的問題
Controller層職責(zé)過多,Model和View層太簡單在MVP模式下,Controller層和View層已經(jīng)合并為View層,專門負(fù)責(zé)處理UI更新和事件傳遞,Model層還是作為實(shí)體類。原本寫在ViewController層的業(yè)務(wù)邏輯已經(jīng)遷移到Presenter中。MVP模式較好地解決了Controller層職責(zé)過多的問題。
業(yè)務(wù)邏輯和UI混雜在一起,難以編寫單元測試Presenter層主要處理業(yè)務(wù)邏輯,ViewController層實(shí)現(xiàn)Presenter提供的接口,Presenter通過接口去更新View,這樣就實(shí)現(xiàn)了業(yè)務(wù)邏輯和UI解耦。如果我們要編寫單元測試的話,只需要Mock一個(gè)對象實(shí)現(xiàn)Presenter提供的接口就好了。MVP模式較好地解決了UI和邏輯的解耦。
業(yè)務(wù)邏輯代碼大量存在于Controller層,維護(hù)困難通過把業(yè)務(wù)邏輯遷移到Presenter層,Controller層的困境似乎得到了解決,但是如果某個(gè)需求邏輯較為復(fù)雜,單純的把業(yè)務(wù)邏輯遷移解決不了根本的問題,Presenter層也會(huì)存在大量業(yè)務(wù)邏輯代碼,維護(hù)困難。這個(gè)問題,我們下面會(huì)討論如何解決。
MVC模式改進(jìn)——Router模式
這里主要是考慮界面間跳轉(zhuǎn)的代碼如何重構(gòu),附圖一張

Router模式
實(shí)例分析
前面我們提到,MVP模式雖然能解決許多MVC模式下存在的問題,但對于比較復(fù)雜的需求,還是會(huì)存在邏輯過于復(fù)雜,Presenter層也出現(xiàn)難以維護(hù)的問題。下面我們就通過一個(gè)實(shí)際的例子,來看看面對復(fù)雜的業(yè)務(wù)邏輯,我們應(yīng)該如何去設(shè)計(jì)和實(shí)現(xiàn)。
很多復(fù)雜的需求,在最初都是從一個(gè)簡單的場景,一步步往上增加功能。在這個(gè)過程中,如果不持續(xù)的進(jìn)行優(yōu)化和重構(gòu),到最后就成了所謂的"只有上帝能看懂的代碼"。說了這么多,進(jìn)入正題,來看這個(gè)需求。
V1.0 單文件上傳
實(shí)現(xiàn)一個(gè)簡單的單文件上傳,文件的索引存儲(chǔ)在數(shù)據(jù)庫中,文件存儲(chǔ)在App的沙箱里面。這個(gè)應(yīng)該對于有經(jīng)驗(yàn)的客戶端開發(fā)者來說是小菜一碟,比較簡單也容易實(shí)現(xiàn)。我們可以把這個(gè)需求大致拆分成以下幾個(gè)子需求
初始化上傳View
更新上傳View
點(diǎn)擊上傳按鈕事件
數(shù)據(jù)庫中獲取上傳模型
發(fā)起HTTP請求上傳文件
檢查網(wǎng)絡(luò)狀態(tài)
以上幾項(xiàng)如果使用傳統(tǒng)的MVC模式,實(shí)現(xiàn)起來如下圖所示

MVC
我們可以看到上述需求基本都直接在UploadViewController中實(shí)現(xiàn),目前需求還是比較簡單的情形下面,還是勉強(qiáng)能夠接受,也不需要更多的思考。如果使用MVP的模式進(jìn)行優(yōu)化,如下圖所示

MVP.png
現(xiàn)在UploadPresenter負(fù)責(zé)處理上傳邏輯了,而UploadViewController專注于UI更新和事件傳遞,整體的結(jié)構(gòu)更加清晰,以后維護(hù)代碼也會(huì)比較方便。
V2.0 多文件上傳
需求來了!需要在原來的基礎(chǔ)上支持多文件上傳,意味著我們多了一個(gè)子需求
7.維護(hù)上傳模型隊(duì)列
很顯然,我們需要在UploadPresenter中增加一個(gè)維護(hù)上傳隊(duì)列的功能,最初我也確實(shí)是這樣實(shí)現(xiàn)的,但是由于文件上傳需要監(jiān)聽的事件比較多,回調(diào)也比較頻繁,直接在Presenter中繼續(xù)寫這樣的邏輯代碼,已經(jīng)成倍增加了代碼的復(fù)雜性。
所以經(jīng)過一番思考,我考慮把文件上傳這部分的邏輯單獨(dú)提取出一層FileUploader,而UploadPresenter只負(fù)責(zé)維護(hù)FileUploader的隊(duì)列以及檢查網(wǎng)絡(luò)狀態(tài)。具體的實(shí)現(xiàn)如下所示。

MVP2.png
我們可以看到,分層之后的結(jié)構(gòu)又更加清晰了,每一層的職責(zé)都比較單一,目前看起來一切OK!
V3.0 多來源上傳
原來我們的上傳文件的來源是存在于App沙箱里的,我們通過數(shù)據(jù)庫查詢可以找到這個(gè)文件的索引和路徑,進(jìn)而獲取到這個(gè)文件進(jìn)行上傳。現(xiàn)在萬惡的需求又來了,需要支持上傳系統(tǒng)相冊中獲取的圖片/視頻。
8.支持系統(tǒng)相冊和App沙箱中獲取文件
到這里可能有些讀者已經(jīng)有點(diǎn)頭大了,如果沒有仔細(xì)思考,很可能從這里就走向了代碼質(zhì)量崩潰的道路。
這個(gè)時(shí)候,我們就要思考,他們是多來源,但是對于FileUploader來說,它其實(shí)不關(guān)心模型的來源,它只需要獲取到模型的二進(jìn)制流。于是,我們可以抽象出一個(gè)BaseModel,提供一個(gè)stream只讀屬性,兩種來源分別繼承BaseModel,各自重載stream只讀屬性,實(shí)現(xiàn)自己的構(gòu)造文件stream的方法。對于FileUploader來說,它只持有BaseModel即可,這就是繼承和多態(tài)的一個(gè)典型的使用場景。
如果后續(xù)還有更多來源的文件,比如網(wǎng)絡(luò)文件(先下載再上傳?),也只需要繼續(xù)繼承BaseModel,重載stream即可,對于FileUploader和它的所有上層來說,一切都是透明的,無需進(jìn)行修改。經(jīng)過這樣的設(shè)計(jì),我們的代碼的可維護(hù)性和可擴(kuò)展性又好了。下面是架構(gòu)圖。

MVP3.png
V4.0 多方式上傳
在HTTP文件上傳中,我們可以直接上傳文件的二進(jìn)制流,這種就需要服務(wù)端做特定的支持。但更為常用和支持廣泛的做法是使用HTTP表單文件傳輸,即組裝HTTP請求的body時(shí)采用multipart/form-data的標(biāo)準(zhǔn)組裝,傳輸數(shù)據(jù)。于是,我們又多了一個(gè)需求:
9.支持表單傳輸和流傳輸
思路和剛才的多來源上傳差不多,我們把上面的兩種來源的模型,即FSBaseM和ABaseM抽象為父類,父類含有各自的文件二進(jìn)制數(shù)據(jù)的抽象,子類分別實(shí)現(xiàn)二進(jìn)制直接組裝流,和按multipart/form-data格式組裝流,實(shí)現(xiàn)如下圖。

MVP4.png
V5.0 支持FTP/Socket上傳
剛才我們的文件上傳,底層的協(xié)議是基于Http,此時(shí)我們需要支持FTP/Socket協(xié)議的傳輸,應(yīng)該怎么辦?
10.支持HTTP/FTP/Socket
經(jīng)過上面的思考,相信你一定知道該怎么做了。
對比
最后,我們把目前的需求全都整理一下
初始化上傳View
更新上傳View
點(diǎn)擊上傳按鈕事件
數(shù)據(jù)庫中獲取上傳模型
發(fā)起HTTP請求上傳文件
檢查網(wǎng)絡(luò)狀態(tài)
維護(hù)上傳模型隊(duì)列
支持系統(tǒng)相冊和App沙箱中獲取文件
支持表單傳輸和流傳輸
支持HTTP/FTP/Socket
我們看看,如果分別采用MVC、MVP_V1、MVP_V2、MVP_V3、MVP_V4、MVP_V5,來實(shí)現(xiàn)目前的十個(gè)需求,我們的代碼大致會(huì)分布在哪幾層。

優(yōu)化后的架構(gòu)模式之間的比較
孰優(yōu)孰劣一目了然。如果采用最原始的MVC模式的話,保守估計(jì)ViewController代碼量至少3K行以上。
總結(jié)
運(yùn)用MVP的設(shè)計(jì)模式,邏輯和UI操作解耦
分層模式,上層擁有下層,下層通過接口與上層通信,達(dá)到解耦。
利用繼承和多態(tài),屏蔽底層實(shí)現(xiàn)的細(xì)節(jié),達(dá)到職責(zé)分離和高擴(kuò)展性
代碼優(yōu)化和重構(gòu)的技巧
在這次的項(xiàng)目重構(gòu)中,我也總結(jié)了一些重構(gòu)方面的技巧和貼士,希望能幫助到想開始進(jìn)行代碼重構(gòu)的同學(xué)
事不過三
大段重復(fù)的代碼出現(xiàn)了三次或以上
——提取成一個(gè)公共的方法
這一點(diǎn)是最常見也最容易做到的,只要在平時(shí)的編碼過程中養(yǎng)成這種習(xí)慣,對于出現(xiàn)過三次以上重復(fù)代碼段,提取成一個(gè)公共方法。
一個(gè)類的職責(zé)有三種或以上
——通過合理分層的方式,減少職責(zé)
這一點(diǎn)在上面的例子中已經(jīng)闡述地比較清楚了,通過職責(zé)的分層,上層持有下層,下層通過接口與上層通訊。其實(shí)這也是MVP模式的本質(zhì)。
同類的if/else出現(xiàn)了三次或以上
——考慮使用抽象類和多態(tài)代替if/else
如果相同的if/else判斷在你的代碼中出現(xiàn)了很多次的話,則應(yīng)該考慮設(shè)計(jì)一個(gè)抽象類去替代這些判斷。這里可能有點(diǎn)難以理解,舉個(gè)例子就好懂很多
比如,現(xiàn)在我們有一個(gè)水果類,有三種水果,水果有顏色、價(jià)錢和品種
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判斷出現(xiàn)了三次,如果此時(shí)我們多了一種水果梨,我們得修改上述所有的if/else判斷,這樣就會(huì)非常難維護(hù)。
這種場景我們可以考慮抽象出一個(gè)Fruit的抽象類/接口/協(xié)議,通過實(shí)現(xiàn)水果類/接口/協(xié)議的方式,此時(shí)如果多了一種水果,讓這種水果繼續(xù)實(shí)現(xiàn)Fruit協(xié)議就行,這樣我們就通過新增的方式替代修改,提高了代碼的可維護(hù)性。
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 "皇帝柑"
}
}
合理分層
縱向分層——層級(jí)之間有關(guān)聯(lián)
上層持有下層,下層通過接口與上層通信。這里為什么不讓下層也持有上層呢?主要還是為了能夠解耦,下層設(shè)計(jì)的目的是為上層服務(wù)的,它不應(yīng)該依賴上層。這種設(shè)計(jì)模式在計(jì)算機(jī)科學(xué)中是很常見的,比如計(jì)算機(jī)網(wǎng)絡(luò)中的網(wǎng)絡(luò)分層設(shè)計(jì)。
橫向分層——層級(jí)之間無關(guān)聯(lián)
適用于功能相對獨(dú)立的模塊,簡單劃分即可。我們的iOS項(xiàng)目的首頁就是由好幾個(gè)部分組成,這個(gè)部分之間無太多的關(guān)聯(lián),我們簡單劃分成幾個(gè)模塊就行。如果出現(xiàn)了少數(shù)需要通訊的場景,使用Notification即可。
避免過度設(shè)計(jì)
越簡單的越是有效的復(fù)雜的架構(gòu)設(shè)計(jì)往往在客戶端高速迭代開發(fā)中意義不大(相比服務(wù)端)
沒有銀彈!軟件開發(fā)是工程化的,沒有完美的架構(gòu)模式,很多時(shí)候需要具體問題具體分析,靈活運(yùn)用設(shè)計(jì)模式,得到局部的最優(yōu)解。比如前面提到的MVP模式,如果生搬硬套,同樣無法解決Presenter層復(fù)雜的問題。
如何判斷過度設(shè)計(jì)?膠水代碼過多
大量文件的行數(shù)小于100
想了一天,沒寫出代碼,也沒寫出架構(gòu)方案
重構(gòu)的時(shí)機(jī)和對象
時(shí)機(jī)單文件代碼行數(shù)開始超過500行的時(shí)候
Code Review是重構(gòu)的好幫手
對象需求經(jīng)常變化或增加的功能,一定要注意設(shè)計(jì),避免走向質(zhì)量不可控
穩(wěn)定且不變的功能,不重構(gòu)
總結(jié)
最后我想談?wù)勗O(shè)計(jì)模式。其實(shí)重構(gòu)的過程其實(shí)也就是靈活運(yùn)用設(shè)計(jì)模式對代碼進(jìn)行優(yōu)化和改進(jìn)。很多人設(shè)計(jì)模式也看了很多,學(xué)習(xí)了很多,但真正在工作中能合理使用的卻很少。所以關(guān)鍵還在靈活運(yùn)用四個(gè)字上,能做到這一點(diǎn),你的水平就會(huì)上一個(gè)臺(tái)階。
所以在平時(shí)的工作中,我們要有對代碼的Taste,知道什么樣的是好代碼,什么樣的是臟代碼,盡早發(fā)現(xiàn)可優(yōu)化可改進(jìn)的地方,持續(xù)產(chǎn)出高質(zhì)量代碼,而不是實(shí)現(xiàn)功能就萬事大吉,否則遲早要為你以前偷的懶買單。