原文連接:https://www.codeproject.com/articles/60845/the-s-o-l-i-d-object-oriented-programming-oop-prinjava
在工做初期,咱們可能會有這樣的感受,本身的代碼接口設計混亂、代碼耦合較爲嚴重、一個類的代碼過多等,當本身回過頭再看這些代碼時可能都會感慨怎麼寫成那樣。再看那些知名的開源庫,他們大多有着整齊的代碼、清晰簡單的接口、職責單一的類,這個時候咱們一般會捶胸頓足而感嘆:何時老夫能寫出這樣的代碼!
其實在作開發這些年中,我漸漸的感受到,其實國內一些初、中級工程師寫的東西不規範或者說不夠清晰的緣由是缺少一些指導原則。他們手中揮舞着面向對象的大旗,寫出來的東西卻充斥着面向過程的氣味。也許是他們不知道有這些原則,也許是他們知道可是不能很好運用到實際代碼中,亦或是他們沒有在實戰中體會到這些原則可以帶來的優勢,以致於他們對這些原則並無足夠的重視。編程
在此以前,有一點須要你們知道,熟悉這些原則並非說你寫出的程序就必定靈活、清晰,只是爲你的優秀代碼之路鋪上了一層柵欄,在這些原則的指導下你才能避免陷入一些常見的代碼泥沼,從而讓你專心寫出優秀的東西。
下面咱們就以Android網絡框架SimpleNet爲例來學習這六大面向對象的基本原則,體會這些原則在開發過程當中帶來的強大能量。json
單一職責原則的英文名稱是Single Responsibility Principle,簡稱是SRP,簡單地說就是一個類只作一件事。這個設計原則備受爭議卻又極其重要。只要你想和別人爭執、慪氣或者是吵架,這個原則是屢試不爽的。由於單一職責的劃分界限並非如馬路上的行車道那麼清晰,不少時候都是須要靠我的經驗來界定。固然,最大的問題就是對職責的定義,什麼是類的職責,以及怎麼劃分類的職責。
試想一下,若是你遵照了這個原則,那麼你的類就會劃分得很細,每一個類都有比較單一的職責,這不就是高內聚、低耦合麼!固然,如何界定類的職責這須要你的我的經驗了。
在SimpleNet中,我以爲很可以體現SRP原則的就是HttpStack這個類族了。HttpStack定義了一個執行網絡請求的接口,代碼以下:設計模式
public interface HttpStack { /** * 執行Http請求,而且返回一個Response */ public Response performRequest(Request<?> request); }
從上述程序中能夠看到,HttpStack只有一個performRequest函數,它的職責就是執行網絡請求而且返回一個Response。它的職責很單一,這樣在須要修改執行網絡請求的相關代碼時,只須要修改實現HttpStack接口的類,而不會影響其餘的類的代碼。若是某個類的職責包含有執行網絡請求、解析網絡請求、進行gzip壓縮、封裝請求參數等,那麼在你修改某處代碼時就必須謹慎,以避免修改的代碼影響了其餘的功能。當你修改的代碼可以基本上不影響其餘的功能。這就在必定程度上保證了代碼的可維護性。注意,單一職責原則並非說一個類只有一個函數,而是說這個類中的函數所作的工做是高度相關的,也就是高內聚。HttpStack抽象了執行網絡請求的具體過程,接口簡單清晰,也便於擴展。緩存
優勢服務器
(1)類的複雜性下降,實現什麼職責都有清晰明確的定義。
(2)可讀性提升,複雜性下降,那固然可讀性提升了。
(3)可維護性提升,可讀性提升,那固然更容易維護了。
(4)變動引發的風險下降,變動是必不可少的,若是接口的單一職責作得好,一個接口修改只對相應的實現類有影響,對其餘的接口無影響,這對系統的擴展性、維護性都有很是大的幫助。網絡
面向對象的語言的三大特色是繼承、封裝、多態,里氏替換原則就是依賴於繼承、多態這兩大特性。里氏替換原則簡單來講就是全部引用基類、接口的地方必須能透明地使用其子類的對象。通俗點講,只要父類能出現的地方子類就能夠出現,並且替換爲子類也不會產生任何錯誤或異常,使用者可能根本就不須要知道是父類仍是子類。可是,反過來就不行了,有子類出現的地方,父類未必就能適應。
仍是以HttpStack爲例,SimpleNet定義了HttpStack來表示執行網絡請求這個抽象概念。在執行網絡請求時,只須要定義一個HttpStack對象,而後調用performRequest便可,至於HttpStack的具體實現由更高層的調用者指定。這部分代碼在RequestQueue類中,示例以下:框架
/** * @paramcoreNums線程核心數 * @paramhttpStack http執行器 */ protected RequestQueue(intcoreNums, HttpStackhttpStack) { mDispatcherNums = coreNums; mHttpStack = httpStack != null ? httpStack : HttpStackFactory.createHttpStack(); }
HttpStackFactory類的createHttpStack函數負責根據API版本建立不一樣的HttpStack,實現代碼以下:ide
// 根據API版本選擇HttpClient或者HttpURLConnection public final class HttpStackFactory { // API 9 private static final int GINGERBREAD_SDK_NUM = 9; /** * 根據SDK版本號來建立不一樣的Http執行器,即SDK 9以前使用HttpClient,以後則使用HttlUrlConnection * @return */ public static HttpStackcreateHttpStack() { intruntimeSDKApi = Build.VERSION.SDK_INT; if (runtimeSDKApi>= GINGERBREAD_SDK_NUM) { return new HttpUrlConnStack(); } return new HttpClientStack(); } }
上述代碼中,RequestQueue類中依賴的是HttpStack接口,而經過HttpStackFactory的createHttpStack函數返回的是HttpStack的實現類HttpClientStack或HttlUrlConnStack。這就是所謂的里氏替換原則,任何父類、父接口出現的地方子類均可以出現,這不就保證了可擴展性嗎!
任何實現HttpStack接口的類的對象均可以傳遞給RequestQueue實現網絡請求的功能,這樣SimpleNet執行網絡請求的方法就有不少種可能性,而不是隻有HttpClient和HttpURLConnection。例如,用戶想使用OkHttp做爲SimpleNet的執行引擎,那麼建立一個實現了HttpStack接口的OkHttpStack類,而後在該類的performRequest函數中執行網絡請求,最終將OkHttpStack對象注入RequestQueue便可。
細想一下,不少應用框架不就是這樣實現嗎?框架定義一系列相關的邏輯骨架與抽象,使得用戶能夠將本身的實現注入到框架中,從而實現變化萬千的功能。函數
優勢
(1)代碼共享,減小建立類的工做量,每一個子類都擁有父類的方法和屬性。
(2)提升代碼的重用性。
(3)提升代碼的可擴展性,實現父類的方法就能夠「隨心所欲」了,不少開源框架的擴展接口都是經過繼承父類來完成的。
(4)提升產品或項目的開放性。
缺點
(1)繼承是侵入性的。只要繼承,就必須擁有父類的全部屬性和方法。
(2)下降代碼的靈活性。子類必須擁有父類的屬性和方法,讓子類自由的世界中多了些約束。
(3)加強了耦合性。當父類的常量、變量和方法被修改時,必須要考慮子類的修改,並且在缺少規範的環境下,這種修改可能帶來很是糟糕的結果——大片的代碼須要重構。
依賴倒置原則這個名字看着有點很差理解,「依賴」還要「倒置」,這究竟是什麼意思?依賴倒置原則的幾個關鍵點以下:
(1)高層模塊不該該依賴低層模塊,二者都應該依賴其抽象。
(2)抽象不該該依賴細節。
(3)細節應該依賴抽象。
在Java語言中,抽象就是指接口或抽象類,二者都是不能直接被實例化的。細節就是實現類、實現接口或繼承抽象類而產生的類就是細節,其特色就是能夠直接被實例化,也就是能夠加上一個關鍵字 new 產生一個對象。依賴倒置原則在 Java 語言中的表現就是:模塊間的依賴經過抽象發生,實現類之間不發生直接的依賴關係,其依賴關係是經過接口或抽象類產生的。軟件先驅們老是喜歡將一些理論定義得很抽象,弄得不是那麼容易理解,其實就是一句話:面向接口編程,或者說是面向抽象編程,這裏的抽象指的是接口或者抽象類。面向接口編程是面向對象精髓之一。
採用依賴倒置原則能夠減小類間的耦合性,提升系統的穩定性,下降並行開發引發的風險,提升代碼的可讀性和可維護性。
優勢
(1)可擴展性好。
(2)耦合度低。
開閉原則是Java世界裏最基礎的設計原則,它指導咱們如何創建一個穩定的、靈活的系統。開閉原則的定義是:一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉。在軟件的生命週期內,由於變化、升級和維護等緣由須要對軟件原有代碼進行修改時,
可能會給舊代碼引入錯誤。所以,當軟件須要變化時,咱們應該儘可能經過擴展的方式來實現變化,而不是經過修改已有的代碼來實現。
在軟件開發過程當中,永遠不變的就是變化。開閉原則是使咱們的軟件系統擁抱變化的核心原則之一。對擴展開放,對修改關閉這樣的高層次的歸納,即在須要對軟件進行升級、變化時應該經過擴展的形式來實現,而非修改原有代碼。固然這只是一種比較理想的狀態,是經過擴展仍是經過修改舊代碼須要根據代碼自身來定。
在SimpleNet中,開閉原則體現得比較好的是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 JSONObjectparseResponse(Response response) { String jsonString = new String(response.getRawData()); try { return new JSONObject(jsonString); } catch (JSONException e) { e.printStackTrace(); } return null; } }
JsonRequest經過實現Request抽象類的parseResponse解析服務器返回的結果,這裏將結果轉換爲JSONObject,而且封裝到Response類中。
例如,SimpleNet添加對圖片請求的支持,即要實現相似ImageLoader的功能。這個時候個人請求返回的數據是Bitmap圖片。所以,我須要在該類型的Request中獲得的結果是Request,但支持一種新的數據格式不能經過修改源碼的形式,這樣可能會爲舊代碼引入錯誤,可是,你又必須實現功能擴展。這就是開閉原則的定義:對擴展開放,對修改關閉。咱們看看SimpleNet是如何作的:
public class ImageRequest extends Request<Bitmap> { public ImageRequest(HttpMethod method, String url, RequestListener<Bitmap> listener) { super(method, url, listener); } // 將數據解析爲Bitmap @Override public Bitmap parseResponse(Response response) { return BitmapFactory.decodeByteArray(response.rawData, 0, response.rawData.length); } }
ImageRequest類的parseResponse函數中將Response中的原始數據轉換爲Bitmap便可。當咱們須要添加其餘數據格式時,只須要繼承自Request類,而且在parseResponse方法中將數據轉換爲具體的形式便可。這樣經過擴展的形式來應對軟件的變化或者說用戶需求的多樣性,既避免了破壞原有系統,又保證了軟件系統的可擴展性。依賴於抽象,而不依賴於具體,使得對擴展開放,對修改關閉。開閉原則與依賴倒置原則、里氏替換原則同樣,實際上最終都遵循一句話:面向接口編程。
優勢
(1)增長穩定性。
(2)可擴展性高。
客戶端不該該依賴它不須要的接口;一個類對另外一個類的依賴應該創建在最小的接口上。根據接口隔離原則,當一個接口太大時,咱們須要將它分割成一些更細小的接口,使用該接口的客戶端僅需知道與之相關的方法便可。
可能描述起來不是很好理解,咱們仍是以示例來增強理解吧。
咱們知道,在SimpleNet的網絡隊列中是會對請求進行排序的。SimpleNet內部使用PriorityBlockingQueue來維護網絡請求隊列,PriorityBlockingQueue須要調用Request類的compareTo函數來進行排序。試想一下,PriorityBlockingQueue其實只須要調用Request類的排序方法就能夠了,其餘的接口它根本不須要,即PriorityBlockingQueue只須要compareTo這個接口,而這個compareTo方法就是咱們上述所說的最小接口。固然,compareTo這個方法並非SimpleNet自己定義的接口方法,而是Java中的Comparable接口,但咱們這裏只是爲了學習,至於哪裏定義的可有可無:
public abstract class Request<T> implements Comparable<Request<T>> { /** * 排序方法,PriorityBlockingQueue只須要調用元素的compareTo便可進行排序 */ @Override public intcompareTo(Request<T> another) { Priority myPriority = this.getPriority(); Priority anotherPriority = another.getPriority(); // 若是優先級相等,那麼按照添加到隊列的序列號順序來執行 return myPriority.equals(anotherPriority) ?this.getSerialNumber() - another.getSerialNumber() : myPriority.ordinal() - anotherPriority.ordinal(); } // 代碼省略 }
PriorityBlockingQueue類相關代碼 :
public class PriorityBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable { // 代碼省略 // 添加元素時進行排序 public boolean offer(E e) { if (e == null) throw new NullPointerException(); final ReentrantLock lock = this.lock; lock.lock(); int n, cap; Object[] array; while ((n = size) >= (cap = (array = queue).length)) tryGrow(array, cap); try { Comparator<? super E>cmp = comparator; // 沒有設置Comparator,則使用元素自己的compareTo方法進行排序 if (cmp == null) siftUpComparable(n, e, array); else siftUpUsingComparator(n, e, array, cmp); size = n + 1; notEmpty.signal(); } finally { lock.unlock(); } return true; } private static <T> void siftUpComparable(int k, T x, Object[] array) { Comparable<? super T> key = (Comparable<? super T>) x; while (k > 0) { int parent = (k - 1) >>> 1; Object e = array[parent]; // 調用元素的compareTo方法進行排序 if (key.compareTo((T) e) >= 0) break; array[k] = e; k = parent; } array[k] = key; } }
從PriorityBlockingQueue的代碼可知,在元素排序時,PriorityBlockingQueue只須要知道元素是個Comparable對象便可,不須要知道這個對象是否是Request類以及這個類的其餘接口。它只須要排序,所以,只要知道它是實現了Comparable接口的對象便可,Comparable就是它的最小接口,也是經過Comparable隔離了PriorityBlockingQueue類對Request類的其餘方法的可見性。
優勢
(1)下降耦合性。
(2)提高代碼的可讀性。
(3)隱藏實現細節。
迪米特法則也稱爲最少知識原則(Least Knowledge Principle),雖然名字不一樣,但描述的是同一個原則:一個對象應該對其餘對象有最少的瞭解。通俗地講,一個類應該對本身須要耦合或調用的類知道得最少,這有點相似接口隔離原則中的最小接口的概念。類的內部如何實現、如何複雜都與調用者或者依賴者沒有關係,調用者或者依賴者只須要知道它須要的方法便可,其餘的一律不關心。類與類之間的關係越密切,耦合度越大,當一個類發生改變時,對另外一個類的影響也越大。
迪米特法則還有一個英文解釋是:Only talk to your immedate friends(只與直接的朋友通訊)。什麼叫作直接的朋友呢?每一個對象都必然會與其餘對象有耦合關係,兩個對象之間的耦合就成爲朋友關係,這種關係的類型有不少,例如組合、聚合、依賴等。
例如,SimpleNet中的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> { /** * Reponse LRU緩存 */ private LruCache<String, Response>mResponseCache; public LruMemCache() { // 計算可以使用的最大內存 final intmaxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // 取八分之一的可用內存做爲緩存 final intcacheSize = maxMemory / 8; mResponseCache = new LruCache<String, Response>(cacheSize) { @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 response) { mResponseCache.put(key, response); } @Override public void remove(String key) { mResponseCache.remove(key); } }
在這裏,SimpleNet的直接朋友就是Cache或者LruMemCahce,間接朋友就是LruCache類。SimpleNet只須要直接和Cache類交互便可,並不須要知道LruCache的對象的存在,即真正實現緩存功能的對象是LruCache。這就是迪米特原則,儘可能少地知道對象的信息,只與直接的朋友交互。
優勢
(1)下降複雜度。
(2)下降耦合度。
(3)增長穩定性。
面向對象六大原則在開發過程當中極爲重要,它們給靈活、可擴展的軟件系統提供了更細粒度的指導原則。若是可以很好地將這些原則運用到項目中,再在一些合適的場景運用一些通過驗證過的設計模式,那麼開發出來的軟件在必定程度上可以獲得質量保證。其實這六大原則最終能夠簡化爲幾個關鍵詞:抽象、單一職責、最小化。那麼在實際開發過程當中如何權衡、實踐這些原則,也是須要你們在工做中不斷地思考、摸索。
設計模式(Design pattern)是一套被反覆使用、多數人知曉的、通過分類編目的、代碼設計經驗的總結。這個術語是由Erich Gamma等人在1990年從建築設計領域引入到軟件工程領域,今後設計模式在面向對象設計領域逐漸被重視起來。
設計模式並不直接用來完成代碼的編寫,而是描述在各類狀況下要如何解決軟件設計問題。面向對象設計模式一般以類或對象來描述其中的關係和相互做用,它們的相互做用可以使軟件系統具備高內聚、低耦合的特性,而且使軟件可以應對變化。