面向對象六大原則和設計模式
本文的內容來自 Android 進階書籍《從小工到專家》,六大原則和設計模式章節。讀過之后覺得非常受用,所以為大家整理出來,之后也會帶來 設計模式 和 單元測試 以及 代碼重構 的介紹,希望我們能早日從碼農變成一個開發工程師。話不多說,下面帶來書中原汁原味的內容。
在工作的初期,我們可能會經常有這樣的感受,自己的代碼接口設計混亂、代碼耦合較為嚴重、一個類的代碼過多等等,當自己回頭再看這些代碼時可能會感慨,怎么能寫成這個鳥樣。再看那些知名的開源庫,它們大多有整潔的代碼、清晰簡單的接口、職責單一的類,這個時候我們會通常會捶胸頓足而感慨:什么時候老夫才能寫出這樣的代碼!
在做開發的這些年中,我漸漸的感覺到,其實國內的一些初、中級工程師寫的東西不規范或者說不夠清晰的原因是缺乏一些指導規則。他們手中揮舞著面向對象的大旗,寫出來的東西卻充斥著面向過程的氣味。也許是他們不知道有這些規則,也許是他們知道但是不能很好的運用到實際的代碼中,亦或是他們沒有在實戰項目中體會到這些原則能夠帶來的優點,以至于他們對這些原則并沒有足夠的重視。
本章沒有詳細介紹 OOP 六大原則、設計模式、反模式等內容,只是對它們做了一些簡單的介紹。并不是因為它們不重要,而是由于它們太重要,因此我們必須閱讀更詳盡的書籍來涉入這些知識,設計模式可以參考《設計模式之禪》、《設計模式:可復用面向對象軟件的基礎》以及《Android源碼設計模式解析與實戰》,反模式的權威書籍則為《反模式:危機中軟件、架構和項目的重構》一書。
(打字好累…)
面向對象六大原則
在此之前,有一點需要大家知道,熟悉這些原則并不是說你寫出的程序就一定靈活、清晰,只是為你優秀的代碼之路鋪上了一層柵欄,在這些原則的指導下,你才能避免陷入一些常見的代碼泥沼,從而讓你寫出優秀的東西。
單一職責原則
單一職責原則的英文名稱是 Single Responsibility Principle,簡稱是 SPR,簡單地說就是一個類只做一件事,這個設計原則備受爭議卻又極其重要。只要你想和別人爭執、慪氣或者是吵架,這個原則是屢試不爽的。因為單一職責的劃分界限并不是如馬路上的行車道那么清晰,很多時候都是需要個人經驗來界定。當然,最大的問題就是對職責的定義,什么是類的職責,以及怎么劃分類的職責。
試想一下,如果你遵守了這個原則,那么你的類就會劃分的很細,每個類都有比較單一的職責,這不就是高內聚、低耦合么!當然,如何界定類的職責就需要你的個人經驗了。
我們定義一個網絡請求的類,來體現 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,實現代碼如下:
/** * 根據 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 代碼模擬一個簡單的原型,確保設計是可行的,最后就可以付諸行動了。切實不要過度的追求設計,適當就好,當我們發現或者預計到將要出現問題時,在判斷是否需要運用設計模式。
反模式
反模式是一種文字記錄形式,描述了對某個問題必然產生的消極后果的常見解決方案。由于管理人員或者開發人員不知道更好的解決方案,缺乏決定特定問題的經驗或知識,或者說不適合的條件下套用了某個設計模式,這些都會造成反模式。與設計模式類似,反模式描述了一個一般的形式,主要原因、典型癥狀。后果,以及最后如何通過重構解決問題。
反模式是把一般情況映射到一類特定解決方案的有效方法。反模式的一般形式為它所針對的哪類問題提供了一個易于辨識的模板。此外,它還清楚地說明了與該問題相關聯的癥狀以及導致這一問題的內在原因:把特定設計模式應用于不正確的環境。
反模式為識別軟件行業反復出現的問題提供了實際經驗,并為大多數常見的問題提供了詳細的解決方案。反模式對業界常見的問題進行總結,并且告訴你如何識別這些問題以及如何解決。它有效的說明了可以在不同的層次上采取的措施,以便改善應用開發過程,軟件系統和對軟件項目的有效管理。
總的來說,設計模式總結了在特定問題下正確的解決方案,而反模式則是告訴你在特定問題上的錯誤解決方案和他們的原因、解決方案,通過最終的解決方案,它能夠將腐化的軟件系統拉回正軌。
總結
靈活的軟件設計需要知識和經驗與思考,好的設計通常是經歷了時間的洗禮慢慢演化而來,工程師的成長也是一樣。因此,掌握必要的面向對象、設計模式、反模式等知識,并且這工作中不斷實踐、思考,將使你的軟件設計之路走得更加從容、順暢。
寫在后面:
面向對象的六大原則在開發過程中極為重要,他們給靈活、可擴展的軟件系統提供了更細粒度的指導原則。如果能很好地將這些原則運用到項目中,再在一些合適的場景運用一些經過驗證過設計模式,那么開發出來的軟件在一定程度上能夠得到質量保證。其實六大原則最終可以簡化為幾個關鍵字:抽象、單一職責、最小化。那么在實際開發中如何權衡,實踐這些原則,也是需要大家在工作過程中不斷地思考、摸索、實踐。
來自:https://itsmelo.github.io/2016/11/20/面向對象六大原則和設計模式/