本文原創(chuàng),轉(zhuǎn)載請注明出處柴底。
歡迎關(guān)注我的 簡書 婿脸,關(guān)注我的專題 Android Class 我會長期堅持為大家收錄簡書上高質(zhì)量的 Android 相關(guān)博文。
寫在前面:
最近這段時間似枕,無論是寫文章的頻率盖淡,還是新知識的汲取年柠,都不如以往有熱情凿歼。總是拿工作忙當借口冗恨,但是心里明白還是懶和拖延作祟答憔。靜下心來反思了最近的狀態(tài),還是要及時止住惰性掀抹,保持一個良好的虐拓、有節(jié)奏的學(xué)習(xí)步調(diào)。
本文的內(nèi)容來自 Android 進階書籍《從小工到專家》傲武,六大原則和設(shè)計模式章節(jié)蓉驹。讀過之后覺得非常受用,所以為大家整理出來揪利,之后也會帶來 設(shè)計模式 和 單元測試 以及 代碼重構(gòu) 的介紹态兴,希望我們能早日從碼農(nóng)變成一個開發(fā)工程師。話不多說疟位,下面帶來書中原汁原味的內(nèi)容瞻润。
在工作的初期,我們可能會經(jīng)常有這樣的感受甜刻,自己的代碼接口設(shè)計混亂绍撞、代碼耦合較為嚴重、一個類的代碼過多等等得院,當自己回頭再看這些代碼時可能會感慨傻铣,怎么能寫成這個鳥樣。再看那些知名的開源庫祥绞,它們大多有整潔的代碼非洲、清晰簡單的接口阱驾、職責(zé)單一的類,這個時候我們會通常會捶胸頓足而感慨:什么時候老夫才能寫出這樣的代碼怪蔑!
在做開發(fā)的這些年中里覆,我漸漸的感覺到,其實國內(nèi)的一些初缆瓣、中級工程師寫的東西不規(guī)范或者說不夠清晰的原因是缺乏一些指導(dǎo)規(guī)則喧枷。他們手中揮舞著面向?qū)ο蟮拇笃欤瑢懗鰜淼臇|西卻充斥著面向過程的氣味弓坞。也許是他們不知道有這些規(guī)則隧甚,也許是他們知道但是不能很好的運用到實際的代碼中,亦或是他們沒有在實戰(zhàn)項目中體會到這些原則能夠帶來的優(yōu)點渡冻,以至于他們對這些原則并沒有足夠的重視戚扳。
本章沒有詳細介紹 OOP 六大原則、設(shè)計模式族吻、反模式等內(nèi)容帽借,只是對它們做了一些簡單的介紹。并不是因為它們不重要超歌,而是由于它們太重要砍艾,因此我們必須閱讀更詳盡的書籍來涉入這些知識,設(shè)計模式可以參考《設(shè)計模式之禪》巍举、《設(shè)計模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》以及《Android源碼設(shè)計模式解析與實戰(zhàn)》脆荷,反模式的權(quán)威書籍則為《反模式:危機中軟件、架構(gòu)和項目的重構(gòu)》一書懊悯。
(打字好累...)
面向?qū)ο罅笤瓌t
在此之前蜓谋,有一點需要大家知道,熟悉這些原則并不是說你寫出的程序就一定靈活炭分、清晰桃焕,只是為你優(yōu)秀的代碼之路鋪上了一層?xùn)艡冢谶@些原則的指導(dǎo)下欠窒,你才能避免陷入一些常見的代碼泥沼覆旭,從而讓你寫出優(yōu)秀的東西。
單一職責(zé)原則
單一職責(zé)原則的英文名稱是 SIngle Responsibility Principle岖妄,簡稱是 SPR型将,簡單地說就是一個類只做一件事,這個設(shè)計原則備受爭議卻又極其重要荐虐。只要你想和別人爭執(zhí)七兜、慪氣或者是吵架,這個原則是屢試不爽的福扬。因為單一職責(zé)的劃分界限并不是如馬路上的行車道那么清晰腕铸,很多時候都是需要個人經(jīng)驗來界定惜犀。當然,最大的問題就是對職責(zé)的定義狠裹,什么是類的職責(zé)虽界,以及怎么劃分類的職責(zé)。
試想一下涛菠,如果你遵守了這個原則莉御,那么你的類就會劃分的很細,每個類都有比較單一的職責(zé)俗冻,這不就是高內(nèi)聚礁叔、低耦合么!當然迄薄,如何界定類的職責(zé)就需要你的個人經(jīng)驗了琅关。
我們定義一個網(wǎng)絡(luò)請求的類,來體現(xiàn) SRP 的原則讥蔽,來執(zhí)行網(wǎng)絡(luò)請求的接口涣易,代碼如下:
public interface HttpStack {
/**
* 執(zhí)行 Http 請求,并且返回一個 Response
*/
public Response performRequest(Request<?> request);
}
從上述程序中可以看到勤篮,HttpStack 只有一個 performRequest 函數(shù)都毒,它的職責(zé)就是執(zhí)行網(wǎng)絡(luò)請求并且返回一個 Response色罚,它的職責(zé)很單一碰缔,這樣在需要修改執(zhí)行網(wǎng)絡(luò)請求的相關(guān)代碼時,只需要修改實現(xiàn) HttpStack 接口的類戳护,而不會影響其他類的代碼金抡。如果某個類的職責(zé)包含有執(zhí)行網(wǎng)絡(luò)請求、解析網(wǎng)絡(luò)請求腌且、進行 gzip 壓縮梗肝、封裝請求參數(shù)等,那么在你修改某處代碼時就必須謹慎铺董,以免修改的代碼影響了其它的功能巫击。當你修改的代碼能夠基本上不影響其他功能。這就一定程度上保證了代碼的可維護性精续。注意坝锰,單一職責(zé)原則并不是一個類只能有一個函數(shù),而是說這個類中的函數(shù)所做的工作是高度相關(guān)的重付,也就是高內(nèi)聚顷级。 HttpStack 抽象了執(zhí)行網(wǎng)絡(luò)請求的具體過程,接口簡單清晰确垫,也便于擴展弓颈。
優(yōu)點:
- 類的復(fù)雜性降低帽芽,實現(xiàn)什么職責(zé)都有清晰明確的定義。
- 可讀性提高翔冀,復(fù)雜性降低导街,那當然可讀性提高了。
- 可維護性提高纤子,可讀性提高了菊匿,那當然更容易維護了。
- 變更引起的風(fēng)險降低计福,變更是必不可少的跌捆,如果接口的單一職責(zé)做得好,一個接口修改只對應(yīng)的實現(xiàn)類有影響象颖,對其他的接口無影響佩厚,這對系統(tǒng)的擴展性、維護性都有非常大的幫助说订。
里氏替換原則
面向?qū)ο蟮恼Z言的三大特點是繼承抄瓦、封裝、多態(tài)陶冷,里氏替換原則就是依賴于繼承钙姊、多態(tài)這兩大特性。里氏替換原則簡單來說就是所有引用基類埂伦、接口的地方必須能透明地使用其子類的對象煞额。通俗點講,只要父類能出現(xiàn)的地方子類就可以出現(xiàn)沾谜,而且替換為子類也不會產(chǎn)生任何報錯或者異常膊毁,使用者可能根本就不需要知道是子類還是父類。但是基跑,反過來就不行了婚温,有子類出現(xiàn)的地方,父類未必就能使用媳否。
還是以 HttpStack 為例栅螟, HttpStack 來表示執(zhí)行網(wǎng)絡(luò)請求這個抽象概念。在執(zhí)行網(wǎng)絡(luò)請求時篱竭,只需要定義一個 HttpStack 對象力图,然后執(zhí)行 performRequest 即可,至于 HttpStack 的具體實現(xiàn)由更高層的調(diào)用者指定室抽。這部分代碼在 RequestQueue 類中搪哪,示例如下:
/**
* @param coreNums 核心線程數(shù)
* @param httpStack http 執(zhí)行器
*/
protected RequestQueue(int coreNums, HttpStack httpStack) {
mDispatcherNums = coreNums;
mHttpStack = httpStack != null ? httpStack : HttpStackFactory.createHttpStack();
}
HttpStackFactory 類的 createHttpStack 函數(shù)負責(zé)根據(jù) API 版本創(chuàng)建不同的 HttpStack,實現(xiàn)代碼如下:
/**
* 根據(jù) sdk 版本選擇 HttpClient 或者 HttpURLConnection
*/
public static HttpStack createHttpStack() {
int runtimeSDKApi = Build.VERSION.SDK_INT;
if (runtimeSDKApi >= GINGERBREAD_SDK_NUM) {
return new HttpUrlConnStack();
}
return new HttpClientStack();
}
上述代碼中, RequestQueue 類中依賴的是 HttpStack 接口晓折,而通過 HttpStackFactory 的 createHttpStack 函數(shù)返回的是 HttpStack 的實現(xiàn)類 HttpClientStack 或 HttpUrlConnStack惑朦。這就是所謂的里氏替換原則,任何父類漓概、父接口出現(xiàn)的地方子類都可以出現(xiàn)漾月,這不就保證了可擴展性嗎!
任何實現(xiàn) HttpStack 接口的類的對象都可以傳遞給 RequestQueue 實現(xiàn)網(wǎng)絡(luò)請求的功能胃珍,這樣執(zhí)行網(wǎng)絡(luò)請求的方法就有很多種可能性梁肿,而不是只有 HttpClient 和 HttpURLConnection。例如觅彰,用戶想使用 OkHttp 作為新的網(wǎng)絡(luò)搜索執(zhí)行引擎吩蔑,那么創(chuàng)建一個實現(xiàn)了 HttpStack 接口的 OkHttpStack 類,然后在該類的 performRequest 函數(shù)中執(zhí)行網(wǎng)絡(luò)請求填抬,最終將 OkHttpStack 對象注入 RequestQueue 即可烛芬。
細想一下,很多應(yīng)用框架不就是這樣實現(xiàn)的嗎飒责?框架定義一系列相關(guān)的邏輯骨架和抽象赘娄,使得用戶可以將自己的實現(xiàn)注入到框架中,從而實現(xiàn)變化萬千的功能宏蛉。
優(yōu)點:
- 代碼共享遣臼,減少創(chuàng)建類的工作量,每個子類都擁有父類的方法和屬性拾并。
- 提高代碼的重用性揍堰。
- 提高代碼的可擴展性,實現(xiàn)父類的方法就可以“為所欲為”了辟灰,很多開源框架的擴展接口都是通過繼承父類來完成的个榕。
- 提高產(chǎn)品或項目的開放性。
缺點:
- 繼承是侵入性的芥喇。只要繼承,就必須擁有父類所有的屬性和方法凰萨。
- 降低了代碼的靈活性继控。子類必須父類的屬性和方法,讓子類自由的世界中多了些約束胖眷。
- 增強了耦合性武通。當父類的常亮、變量和方法被修改時珊搀,必須要考慮子類的修改冶忱,而且在缺乏規(guī)范的環(huán)境下,這種修改可能帶來非常糟糕的后果---大量的代碼需要重構(gòu)境析。
依賴倒置原則
依賴倒置原則這個名字看起來有點不好理解囚枪,“依賴”還有“倒置”派诬,這到底是什么意思?依賴倒置原則的幾個關(guān)鍵點如下链沼。
- 高層模塊不應(yīng)該依賴底層模塊默赂,兩者都應(yīng)該依賴其抽象。
- 抽象不應(yīng)該依賴細節(jié)括勺。
- 細節(jié)應(yīng)該依賴抽象缆八。
在 Java 語言中,抽象就是指接口或者抽象類疾捍,兩者都是不能直接被實例化的奈辰。細節(jié)就是實現(xiàn)類、實現(xiàn)接口或者繼承抽象類而產(chǎn)生的類就是細節(jié)乱豆,其特點就是可以直接被實例化冯挎,也就是可以加上一個關(guān)鍵字 new 產(chǎn)生一個對象。依賴倒置原則是 Java 語言中的表現(xiàn)就是:模塊間的依賴通過抽象發(fā)生咙鞍,實現(xiàn)類之間不發(fā)生直接依賴的關(guān)系房官,其依賴關(guān)系是通過接口或者抽象類產(chǎn)生的。軟件先驅(qū)們總是喜歡將一些理論定義得很抽象续滋,弄得不是那么容易理解翰守,其實就是一句話:面向接口編程,或者說是面向抽象編程疲酌,這里的抽象是指抽象類或者是接口蜡峰。面向接口編程是面向?qū)ο缶柚弧?/p>
采用依賴倒置原則可以減少類之間的耦合性,提高系統(tǒng)的穩(wěn)定性朗恳,降低并行開發(fā)引起的風(fēng)險湿颅,提高代碼的可讀性和可維護性。
在前面我們的例子中粥诫, RequestQueue 實現(xiàn)類依賴于 HttpStack 接口(抽象)油航,而不依賴于 HttpClientStack 與 HttpUrlConnStack 實現(xiàn)類(細節(jié)),這就是依賴倒置原則的體現(xiàn)怀浆。如果 RequestQueue 直接依賴了 HttpClientStack 谊囚,那么 HttpUrlConnStack 就不能傳遞給 RequestQueue 了。除非 HttpUrlConnStack 繼承自 HttpClientStack 执赡。但這么設(shè)計顯然不符合邏輯镰踏,他們兩個之間是同等級的“兄弟”關(guān)系,而不是父子的關(guān)系沙合,因此奠伪,正確的設(shè)計就是依賴于 HttpStack 抽象,HttpStack 只是負責(zé)定義規(guī)范,而 HttpClientStack 和 HttpUrlConnStack 分別實現(xiàn)具體的功能绊率。這樣一來也同樣保證了擴展性谨敛。
優(yōu)點:
- 可擴展性好
- 耦合度低
開閉原則
開閉原則是 Java 世界里最基礎(chǔ)的設(shè)計原則,它指導(dǎo)我們?nèi)绾谓⒁粋€穩(wěn)定的即舌、靈活的系統(tǒng)佣盒。開閉原則的定義是:一個軟件實體類,模塊和函數(shù)應(yīng)該對擴展開放顽聂,對修改關(guān)閉肥惭。在軟件的生命周期內(nèi),因為變化紊搪、升級和維護等原因蜜葱,需要對軟件原有的代碼進行修改時,可能會給舊代碼引入錯誤耀石。因此牵囤,當軟件需要變化時,我們應(yīng)該盡量通過擴展的方式來實現(xiàn)變化滞伟,而不是通過修改已有的代碼來實現(xiàn)揭鳞。
在軟件開發(fā)過程中,永遠不變的就是變化梆奈。開閉原則是使我們的軟件系統(tǒng)擁抱變化的核心原則之一野崇。對擴展開放,對修改關(guān)閉這樣的高層次概括亩钟,即在需要對軟件進行升級乓梨、變化時應(yīng)該通過擴展的形式來實現(xiàn),而非修改原有代碼清酥。當然這只是一種比較理想的狀態(tài)扶镀,是通過擴展還是通過修改舊代碼需要依據(jù)代碼自身來定。
在我們封裝的網(wǎng)絡(luò)請求模塊中焰轻,開閉原則體現(xiàn)的比較好的就是 Request 類族的設(shè)計臭觉。我們知道,在開發(fā) C/S 應(yīng)用時鹦马,服務(wù)器返回的數(shù)據(jù)多種多樣胧谈,有字符串類型、xml荸频、Json 等。而解析服務(wù)器返回的 Response 的原始數(shù)據(jù)類型則是通過 Request 類來實現(xiàn)的客冈,這樣就使得 Request 類對于服務(wù)器返回的數(shù)據(jù)格式有良好的擴展性旭从,即 Request 的可變性太大。
例如,返回的數(shù)據(jù)格式是 Json和悦,那么使用 JsonRequest 請求來獲取數(shù)據(jù)退疫,它會將結(jié)果轉(zhuǎn)成 JsonObject 對象,我們看看 JsonRequest 的核心實現(xiàn):
// 返回的數(shù)據(jù)格式為 Json 的請求鸽素,Json 對應(yīng)的對象類型為 JSONObject
public class JsonRequest extends Request<JSONObject> {
public JsonRequest(HttpMethod method, String url,
RequestListener<JSONObject> listener) {
super(method, url, listener);
}
// 將 Response 的結(jié)果轉(zhuǎn)化為 JSONObject
@Override
public JSONObject parseResponse(Response response) {
String jsonString = new String(response.getRawData());
try {
return new JSONObject();
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
}
JsonRequest 通過實現(xiàn) Request 抽象類的 parseResponse 解析服務(wù)器返回的結(jié)果褒繁,這里將結(jié)果轉(zhuǎn)換為 JSONObject,并且封裝到 Response 類中馍忽。
例如棒坏,我們的網(wǎng)絡(luò)框架中,添加對圖片請求的支持遭笋,即要實現(xiàn)類似 ImageLoader 的功能坝冕。這個時候我的請求返回的是 Bitmap 圖片,因此瓦呼,我需要在該類型的 Request 中得到的結(jié)果是 Request喂窟,但支持一種新的數(shù)據(jù)格式不能通過修改源碼的形式,這樣可能會為舊代碼引入錯誤央串,但是磨澡,你又必須實現(xiàn)功能擴展。這就是開閉原則的定義:對擴展開放质和,對修改關(guān)閉稳摄。我們看看應(yīng)該如何做:
public class ImageRequest extends Request<Bitmap> {
public ImageRequest(HttpMethod method, String url,
RequestListener<Bitmap> listener) {
super(method, url, listener);
}
// 將 Response 的結(jié)果轉(zhuǎn)化為 Bitmap
@Override
public Bitmap parseResponse(Response response) {
return BitmapFactory.decodeByteArray(response.rawData, 0, response.rawData.length);
}
}
ImageRequest 類的 parseResponse 函數(shù)中將 Response 中的原始數(shù)據(jù)轉(zhuǎn)換成為 Bitmap 即可,當我們需要添加其他數(shù)據(jù)格式的時候侦另,只需要繼承自 Request 類秩命,并且在 parseResponse 方法中將數(shù)據(jù)轉(zhuǎn)換為具體的形式即可。這樣通過擴展的形式來應(yīng)對軟件的變化或者說用戶需求的多樣性褒傅,既避免了破壞原有系統(tǒng)弃锐,又保證了軟件系統(tǒng)的可維護性。依賴于抽象殿托,而不依賴于具體霹菊,使得對擴展開放,對修改關(guān)閉支竹。開閉原則與依賴倒置原則旋廷,里氏替換原則一樣,實際上都遵循一句話:面向接口編程礼搁。
優(yōu)點:
- 增加穩(wěn)定性
- 可擴展性高
接口隔離原則
客戶端應(yīng)該依賴于它不需要的接口:一個類對另一個類的依賴應(yīng)該建立在最小的接口上饶碘。根據(jù)接口隔離原則,當一個接口太大時馒吴,我們需要把它分離成一些更細小的接口扎运,使用該接口的客戶端僅需知道與之相關(guān)的方法即可瑟曲。
可能描述起來不是很好理解,我們還是以示例來加強理解吧豪治。
我們知道洞拨,在網(wǎng)絡(luò)框架中,網(wǎng)絡(luò)隊列中是會對請求進行排序的负拟。內(nèi)部使用 PriorityBlockingQueue 來維護網(wǎng)絡(luò)請求隊列烦衣,PriorityBlockingQueue 需要調(diào)用 Request 類的排序方法就可以了,其他的接口他根本不需要掩浙,即 PriorityBlockingQueue 只需要 compareTo 這個接口花吟,而這個 compareTo 方法就是我們所說的最小接口方法,而是 Java 中的 Comparable 接口涣脚,但我們這里是指為了學(xué)習(xí)示辈,至于哪里定義的無關(guān)緊要。
在元素排序時遣蚀,PriorityBlockingQueue 只需要知道元素是個 Comparable 對象即可矾麻,不需要知道這個對象是不是 Request 類以及這個類的其他接口。它只需要排序芭梯,因此险耀,只要知道它是實現(xiàn)了 Comparable 對象即可,Comparable 就是它的最小接口玖喘,也是通過 Comparable 隔離了 PriorityBlockingQueue 類對 Request 類的其他方法的可見性甩牺。
優(yōu)點:
- 降低耦合性
- 提升代碼的可讀性
- 隱藏實現(xiàn)的細節(jié)
迪米特原則
迪米特法則也成為最少知識原則(Least Knowledge Principle),雖然名字不同累奈,但是描述的是同一個原則贬派,一個對象應(yīng)該對其他對象有最少的了解。通俗地講澎媒,一個類應(yīng)該對自己需要耦合或者調(diào)用的類知道得最少搞乏,這有點類似于接口隔離原則中的最小接口的概念。類的內(nèi)部如何實現(xiàn)戒努、如何復(fù)雜都與調(diào)用者或者依賴者沒有關(guān)系请敦,調(diào)用者或者依賴者只需要知道它需要它需要的方法即可,其他的一概不關(guān)心储玫。類與類之間的關(guān)系越密切侍筛,耦合度越大,當一個類發(fā)生改變時撒穷,對另一個類的影響也越大匣椰。
迪米特原則還有一個英文解釋是:Only talk to your immedate friends(只與直接的朋友通信)。什么叫做直接的朋友呢端礼?每個對象都必然會與其他對象有耦合關(guān)系窝爪,兩個對象之間的耦合就成為朋友關(guān)系弛车,這種關(guān)系的類型有很多例如組合齐媒、聚合蒲每、依賴等。
例如在本例中喻括,網(wǎng)絡(luò)緩存中的 Response 緩存接口的設(shè)計邀杏。
/**
* 請求緩存接口
*
* @param <K> key 的類型
* @param <V> value 的類型
*/
public interface Cache<K, V> {
public V get(K key);
public void put(K key, V value);
public void remove(K key);
}
Cache 接口定義了緩存類型需要實現(xiàn)的最小接口,依賴緩存類的對象只需要知道這些接口即可唬血。例如望蜡,需要將 Http Response 緩存到內(nèi)存中,并且按照 LRU 的規(guī)則進行存儲拷恨。我們需要 LruCache 類實現(xiàn)這個功能脖律。代碼如下:
// 講請求結(jié)果緩存到內(nèi)存中
public class LruMemCache implements Cache<String, Response> {
/**
* Response LRU 緩存
*
* @param key
* @return
*/
private LruCache<String, Response> mResponseCache;
public LruMemCache() {
//計算可使用的最大內(nèi)存
final intmaxMemory=(int) (Runtime.getRuntime().maxMemory() / 1024);
//取八分之一的可用最大內(nèi)存為緩存
final intCacheSize=intmaxMemory / 8;
mResponseCache = new LruCache<String, Response>(intCacheSize) {
@Override
protected intSizeOf(String key, Response response) {
return response.rawData.length / 1024;
}
};
}
@Override
public Response get(String key) {
return mResponseCache.get(key);
}
@Override
public void put(String key, Response value) {
mResponseCache.get(key, value);
}
@Override
public void remove(String key) {
mResponseCache.remove(key);
}
}
在這里,網(wǎng)絡(luò)請求框架的直接朋友就是 Cache 或者 LruMemCache腕侄,間接朋友就是 LruCache 類小泉。它只需要跟 Cache 類交互即可,并不需要知道 LruCache 類的存在冕杠,即真正實現(xiàn)了緩存功能的是 LruCache微姊。這就是迪米特原則,盡量少地知道對象的信息分预,只與直接的朋友交互兢交。
優(yōu)點:
- 降低復(fù)雜度
- 降低耦合性
- 增加穩(wěn)定性
寫在后面:
面向?qū)ο蟮牧笤瓌t在開發(fā)過程中極為重要,他們給靈活笼痹、可擴展的軟件系統(tǒng)提供了更細粒度的指導(dǎo)原則配喳。如果能很好地將這些原則運用到項目中,再在一些合適的場景運用一些經(jīng)過驗證過設(shè)計模式凳干,那么開發(fā)出來的軟件在一定程度上能夠得到質(zhì)量保證晴裹。其實六大原則最終可以簡化為幾個關(guān)鍵字:抽象、單一職責(zé)纺座、最小化息拜。那么在實際開發(fā)中如何權(quán)衡,實踐這些原則净响,也是需要大家在工作過程中不斷地思考少欺、摸索、實踐馋贤。
本文終于要結(jié)束了赞别,讓我擦擦屏幕上的血(話說寫讀書筆記比自己寫文章累多了...),未來會繼續(xù)給大家總結(jié)設(shè)計模式配乓、重構(gòu)的手法仿滔、以及本例中非常實用的 網(wǎng)絡(luò)框架 的封裝惠毁,敬請期待~