在此以前,有一點須要你們知道,熟悉這些原則並非說你寫出的程序就必定靈活、清晰,只是爲你優秀的代碼之路鋪上了一層柵欄,在這些原則的指導下,你才能避免陷入一些常見的代碼泥沼,從而讓你寫出優秀的東西。編程
單一職責原則的英文名稱是 Single Responsibility Principle,簡稱是 SPR,簡單地說就是一個類只作一件事,這個設計原則備受爭議卻又極其重要。只要你想和別人爭執、慪氣或者是吵架,這個原則是屢試不爽的。由於單一職責的劃分界限並非如馬路上的行車道那麼清晰,不少時候都是須要我的經驗來界定。固然,最大的問題就是對職責的定義,什麼是類的職責,以及怎麼劃分類的職責。json
試想一下,若是你遵照了這個原則,那麼你的類就會劃分的很細,每一個類都有比較單一的職責,這不就是高內聚、低耦合麼!固然,如何界定類的職責就須要你的我的經驗了。設計模式
咱們定義一個網絡請求的類,來體現 SRP 的原則,來執行網絡請求的接口,代碼以下:緩存
public interface HttpStack { /** * 執行 Http 請求,而且返回一個 Response */ public Response performRequest(Request<?> request); }
從上述程序中能夠看到,HttpStack 只有一個 performRequest 函數,它的職責就是執行網絡請求而且返回一個 Response,它的職責很單一,這樣在須要修改執行網絡請求的相關代碼時,只須要修改實現 HttpStack 接口的類,而不會影響其餘類的代碼。若是某個類的職責包含有執行網絡請求、解析網絡請求、進行 gzip 壓縮、封裝請求參數等,那麼在你修改某處代碼時就必須謹慎,以避免修改的代碼影響了其它的功能。當你修改的代碼可以基本上不影響其餘功能。這就必定程度上保證了代碼的可維護性。注意,單一職責原則並非一個類只能有一個函數,而是說這個類中的函數所作的工做是高度相關的,也就是高內聚。 HttpStack 抽象了執行網絡請求的具體過程,接口簡單清晰,也便於擴展。服務器
優勢:網絡
面向對象的語言的三大特色是繼承、封裝、多態,里氏替換原則就是依賴於繼承、多態這兩大特性。里氏替換原則簡單來講就是全部引用基類、接口的地方必須能透明地使用其子類的對象。通俗點講,只要父類能出現的地方子類就能夠出現,並且替換爲子類也不會產生任何報錯或者異常,使用者可能根本就不須要知道是子類仍是父類。可是,反過來就不行了,有子類出現的地方,父類未必就能使用。架構
仍是以 HttpStack 爲例, HttpStack 來表示執行網絡請求這個抽象概念。在執行網絡請求時,只須要定義一個 HttpStack 對象,而後執行 performRequest 便可,至於 HttpStack 的具體實現由更高層的調用者指定。這部分代碼在 RequestQueue 類中,示例以下:框架
/** * @param coreNums 核心線程數 * @param httpStack http 執行器 */ protected RequestQueue(int coreNums, HttpStack httpStack) { mDispatcherNums = coreNums; mHttpStack = httpStack != null ? httpStack : HttpStackFactory.createHttpStack(); }
HttpStackFactory 類的 createHttpStack 函數負責根據 API 版本建立不一樣的 HttpStack,實現代碼以下:ide
/** * 根據 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 函數返回的是 HttpStack 的實現類 HttpClientStack 或 HttpUrlConnStack。這就是所謂的里氏替換原則,任何父類、父接口出現的地方子類均可以出現,這不就保證了可擴展性嗎!函數
任何實現 HttpStack 接口的類的對象均可以傳遞給 RequestQueue 實現網絡請求的功能,這樣執行網絡請求的方法就有不少種可能性,而不是隻有 HttpClient 和 HttpURLConnection。例如,用戶想使用 OkHttp 做爲新的網絡搜索執行引擎,那麼建立一個實現了 HttpStack 接口的 OkHttpStack 類,而後在該類的 performRequest 函數中執行網絡請求,最終將 OkHttpStack 對象注入 RequestQueue 便可。
細想一下,不少應用框架不就是這樣實現的嗎?框架定義一系列相關的邏輯骨架和抽象,使得用戶能夠將本身的實現注入到框架中,從而實現變化萬千的功能。
優勢:
缺點:
依賴倒置原則這個名字看起來有點很差理解,「依賴」還有「倒置」,這究竟是什麼意思?依賴倒置原則的幾個關鍵點以下。
在 Java 語言中,抽象就是指接口或者抽象類,二者都是不能直接被實例化的。細節就是實現類、實現接口或者繼承抽象類而產生的類,其特色就是能夠直接被實例化,也就是能夠加上一個關鍵字 new 產生一個對象。依賴倒置原則是 Java 語言中的表現就是:模塊間的依賴經過抽象發生,實現類之間不發生直接依賴的關係,其依賴關係是經過接口或者抽象類產生的。軟件先驅們老是喜歡將一些理論定義得很抽象,弄得不是那麼容易理解,其實就是一句話:面向接口編程,或者說是面向抽象編程,這裏的抽象是指抽象類或者是接口。面向接口編程是面向對象精髓之一。
採用依賴倒置原則能夠減小類之間的耦合性,提升系統的穩定性,下降並行開發引發的風險,提升代碼的可讀性和可維護性。
在前面咱們的例子中, RequestQueue 實現類依賴於 HttpStack 接口(抽象),而不依賴於 HttpClientStack 與 HttpUrlConnStack 實現類(細節),這就是依賴倒置原則的體現。若是 RequestQueue 直接依賴了 HttpClientStack ,那麼 HttpUrlConnStack 就不能傳遞給 RequestQueue 了。除非 HttpUrlConnStack 繼承自 HttpClientStack 。但這麼設計顯然不符合邏輯,他們兩個之間是同等級的「兄弟」關係,而不是父子的關係,所以,正確的設計就是依賴於 HttpStack 抽象,HttpStack 只是負責定義規範,而 HttpClientStack 和 HttpUrlConnStack 分別實現具體的功能。這樣一來也一樣保證了擴展性。
優勢:
開閉原則是 Java 世界裏最基礎的設計原則,它指導咱們如何創建一個穩定的、靈活的系統。開閉原則的定義是:一個軟件實體類,模塊和函數應該對擴展開放,對修改關閉。在軟件的生命週期內,由於變化、升級和維護等緣由,須要對軟件原有的代碼進行修改時,可能會給舊代碼引入錯誤。所以,當軟件須要變化時,咱們應該儘可能經過擴展的方式來實現變化,而不是經過修改已有的代碼來實現。
在軟件開發過程當中,永遠不變的就是變化。開閉原則是使咱們的軟件系統擁抱變化的核心原則之一。對擴展開放,對修改關閉這樣的高層次歸納,即在須要對軟件進行升級、變化時應該經過擴展的形式來實現,而非修改原有代碼。固然這只是一種比較理想的狀態,是經過擴展仍是經過修改舊代碼須要依據代碼自身來定。
在咱們封裝的網絡請求模塊中,開閉原則體現的比較好的就是 Request 類族的設計。咱們知道,在開發 C/S 應用時,服務器返回的數據多種多樣,有字符串類型、xml、Json 等。而解析服務器返回的 Response 的原始數據類型則是經過 Request 類來實現的,這樣就使得 Request 類對於服務器返回的數據格式有良好的擴展性,即 Request 的可變性太大。
例如,返回的數據格式是 Json,那麼使用 JsonRequest 請求來獲取數據,它會將結果轉成 JsonObject 對象,咱們看看 JsonRequest 的核心實現:
// 返回的數據格式爲 Json 的請求,Json 對應的對象類型爲 JSONObject public class JsonRequest extends Request<JSONObject> { public JsonRequest(HttpMethod method, String url, RequestListener<JSONObject> listener) { super(method, url, listener); } // 將 Response 的結果轉化爲 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 經過實現 Request 抽象類的 parseResponse 解析服務器返回的結果,這裏將結果轉換爲 JSONObject,而且封裝到 Response 類中。
例如,咱們的網絡框架中,添加對圖片請求的支持,即要實現相似 ImageLoader 的功能。這個時候個人請求返回的是 Bitmap 圖片,所以,我須要在該類型的 Request 中獲得的結果是 Request,但支持一種新的數據格式不能經過修改源碼的形式,這樣可能會爲舊代碼引入錯誤,可是,你又必須實現功能擴展。這就是開閉原則的定義:對擴展開放,對修改關閉。咱們看看應該如何作:
public class ImageRequest extends Request<Bitmap> { public ImageRequest(HttpMethod method, String url, RequestListener<Bitmap> listener) { super(method, url, listener); } // 將 Response 的結果轉化爲 Bitmap @Override public Bitmap parseResponse(Response response) { return BitmapFactory.decodeByteArray(response.rawData, 0, response.rawData.length); } }
ImageRequest 類的 parseResponse 函數中將 Response 中的原始數據轉換成爲 Bitmap 便可,當咱們須要添加其餘數據格式的時候,只須要繼承自 Request 類,而且在 parseResponse 方法中將數據轉換爲具體的形式便可。這樣經過擴展的形式來應對軟件的變化或者說用戶需求的多樣性,既避免了破壞原有系統,又保證了軟件系統的可維護性。依賴於抽象,而不依賴於具體,使得對擴展開放,對修改關閉。開閉原則與依賴倒置原則,里氏替換原則同樣,實際上都遵循一句話:面向接口編程。
優勢:
客戶端應該依賴於它不須要的接口:一個類對另外一個類的依賴應該創建在最小的接口上。根據接口隔離原則,當一個接口太大時,咱們須要把它分離成一些更細小的接口,使用該接口的客戶端僅需知道與之相關的方法便可。
可能描述起來不是很好理解,咱們仍是以示例來增強理解吧。
咱們知道,在網絡框架中,網絡隊列中是會對請求進行排序的。內部使用 PriorityBlockingQueue 來維護網絡請求隊列,PriorityBlockingQueue 須要調用 Request 類的排序方法就能夠了,其餘的接口他根本不須要,即 PriorityBlockingQueue 只須要 compareTo 這個接口,而這個 compareTo 接口就是咱們所說的最小接口,而是 Java 中的 Comparable 接口,但咱們這裏是指爲了學習,至於哪裏定義的可有可無。
在元素排序時,PriorityBlockingQueue 只須要知道元素是個 Comparable 對象便可,不須要知道這個對象是否是 Request 類以及這個類的其餘接口。它只須要排序,所以,只要知道它是實現了 Comparable 對象便可,Comparable 就是它的最小接口,也是經過 Comparable 隔離了 PriorityBlockingQueue 類對 Request 類的其餘方法的可見性。
優勢:
迪米特法則也成爲最少知識原則(Least Knowledge Principle),雖然名字不一樣,可是描述的是同一個原則,一個對象應該對其餘對象有最少的瞭解。通俗地講,一個類應該對本身須要耦合或者調用的類知道得最少,這有點相似於接口隔離原則中的最小接口的概念。類的內部如何實現、如何複雜都與調用者或者依賴者沒有關係,調用者或者依賴者只須要知道它須要它須要的方法便可,其餘的一律不關心。類與類之間的關係越密切,耦合度越大,當一個類發生改變時,對另外一個類的影響也越大。
迪米特原則還有一個英文解釋是:Only talk to your immedate friends(只與直接的朋友通訊)。什麼叫作直接的朋友呢?每一個對象都必然會與其餘對象有耦合關係,兩個對象之間的耦合就成爲朋友關係,這種關係的類型有不少例如組合、聚合、依賴等。
例如在本例中,網絡緩存中的 Response 緩存接口的設計。
/** * 請求緩存接口 * * @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 接口定義了緩存類型須要實現的最小接口,依賴緩存類的對象只須要知道該接口便可。例如,須要將 Http Response 緩存到內存中,而且按照 LRU 的規則進行存儲。咱們須要 LruCache 類實現這個功能。代碼以下:
// 將請求結果緩存到內存中 public class LruMemCache implements Cache<String, Response> { /** * Response LRU 緩存 * * @param key * @return */ private LruCache<String, Response> mResponseCache; public LruMemCache() { //計算可以使用的最大內存 final int maxMemory=(int) (Runtime.getRuntime().maxMemory() / 1024); //取八分之一的可用最大內存爲緩存 final int CacheSize = int maxMemory / 8; mResponseCache = new LruCache<String, Response>(int CacheSize) { @Override protected int SizeOf(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); } }
在這裏,網絡請求框架的直接朋友就是 Cache 或者 LruMemCache,間接朋友就是 LruCache 類。它只須要跟 Cache 類交互便可,並不須要知道 LruCache 類的存在,即真正實現了緩存功能的是 LruCache。這就是迪米特原則,儘可能少地知道對象的信息,只與直接的朋友交互。
優勢:
在軟件工程中,設計模式是對軟件設計中廣泛存在、反覆出現的各類問題所提出的通用解決方案。這個術語是由 Erich Gamma 等人在1990 年從 建築設計 領域引入到軟件工程領域,今後設計模式在面向對象設計領域逐漸被重視起來。
設計模式並不直接用來完成代碼的編寫,而是描述在各類狀況下要如何解決軟件設計問題。面向對象設計模式一般以類或對象來描述其中的關係和相互做用,他們的相互做用可以使軟件系統具備高內聚、低耦合的特性,而且使軟件可以應對變化。
模式的4個要素
模式名稱
模式名稱用一兩個詞來描述模式的問題、解決防範和效果。基於一個模式詞彙表,同行、同事之間就能夠經過它們進行交流,文檔中也能夠經過模式名來表明一個設計。模式名能夠幫助咱們思考,便於咱們與其餘人交流設計思想以及設計結果。
問題
描述了應該在什麼狀況使用設計模式。它解釋了設計問題和問題存在的來龍去脈,它可能描述了特定的設計問題,例如,某個設計不具有良好的可擴展性等,也可能描述了致使不靈活設計的類或者對象結構。
解決方案
描述了設計的組成成分,它們之間的相互關係以及各自的職責和協做方式。由於模式就像一個模板,可應用於多種不一樣的場合,因此解決方案並不描述一個具體的設計或者實現,而是提供設計問題的抽象描述和怎樣用一個具備通常意義的類或者對象組合來解決這個問題。
效果
描述了模式應用的效果及使用模式應權衡的問題。儘管咱們描述設計決策時,並不總提到模式效果,但它們對於評價設計選擇和理解使用模式的代價及好處具備重要意義。軟件效果大多關注對時間和空間的衡量,它們也表述了語言和實現問題。由於複用是面向對象的設計要素之一。因此模式效果包括對它系統的靈活性、擴充性或可移植性的影響,顯式地列出這些效果對理解和評價這些模式頗有幫助。
設計模式爲反覆出現的局部軟件設計問題指出了通用的解決方案,在很大程度上促進了面向對象軟件工程的發展。它將這些常見的設計問題一一總結,將大師們的經驗、教訓、設計經驗分享給了全部人,使得即使是剛剛入行的工程師,也可以設計出可擴展、靈活的軟件系統,大大提高了軟件質量。關於設計模式領域的書籍你們能夠參考《設計模式之禪》、《Android 源碼設計模式解析與實戰》。
當你掌握一些設計模式或者方法以後,比較容易出現的問題就是過分設計。有的人甚至在一個應用中必定要將 23 種常見的設計模式運用上,這就本末倒置了。設計模式的四大要素中就明確指出,模式的運用應該根據軟件系統所面臨的問題來決定是否須要使用現有的設計。也就是說,再出現問題或者你預計會出現那樣的問題時,才推薦使用特定的設計模式,而不是將各類設計模式套進你的軟件中。
無論在設計、實現、測試之劍有多少時間都應該避免過分設計,它會打破你的反饋迴路,使你的設計得不到反饋,從而慢慢陷入危險中。因此你只須要保持簡單的設計,這樣就有時間來測試該設計是否真的可行,而後做出最後的決策。
當設計一款軟件時,從總體高度上設定一種架構模式,肯定應用的總體架構,而後再分析一些重要米快的設計思路,而且保證他們的簡單性、清晰性,若是有時間可使用 Java 代碼模擬一個簡單的原型,確保設計是可行的,最後就能夠付諸行動了。切實不要過分的追求設計,適當就好,當咱們發現或者預計到將要出現問題時,在判斷是否須要運用設計模式。
反模式是一種文字記錄形式,描述了對某個問題必然產生的消極後果的常看法決方案。因爲管理人員或者開發人員不知道更好的解決方案,缺少決定特定問題的經驗或知識,或者說不適合的條件下套用了某個設計模式,這些都會形成反模式。與設計模式相似,反模式描述了一個通常的形式,主要緣由、典型症狀。後果,以及最後如何經過重構解決問題。
反模式是把通常狀況映射到一類特定解決方案的有效方法。反模式的通常形式爲它所針對的哪類問題提供了一個易於辨識的模板。此外,它還清楚地說明了與該問題相關聯的症狀以及致使這一問題的內在緣由:把特定設計模式應用於不正確的環境。
反模式爲識別軟件行業反覆出現的問題提供了實際經驗,併爲大多數常見的問題提供了詳細的解決方案。反模式對業界常見的問題進行總結,而且告訴你如何識別這些問題以及如何解決。它有效的說明了能夠在不一樣的層次上採起的措施,以便改善應用開發過程,軟件系統和對軟件項目的有效管理。
總的來講,設計模式總結了在特定問題下正確的解決方案,而反模式則是告訴你在特定問題上的錯誤解決方案和他們的緣由、解決方案,經過最終的解決方案,它可以將腐化的軟件系統拉回正軌。
靈活的軟件設計須要知識和經驗與思考,好的設計一般是經歷了時間的洗禮慢慢演化而來,工程師的成長也是同樣。所以,掌握必要的面向對象、設計模式、反模式等知識,而且這工做中不斷實踐、思考,將使你的軟件設計之路走得更加從容、順暢。
寫在後面:
面向對象的六大原則在開發過程當中極爲重要,他們給靈活、可擴展的軟件系統提供了更細粒度的指導原則。若是能很好地將這些原則運用到項目中,再在一些合適的場景運用一些通過驗證過設計模式,那麼開發出來的軟件在必定程度上可以獲得質量保證。其實六大原則最終能夠簡化爲幾個關鍵字:抽象、單一職責、最小化。那麼在實際開發中如何權衡,實踐這些原則,也是須要你們在工做過程當中不斷地思考、摸索、實踐。