設計模式詳解(總綱) 原文地址:https://www.cnblogs.com/zuoxiaolong/p/pattern1.html 作者:zuoxiaolong8810(左瀟龍),轉載請註明出處,特別說明:本博文來自博主原博客,為保證新博客中博文的完整性,特複製到此留存,如需轉載請註明新博客地址 ...
設計模式詳解(總綱)
原文地址:https://www.cnblogs.com/zuoxiaolong/p/pattern1.html作者:zuoxiaolong8810(左瀟龍),轉載請註明出處,特別說明:本博文來自博主原博客,為保證新博客中博文的完整性,特複製到此留存,如需轉載請註明新博客地址即可。
最近一直在學習設計模式相關的知識,還是老規矩,和各位一起學習,一起探討,本系列所發表所有內容僅代表個人觀點。
《簡介》
說到設計模式,當初第一次聽到時,第一反應就是很深奧,完全理解不了這個概念到底是什麼意思,下麵我先從網上摘錄一份定義。
設計模式(Designpattern)是一套被反覆使用、多數人知曉的、經過分類編目的、代碼設計經驗的總結。
上面是百度當中的解釋,來解釋一下這句簡單的話的含義,幾個關鍵詞。
反覆使用:這個不用過多解釋,設計模式被使用太多了,上個系列spring源碼當中就出現了很多模式,記憶中比較深刻的有模板模式,代理模式,單例模式,工廠模式等等。
多數人知曉:這個就不需要過多解釋了。
分類編目:就是說可以找到一些特征去劃分這些設計模式,從而進行分類。
代碼設計經驗:這句很重要,設計經驗的總結,也就是說設計模式,是為了指導設計而從經驗中總結出來的套路。
還有一種說法是說,設計模式是可以解決特定場景的問題的一系列方法,其實我覺得這個解釋更貼切一點。
《為何學習設計模式》
上面簡單的介紹,是讓各位首先搞清楚設計模式是什麼,下麵我們來說說為什麼要學習設計模式,學習總要有個驅動力。
有過工作經驗的人都知道,特別是那些在維護一個項目的人更是體會的貼切,像我就是其中一個,有的時候,一個很簡單的需求,或者說,本來應該是很快就可以實現的需求,但是由於系統當初設計的時候沒有考慮這些需求的變化,或者隨著需求的累加,系統越來越臃腫,導致隨便修改一處都可能造成不可預料的後果,或者是我本來可以修改下配置文件或者改一處代碼就可以解決的事情,結果需要修改N處代碼才可以達到我的目的。
以上都是非常可怕的後果,這些我已經深深體會過了。
《設計模式的好處及註意點》
設計模式可以幫助我們改善系統的設計,增強系統的健壯性、可擴展性,為以後鋪平道路。
但是,這些是我當初第一次接觸設計模式時的感受,現在我並不這麼認為,設計模式可以改善系統的設計是沒錯,但是過多的模式也會系統變的複雜。所以當我們第一次設計一個系統時,請將你確定的變化點處理掉,不確定的變化點千萬不要假設它存在,如果你曾經這麼做過,那麼請改變你的思維,讓這些虛無的變化點在你腦子中徹底消失。
因為我們完全可以使用另外一種手法來容納我們的變化點,那就是重構,不過這是我們在討論過設計模式之後的事情,現在我們就是要把這些設計模式全部理解,來鍛煉我們的設計思維,而不是只做一個真正的碼農。
《指導原則:六大規則》
在學習設計模式之前,為了不讓設計模式顯得很模式,我們還必須瞭解一個東西,那就是程式設計六大原則。
這些原則是指導模式的規則,我會給一些原則附上一個例子,來說明這個原則所要表達的意思,註意,原則是死的,人是活的,所以並不是要你完完全全遵守這些規則,否則為何資料庫會有逆範式,只是在可能的情況下,請儘量遵守。
單一職責原則(六大規則中的小蘿莉,人見人愛):描述的意思是每個類都只負責單一的功能,切不可太多,並且一個類應當儘量的把一個功能做到極致。
否則當你去維護這個系統的時候,你會後悔你當初的決定,下麵是我自己思索的例子,給各位參考一下,給出代碼。
import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; public class Calculator { public int add() throws NumberFormatException, IOException{ File file = new File("E:/data.txt"); BufferedReader br = new BufferedReader(new FileReader(file)); int a = Integer.valueOf(br.readLine()); int b = Integer.valueOf(br.readLine()); return a+b; } public static void main(String[] args) throws NumberFormatException, IOException { Calculator calculator = new Calculator(); System.out.println("result:" + calculator.add()); } }
來看上面這個例子,這個方法的作用是從一個文件中讀出兩個數,並返回它們的和,我相信各位也能看出當中有明顯的多職責問題。如果沒看出來的話,我想問各位一句,如果我想算這個文件中兩個數字的差該如何做?
相信答案應該是我COPY出來一個div方法,把最後的加號改成減號。好吧,那我要除法呢?乘法呢?取模呢?COPY四次嗎。這就造成了很多很多的代碼重覆,這不符合系統設計的規則。下麵我把上述程式改善一下。
我們分離出來一個類用來讀取數據,來看Reader。
package com.test; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; public class Reader { int a,b; public Reader(String path) throws NumberFormatException, IOException{ BufferedReader br = new BufferedReader(new FileReader(new File(path))); a = Integer.valueOf(br.readLine()); b = Integer.valueOf(br.readLine()); } public int getA(){ return a; } public int getB(){ return b; } }
下麵是我們單獨的計算器類。
package com.test; import java.io.IOException; public class Calculator { public int add(int a,int b){ return a + b; } public static void main(String[] args) throws NumberFormatException, IOException { Reader reader = new Reader("E:/data.txt"); Calculator calculator = new Calculator(); System.out.println("result:" + calculator.add(reader.getA(),reader.getB())); } }
我們將一個類拆成了兩個類,這樣以後我們如果有減法,乘法等等,就不用出現那麼多重覆代碼了。
以上是我臨時杜撰的例子,雖然很簡單,並且沒有什麼現實意義,但是我覺得足夠表達單一職責的意思,並且也足夠說明它的重要性。單一職責原則是我覺得六大原則當中最應該遵守的原則,因為我在實踐過程中發現,當你在項目的開發過程中遵循它,幾乎完全不會給你的系統造成任何多餘的複雜性,反而會令你的程式看起來井然有序。
里氏替換原則(六大原則中最文靜的姑娘,但卻不太招人喜歡):這個原則表達的意思是一個子類應該可以替換掉父類並且可以正常工作。
那麼翻譯成比較容易理解的話,就是說,子類一般不該重寫父類的方法,因為父類的方法一般都是對外公佈的介面,是具有不可變性的,你不該將一些不該變化的東西給修改掉。
上述只是通常意義上的說法,很多情況下,我們不必太理解里氏替換這個文靜的姑娘,比如模板方法模式,預設適配器,裝飾器模式等一些設計模式,就完全不搭理這個文靜的姑娘。
不過就算如此,如果你真的遇見了不得不重寫父類方法的場景,那麼請你考慮,你是否真的要把這個類作為子類出現在這裡,或者說這樣做所換來的是否能彌補你失去的東西,比如子類無法代替父類工作,那麼就意味著如果你的父類可以在某一個場景里工作的很正常,那麼你的子類當然也應該可以,否則就會出現下述場景。
比如我們有某一個類,其中有一個方法,調用了某一個父類的方法。
//某一個類 public class SomeoneClass { //有某一個方法,使用了一個父類類型 public void someoneMethod(Parent parent){ parent.method(); } }
父類代碼如下。
public class Parent { public void method(){ System.out.println("parent method"); } }
結果我有一個子類把父類的方法給覆蓋了,並且拋出了一個異常。
public class SubClass extends Parent{ //結果某一個子類重寫了父類的方法,說不支持該操作了 public void method() { throw new UnsupportedOperationException(); } }
這個異常是運行時才會產生的,也就是說,我的SomeoneClass並不知道會出現這種情況,結果就是我調用下麵這段代碼的時候,本來我們的思維是Parent都可以傳給someoneMethod完成我的功能,我的SubClass繼承了Parent,當然也可以了,但是最終這個調用會拋出異常。
public class Client { public static void main(String[] args) { SomeoneClass someoneClass = new SomeoneClass(); someoneClass.someoneMethod(new Parent()); someoneClass.someoneMethod(new SubClass()); } }
這就相當於埋下了一個個陷阱,因為本來我們的原則是,父類可以完成的地方,我用子類替代是絕對沒有問題的,但是這下反了,我每次使用一個子類替換一個父類的時候,我還要擔心這個子類有沒有給我埋下一個上面這種炸彈。
所以里氏替換原則是一個需要我們深刻理解的原則,因為往往有時候違反它我們可以得到很多,失去一小部分,但是有時候卻會相反,所以要想做到活學活用,就要深刻理解這個原則的意義所在。
介面隔離原則(六大原則當中最挑三揀四的挑剔女,胸部極小):也稱介面最小化原則,強調的是一個介面擁有的行為應該儘可能的小。
如果你做不到這一點你經常會發現這樣的狀況,一個類實現了一個介面,裡面很多方法都是空著的,只有個別幾個方法實現了。
這樣做不僅會強制實現的人不得不實現本來不該實現的方法,最嚴重的是會給使用者造成假象,即這個實現類擁有介面中所有的行為,結果調用方法時卻沒收穫到想要的結果。
比如我們設計一個手機的介面時,就要手機哪些行為是必須的,要讓這個介面儘量的小,或者通俗點講,就是裡面的行為應該都是這樣一種行為,就是說只要是手機,你就必須可以做到的。
上面就是介面隔離原則這個挑剔女所挑剔的地方,假設你沒有滿足她,你或許會寫出下麵這樣的手機介面。
public interface Mobile { public void call();//手機可以打電話 public void sendMessage();//手機可以發簡訊 public void playBird();//手機可以玩憤怒的小鳥? }
上面第三個行為明顯就不是一個手機應該有的,或者說不是一個手機必須有的,那麼上面這個手機的介面就不是最小介面,假設我現在的非智能手機去實現這個介面,那麼playBird方法就只能空著了,因為它不能玩。
所以我們更好的做法是去掉這個方法,讓Mobile介面最小化,然後再建立下麵這個介面去擴展現有的Mobile介面。
public interface SmartPhone extends Mobile{ public void playBird();//智能手機的介面就可以加入這個方法了 }
這樣兩個介面就都是最小化的了,這樣我們的非智能手機就去實現Mobile介面,實現打電話和發簡訊的功能,而智能手機就實現SmartPhone介面,實現打電話、發簡訊以及玩憤怒的小鳥的功能,兩者都不會有多餘的要實現的方法。
最小介面原則一般我們是要儘量滿足的,如果實在有多餘的方法,我們也有補救的辦法,而且有的時候也確實不可避免的有一些實現類無法全部實現介面中的方法,這時候就輪到預設適配器上場了,這個在後面再介紹。
依賴倒置原則(六大原則中最小鳥依人的姑娘,對抽象的東西非常依賴):這個原則描述的是高層模塊不該依賴於低層模塊,二者都應該依賴於抽象,抽象不應該依賴於細節,細節應該依賴於抽象。
上面黑色加粗這句話是這個原則的原版描述,我來解釋下我自己的理解,這個原則描述的是一個現實當中的事實,即實現都是易變的,而只有抽象是穩定的,所以當依賴於抽象時,實現的變化並不會影響客戶端的調用。
比如上述的計算器例子,我們的計算器其實是依賴於數據讀取類的,這樣做並不是很好,因為如果我的數據不是文件里的了,而是在資料庫里,這樣的話,為了不影響你現有的代碼,你就只能將你的Reader類整個改頭換面。
或者還有一種方式就是,你再添加一個DBReader類,然後把你所有使用Reader讀取的地方,全部手動替換成DBReader,這樣其實也還可以接受,那假設我有的從文件讀取,有的從資料庫讀取,有的從XML文件讀取,有的從網路中讀取,有的從標準的鍵盤輸入讀取等等。
你想怎麼辦呢?
所以我們最好的做法就是抽象出一個抽象類或者是介面,來表述數據讀取的行為,然後讓上面所有的讀取方式所實現的類都實現這個介面,而我們的客戶端,只使用我們定義好的介面,當我們的實現變化時,我只需要設置不同的實際類型就可以了,這樣對於系統的擴展性是一個大大的提升。
針對上面簡單的數據讀取,我們可以定義如下介面去描述。
public interface Reader { public int getA(); public int getB(); }
讓我們原來的Reader改名為FileReader去實現這個介面,這樣計算器就依賴於抽象的介面,這個依賴是非常穩定的,因為不論你以後要從哪讀取數據,你的兩個獲取數據的方法永遠都不會變。
這樣,我們讓DBReader,XMLReader,NETReader,StandardOutPutStreamReader等等,都可以實現Reader這個介面,而我們的客戶端調用依賴於一個Reader,這樣不管數據是從哪來的,我們都可以應對自如,因為我根本不關心你是什麼Reader,我只知道你能讓我獲得A和B這兩個值就行了。
這便是我們依賴於抽象所得到的靈活性,這也是JAVA語言的動態特性給我們帶來的便利,所以我們一定要好好珍惜這個依賴於抽象的姑娘。
迪米特原則(六大原則中最害羞的姑娘,不太愛和陌生人說話):也稱最小知道原則,即一個類應該儘量不要知道其他類太多的東西,不要和陌生的類有太多接觸。
這個原則的制定,是因為如果一個類知道或者說是依賴於另外一個類太多細節,這樣會導致耦合度過高,應該將細節全部高內聚於類的內部,其他的類只需要知道這個類主要提供的功能即可。
所謂高內聚就是儘可能將一個類的細節全部寫在這個類的內部,不要漏出來給其他類知道,否則其他類就很容易會依賴於這些細節,這樣類之間的耦合度就會急速上升,這樣做的後果往往是一個類隨便改點東西,依賴於它的類全部都要改。
比如我把上述的例子改變一下。
import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; public class Reader { int a,b; private String path; private BufferedReader br; public Reader(String path){ this.path = path; } public void setBufferedReader() throws FileNotFoundException{ br = new BufferedReader(new FileReader(new File(path))); } public void readLine() throws NumberFormatException, IOException{ a = Integer.valueOf(br.readLine()); b = Integer.valueOf(br.readLine()); } public int getA(){ return a; } public int getB(){ return b; } }
Reader類改成上述這個樣子,顯然它給其他的類透漏了太多細節,讓別人知道了它的太多細節,這樣我客戶端調用的時候就很可能寫成如下形式。
public class Client { public static void main(String[] args) throws Exception { Reader reader = new Reader("E:/test.txt"); reader.setBufferedReader(); reader.readLine(); int a = reader.getA(); int b = reader.getB(); //以下用於計算等等 } }
這樣客戶端就依賴於reader的多個行為才能最終獲取到A和B兩個數值,這時候兩個類的耦合度就太高了,我們更好的做法使用訪問許可權限制將二者都給隱藏起來不讓外部調用的類知道這些。就像下麵這樣。
public class Reader { int a,b; private String path; private BufferedReader br; public Reader(String path) throws Exception{ super(); this.path = path; setBufferedReader(); readLine(); } //註意,我們變為私有的方法 private void setBufferedReader() throws FileNotFoundException{ br = new BufferedReader(new FileReader(path)); } //註意,我們變為私有的方法 private void readLine() throws NumberFormatException, IOException{ a = Integer.valueOf(br.readLine()); b = Integer.valueOf(br.readLine()); } public int getA(){ return a; } public int getB(){ return b; } }
我們最終將兩個方法都變為私有封裝在Reader類當中,這樣外部的類就無法知道這兩個方法了,所以迪米特原則雖說是指的一個類應當儘量不要知道其他類太多細節,但其實更重要的是一個類應當不要讓外部的類知道自己太多。兩者是相輔相成的,只要你將類的封裝性做的很好,那麼外部的類就無法依賴當中的細節。
開-閉原則(六大原則中絕對的大姐大,另外五姐妹心甘情願臣服):最後一個原則,一句話,對修改關閉,對擴展開放。
就是說我任何的改變都不需要修改原有的代碼,而只需要加入一些新的實現,就可以達到我的目的,這是系統設計的理想境界,但是沒有任何一個系統可以做到這一點,哪怕我一直最欣賞的spring框架也做不到,雖說它的擴展性已經強到變態。
這個原則更像是前五個原則的總綱,前五個原則就是圍著它轉的,只要我們儘量的遵守前五個原則,那麼設計出來的系統應該就比較符合開閉原則了,相反,如果你違背了太多,那麼你的系統或許也不太遵循開閉原則。
在《大話設計模式》一書中,提到一句話與各位共勉,我覺得很有說服力,即用抽象構建框架,用細節實現擴展。
以上六個原則寫出來是為了指導後面設計模式的描述,基本都是我自己體會出來的理解,或許當中有好有壞,有優有差,各位不需要太過在意形式的表述,完全可以有自己的理解。