前言
我們?cè)谝话銓?shí)現(xiàn)一個(gè)系統(tǒng)的時(shí)候闸溃,通常是將定義與實(shí)現(xiàn)合為一體胯努,不加分離的东臀,但是有時(shí)候最為理想的系統(tǒng)設(shè)計(jì)規(guī)范應(yīng)是所有的定義與實(shí)現(xiàn)分離,盡管這可能對(duì)系統(tǒng)中的某些情況有點(diǎn)麻煩。
在一個(gè)面向?qū)ο蟮南到y(tǒng)中龄广,系統(tǒng)的各種功能是由許許多多的不同對(duì)象協(xié)作完成的硫眯。在這種情況下,各個(gè)對(duì)象內(nèi)部是如何實(shí)現(xiàn)自己的,對(duì)系統(tǒng)設(shè)計(jì)人員來(lái)講就不那么重要了择同;而各個(gè)對(duì)象之間的協(xié)作關(guān)系則成為系統(tǒng)設(shè)計(jì)的關(guān)鍵两入。小到不同類之間的通信,大到各模塊之間的交互敲才,在系統(tǒng)設(shè)計(jì)之初都是要著重考慮的裹纳,這也是系統(tǒng)設(shè)計(jì)的主要工作內(nèi)容。面向接口編程就是指按照這種思想來(lái)編程归斤。
關(guān)于接口的理解
接口從更深層次的理解痊夭,應(yīng)是定義(規(guī)范,約束)與實(shí)現(xiàn)(名實(shí)分離的原則)的分離脏里。
接口的本身反映了系統(tǒng)設(shè)計(jì)人員對(duì)系統(tǒng)的抽象理解。
接口應(yīng)有兩類:
第一類是對(duì)一個(gè)體的抽象虹曙,它可對(duì)應(yīng)為一個(gè)抽象體(abstract class)迫横;
第二類是對(duì)一個(gè)體某一方面的抽象,即形成一個(gè)抽象面(interface)酝碳;
依賴注入(Dependency Injection)
說(shuō)到面向接口編程矾踱,我覺(jué)得還是有必要說(shuō)下依賴注入這個(gè)概念硫痰,下面舉下網(wǎng)上很多文章都在舉的例子來(lái)說(shuō)明下:
我們有一個(gè)公交車(chē)類(Bus)访忿,每天早上6點(diǎn)鐘需要發(fā)車(chē)(work)煤蹭,為其分配對(duì)應(yīng)的司機(jī)(Driver)毕莱,看代碼
@implementation Bus
- (void)work {
Driver *driver = [[Driver alloc] initWithName:@"張三"];
//dosomething
}
@end
在上面這段代碼中储耐,Bus對(duì)象的運(yùn)作需要用到Driver對(duì)象动分,因而創(chuàng)建了一個(gè)Driver對(duì)象郑气,我們稱Bus對(duì)Driver有一個(gè)依賴怠堪。這樣的強(qiáng)耦合關(guān)系會(huì)因?yàn)槿蘸蟮淖兓o我們帶來(lái)很多麻煩芽偏,不久將來(lái)張三師傅辭職了雷逆,我們需要修改Bus-work()的代碼,也就是說(shuō)在開(kāi)發(fā)過(guò)程中非常不便于單元測(cè)試(一是不能方便地更換各種Driver對(duì)象污尉,二是如果Driver這個(gè)職位創(chuàng)建是耗時(shí)操作或者高成本操作膀哲,我們并不能使用準(zhǔn)備好的Driver實(shí)現(xiàn)快速重復(fù)測(cè)試)。 我們繼續(xù):
@implementation Bus
@property (strong, nonatomic) Driver *driver;
- (instancetype)initWithDriver:(Driver *)driver {
self = [super init];
if (self) {
self.dirver = driver;
}
return self;
}
- (void)work {
//dosomething
}
@end
以上這段代碼我們通過(guò)init方法被碗,為Bus對(duì)象傳入了一個(gè)Driver對(duì)象某宪,像這種非自己主動(dòng)初始化依賴,而從外部通過(guò)注入點(diǎn)注入依賴的方式锐朴,我們就稱為依賴注入兴喂,而例子中的這種注入的方法稱之為構(gòu)造器注入。明顯的,這個(gè)場(chǎng)景中Bus和Driver的耦合因此輕了一層瞻想。說(shuō)到解耦压真,并不是說(shuō)Bus和Driver之間的依賴關(guān)系就不存在了,在Bus的范圍內(nèi)看來(lái)蘑险,只是將依賴建立從編譯期間推遲到了運(yùn)行期間滴肿,畢竟Bus無(wú)論如何也是需要Driver提供服務(wù)的。對(duì)此佃迄,這篇文章有一個(gè)非常形象的比喻泼差,“依賴就像是系統(tǒng)中的 plugin (插件),主系統(tǒng)并不強(qiáng)依賴于任何一個(gè)插件呵俏,但一旦插件被加載堆缘,主系統(tǒng)就應(yīng)該可以準(zhǔn)確調(diào)用適當(dāng)插件的功能”。
類似這樣的注入方式還有
* 屬性注入
* 方法注入
* 環(huán)境上下文注入
* 子類重寫(xiě)方法注入等
不同的只是注入的手段普碎,思想還是一樣的吼肥。
實(shí)際開(kāi)發(fā)中的實(shí)例展示
下面文中用到這個(gè)例子和代碼來(lái)著這篇文章,我覺(jué)得文章中的這個(gè)例子很有啟發(fā)性麻车,所以加上了一些自己的理解進(jìn)去缀皱,想看原文的可以點(diǎn)進(jìn)進(jìn)入閱讀原著。
MVP模式雖然能解決許多MVC模式下存在的問(wèn)題动猬,但對(duì)于比較復(fù)雜的需求啤斗,
還是會(huì)存在邏輯過(guò)于復(fù)雜,Presenter層也出現(xiàn)難以維護(hù)的問(wèn)題赁咙。下面我們就通過(guò)一個(gè)實(shí)際的例子钮莲,
來(lái)看看面對(duì)復(fù)雜的業(yè)務(wù)邏輯,我們應(yīng)該如何去設(shè)計(jì)和實(shí)現(xiàn)彼水。
很多復(fù)雜的需求崔拥,在最初都是從一個(gè)簡(jiǎn)單的場(chǎng)景,一步步往上增加功能猿涨。
在這個(gè)過(guò)程中握童,如果不持續(xù)的進(jìn)行優(yōu)化和重構(gòu),到最后就成了所謂的"只有上帝能看懂的代碼"叛赚。說(shuō)了這么多澡绩,進(jìn)入正題,來(lái)看這個(gè)需求俺附。
V1.0 單文件上傳
實(shí)現(xiàn)一個(gè)簡(jiǎn)單的單文件上傳肥卡,文件的索引存儲(chǔ)在數(shù)據(jù)庫(kù)中,文件存儲(chǔ)在App的沙箱里面事镣。這個(gè)應(yīng)該對(duì)于有經(jīng)驗(yàn)的客戶端開(kāi)發(fā)者來(lái)說(shuō)是小菜一碟步鉴,比較簡(jiǎn)單也容易實(shí)現(xiàn)。我們可以把這個(gè)需求大致拆分成以下幾個(gè)子需求
- 初始化上傳View
- 更新上傳View
- 點(diǎn)擊上傳按鈕事件
- 數(shù)據(jù)庫(kù)中獲取上傳模型
- 發(fā)起HTTP請(qǐng)求上傳文件
- 檢查網(wǎng)絡(luò)狀態(tài)
以上幾項(xiàng)如果使用傳統(tǒng)的MVC模式,實(shí)現(xiàn)起來(lái)如下圖所示
我們可以看到上述需求基本都直接在UploadViewController
中實(shí)現(xiàn)氛琢,目前需求還是比較簡(jiǎn)單的情形下面喊递,還是勉強(qiáng)能夠接受,也不需要更多的思考阳似。如果使用MVP的模式進(jìn)行優(yōu)化骚勘,如下圖所示
現(xiàn)在UploadPresenter
負(fù)責(zé)處理上傳邏輯了,而UploadViewController
專注于UI更新和事件傳遞撮奏,整體的結(jié)構(gòu)更加清晰俏讹,以后維護(hù)代碼也會(huì)比較方便。
V2.0 多文件上傳
需求來(lái)了畜吊!需要在原來(lái)的基礎(chǔ)上支持多文件上傳泽疆,意味著我們多了一個(gè)子需求
- 維護(hù)上傳模型隊(duì)列
很顯然,我們需要在UploadPresenter
中增加一個(gè)維護(hù)上傳隊(duì)列的功能玲献,最初我也確實(shí)是這樣實(shí)現(xiàn)的殉疼,但是由于文件上傳需要監(jiān)聽(tīng)的事件比較多,回調(diào)也比較頻繁青自,直接在Presenter中繼續(xù)寫(xiě)這樣的邏輯代碼株依,已經(jīng)成倍增加了代碼的復(fù)雜性。
所以經(jīng)過(guò)一番思考延窜,我考慮把文件上傳這部分的邏輯單獨(dú)提取出一層FileUploader
,而UploadPresenter
只負(fù)責(zé)維護(hù)FileUploader
的隊(duì)列以及檢查網(wǎng)絡(luò)狀態(tài)抹锄。具體的實(shí)現(xiàn)如下所示逆瑞。
我們可以看到,分層之后的結(jié)構(gòu)又更加清晰了伙单,每一層的職責(zé)都比較單一获高,目前看起來(lái)一切OK!
V3.0 多來(lái)源上傳
原來(lái)我們的上傳文件的來(lái)源是存在于App沙箱里的吻育,我們通過(guò)數(shù)據(jù)庫(kù)查詢可以找到這個(gè)文件的索引和路徑念秧,進(jìn)而獲取到這個(gè)文件進(jìn)行上傳。現(xiàn)在萬(wàn)惡的需求又來(lái)了布疼,需要支持上傳系統(tǒng)相冊(cè)中獲取的圖片/視頻摊趾。
- 支持系統(tǒng)相冊(cè)和App沙箱中獲取文件
到這里可能有些讀者已經(jīng)有點(diǎn)頭大了,如果沒(méi)有仔細(xì)思考游两,很可能從這里就走向了代碼質(zhì)量崩潰的道路砾层。
這個(gè)時(shí)候,我們就要思考贱案,他們是多來(lái)源肛炮,但是對(duì)于FileUploader來(lái)說(shuō),它其實(shí)不關(guān)心模型的來(lái)源,它只需要獲取到模型的二進(jìn)制流侨糟。于是碍扔,我們可以抽象出一個(gè)BaseModel
,提供一個(gè)stream
只讀屬性秕重,兩種來(lái)源分別繼承BaseModel
不同,各自重載stream
只讀屬性,實(shí)現(xiàn)自己的構(gòu)造文件stream
的方法悲幅。對(duì)于FileUploader
來(lái)說(shuō)套鹅,它只持有BaseModel
即可,這就是繼承和多態(tài)的一個(gè)典型的使用場(chǎng)景汰具。
如果后續(xù)還有更多來(lái)源的文件卓鹿,比如網(wǎng)絡(luò)文件(先下載再上傳?),也只需要繼續(xù)繼承BaseModel
留荔,重載stream
即可吟孙,對(duì)于FileUploader
和它的所有上層來(lái)說(shuō),一切都是透明的聚蝶,無(wú)需進(jìn)行修改杰妓。經(jīng)過(guò)這樣的設(shè)計(jì),我們的代碼的可維護(hù)性和可擴(kuò)展性又好了碘勉。下面是架構(gòu)圖巷挥。
V4.0 多方式上傳
在HTTP文件上傳中,我們可以直接上傳文件的二進(jìn)制流验靡,這種就需要服務(wù)端做特定的支持倍宾。但更為常用和支持廣泛的做法是使用HTTP表單文件傳輸,即組裝HTTP請(qǐng)求的body時(shí)采用multipart/form-data
的標(biāo)準(zhǔn)組裝胜嗓,傳輸數(shù)據(jù)高职。于是,我們又多了一個(gè)需求:
- 支持表單傳輸和流傳輸
思路和剛才的多來(lái)源上傳差不多辞州,我們把上面的兩種來(lái)源的模型怔锌,即FSBaseM
和ABaseM
抽象為父類,父類含有各自的文件二進(jìn)制數(shù)據(jù)的抽象变过,子類分別實(shí)現(xiàn)二進(jìn)制直接組裝流埃元,和按multipart/form-data
格式組裝流,實(shí)現(xiàn)如下圖牵啦。
V5.0 支持FTP/Socket上傳
剛才我們的文件上傳亚情,底層的協(xié)議是基于Http,此時(shí)我們需要支持FTP/Socket協(xié)議的傳輸哈雏,應(yīng)該怎么辦楞件?
- 支持HTTP/FTP/Socket
經(jīng)過(guò)上面的思考衫生,相信你一定知道該怎么做了。這里留個(gè)思考土浸,答案:
對(duì)比
最后罪针,我們把目前的需求全都整理一下
- 初始化上傳View
- 更新上傳View
- 點(diǎn)擊上傳按鈕事件
- 數(shù)據(jù)庫(kù)中獲取上傳模型
- 發(fā)起HTTP請(qǐng)求上傳文件
- 檢查網(wǎng)絡(luò)狀態(tài)
- 維護(hù)上傳模型隊(duì)列
- 支持系統(tǒng)相冊(cè)和App沙箱中獲取文件
- 支持表單傳輸和流傳輸
- 支持HTTP/FTP/Socket
我們看看,如果分別采用MVC黄伊、MVP_V1泪酱、MVP_V2、MVP_V3还最、MVP_V4墓阀、MVP_V5,來(lái)實(shí)現(xiàn)目前的十個(gè)需求拓轻,我們的代碼大致會(huì)分布在哪幾層斯撮。
孰優(yōu)孰劣一目了然。如果采用最原始的MVC模式的話扶叉,保守估計(jì)ViewController
代碼量至少3K行以上勿锅。
總結(jié)
- 運(yùn)用MVP的設(shè)計(jì)模式,邏輯和UI操作解耦
- 分層模式枣氧,上層擁有下層溢十,下層通過(guò)接口與上層通信,達(dá)到解耦达吞。
- 利用繼承和多態(tài)张弛,屏蔽底層實(shí)現(xiàn)的細(xì)節(jié),達(dá)到職責(zé)分離和高擴(kuò)展性
代碼優(yōu)化和重構(gòu)的技巧
在這次的項(xiàng)目重構(gòu)中酪劫,我也總結(jié)了一些重構(gòu)方面的技巧和貼士乌庶,希望能幫助到想開(kāi)始進(jìn)行代碼重構(gòu)的同學(xué)
事不過(guò)三
大段重復(fù)的代碼出現(xiàn)了三次或以上——提取成一個(gè)公共的方法,這一點(diǎn)是最常見(jiàn)也最容易做到的契耿,只要在平時(shí)的編碼過(guò)程中養(yǎng)成這種習(xí)慣,對(duì)于出現(xiàn)過(guò)三次以上重復(fù)代碼段螃征,提取成一個(gè)公共方法搪桂。
一個(gè)類的職責(zé)有三種或以上——通過(guò)合理分層的方式,減少職責(zé)盯滚,這一點(diǎn)在上面的例子中已經(jīng)闡述地比較清楚了踢械,通過(guò)職責(zé)的分層,上層持有下層魄藕,下層通過(guò)接口與上層通訊内列。其實(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à)錢(qián)和品種
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
}
}
這里的對(duì)名稱name的相同的if/else判斷出現(xiàn)了三次,如果此時(shí)我們多了一種水果梨架专,我們得修改上述所有的if/else判斷同窘,這樣就會(huì)非常難維護(hù)。
這種場(chǎng)景我們可以考慮抽象出一個(gè)Fruit的抽象類/接口/協(xié)議部脚,通過(guò)實(shí)現(xiàn)水果類/接口/協(xié)議的方式想邦,此時(shí)如果多了一種水果,讓這種水果繼續(xù)實(shí)現(xiàn)Fruit協(xié)議就行睛低,這樣我們就通過(guò)新增的方式替代修改案狠,提高了代碼的可維護(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 "皇帝柑"
}
}
這個(gè)例子跟我之前提的一篇文章的例子是極其相似的:使用state pattern替代if else钱雷,都是面向接口編程的典型實(shí)例骂铁。而且這個(gè)例子還不如我之前提到的那篇文章中的實(shí)例更能說(shuō)明面向接口編程的必要性,因?yàn)槟芹邕壿嫷阶詈髸?huì)發(fā)展為不得不解決的痛了罩抗。
合理分層
縱向分層——層級(jí)之間有關(guān)聯(lián)
上層持有下層拉庵,下層通過(guò)接口與上層通信。這里為什么不讓下層也持有上層呢套蒂?主要還是為了能夠解耦钞支,下層設(shè)計(jì)的目的是為上層服務(wù)的,它不應(yīng)該依賴上層操刀。這種設(shè)計(jì)模式在計(jì)算機(jī)科學(xué)中是很常見(jiàn)的烁挟,比如計(jì)算機(jī)網(wǎng)絡(luò)中的網(wǎng)絡(luò)分層設(shè)計(jì)。橫向分層——層級(jí)之間無(wú)關(guān)聯(lián)
適用于功能相對(duì)獨(dú)立的模塊骨坑,簡(jiǎn)單劃分即可撼嗓。我們的iOS項(xiàng)目的首頁(yè)就是由好幾個(gè)部分組成,這個(gè)部分之間無(wú)太多的關(guān)聯(lián)欢唾,我們簡(jiǎn)單劃分成幾個(gè)模塊就行且警。如果出現(xiàn)了少數(shù)需要通訊的場(chǎng)景,使用Notification即可礁遣。
避免過(guò)度設(shè)計(jì)
越簡(jiǎn)單的越是有效的斑芜,復(fù)雜的架構(gòu)設(shè)計(jì)往往在客戶端高速迭代開(kāi)發(fā)中意義不大(相比服務(wù)端)
沒(méi)有銀彈!軟件開(kāi)發(fā)是工程化的祟霍,沒(méi)有完美的架構(gòu)模式杏头,很多時(shí)候需要具體問(wèn)題具體分析盈包,靈活運(yùn)用設(shè)計(jì)模式,得到局部的最優(yōu)解大州。比如前面提到的MVP模式续语,如果生搬硬套,同樣無(wú)法解決Presenter層復(fù)雜的問(wèn)題厦画。
如何判斷過(guò)度設(shè)計(jì)疮茄?膠水代碼過(guò)多,大量文件的行數(shù)小于100根暑,想了一天力试,沒(méi)寫(xiě)出代碼,也沒(méi)寫(xiě)出架構(gòu)方案
重構(gòu)的時(shí)機(jī)和對(duì)象
-
時(shí)機(jī)排嫌,單文件代碼行數(shù)開(kāi)始超過(guò)500行的時(shí)候
Code Review是重構(gòu)的好幫手 -
對(duì)象畸裳,需求經(jīng)常變化或增加的功能,一定要注意設(shè)計(jì)淳地,避免走向質(zhì)量不可控
穩(wěn)定且不變的功能怖糊,不重構(gòu)
總結(jié)
上文中我引用的是一篇?jiǎng)e人文章中的內(nèi)容,我從中看到的是面向接口的思想颇象,并沒(méi)有多突出 MVP的思想伍伤。面向接口的編程思想對(duì)程序的擴(kuò)展性和維護(hù)性是極友好的,是大型系統(tǒng)中復(fù)雜邏輯的一個(gè)極其好的解決方案遣钳,本文只是一個(gè)例子扰魂,一個(gè)引子,引導(dǎo)我們從這個(gè)思想層面來(lái)思考程序的設(shè)計(jì)蕴茴,希望大家有所獲劝评。
參考文章: