0.設計模式-----六大基本原則
在學習設計模式之前,為了不讓設計模式顯得很模式,我們還必須瞭解一個東西,那就是程式設計六大原則。
這些原則是指導模式的規則,我會給一些原則附上一個例子,來說明這個原則所要表達的意思,注意,原則是死的,人是活的,所以並不是要你完完全全遵守這些規則,否則為何資料庫會有逆正規化,只是在可能的情況下,請儘量遵守。
單一職責原則(六大規則中的小蘿莉,人見人愛):描述的意思是每個類都只負責單一的功能,切不可太多,並且一個類應當儘量的把一個功能做到極致
否則當你去維護這個系統的時候,你會後悔你當初的決定,下面是我自己思索的例子,給各位參考一下,給出程式碼。
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框架也做不到,雖說它的擴充套件性已經強到變態。
這個原則更像是前五個原則的總綱,前五個原則就是圍著它轉的,只要我們儘量的遵守前五個原則,那麼設計出來的系統應該就比較符合開閉原則了,相反,如果你違背了太多,那麼你的系統或許也不太遵循開閉原則。
在《大話設計模式》一書中,提到一句話與各位共勉,我覺得很有說服力,即用抽象構建框架,用細節實現擴充套件。
以上六個原則寫出來是為了指導後面設計模式的描述,基本都是我自己體會出來的理解,或許當中有好有壞,有優有差,各位不需要太過在意形式的表述,完全可以有自己的理解。