實驗二 Java面向對象程序設計

實驗二 Java面向對象程序設計

實驗內容

1. 初步掌握單元測試和TDD

2. 理解並掌握面向對象三要素:封裝、繼承、多態

3. 初步掌握UML建模

4. 熟悉S.O.L.I.D原則

5. 瞭解設計模式

實驗要求

1.沒有Linux基礎的同窗建議先學習《Linux基礎入門(新版)》《Vim編輯器》 課程

2.完成實驗、撰寫實驗報告,實驗報告以博客方式發表在博客園,注意實驗報告重點是運行結果,遇到的問題(工具查找,安裝,使用,程序的編輯,調試,運行等)、解決辦法(空洞的方法如「查網絡」、「問同窗」、「看書」等一概得0分)以及分析(從中能夠獲得什麼啓示,有什麼收穫,教訓等)。報告能夠參考範飛龍老師的指導

3. 嚴禁抄襲,有該行爲者實驗成績歸零,並附加其餘懲罰措施。

4. 請你們先在實驗樓中的~/Code目錄中用本身的學號創建一個目錄,代碼和UML圖要放到這個目錄中,截圖中沒有學號的會要求重作,而後跟着下面的步驟練習

實驗步驟

(一)單元測試

(1) 三種代碼

編程是智力活動,不是打字,編程前要把幹什麼、如何幹想清楚才能把程序寫對、寫好。與目前很多同窗一說編程就打開編輯器寫代碼不一樣,我但願同窗們養成一個習慣,當大家想用程序解決問題時,要會寫三種碼:html

  • 僞代碼
  • 產品代碼
  • 測試代碼

咱們經過一個例子說明如何寫這三種代碼。java

需求:咱們要在一個MyUtil類中解決一個百分制成績轉成「優、良、中、及格、不及格」五級製成績的功能。linux

咱們先寫僞代碼,僞代碼能夠用漢語寫,推薦你們用英語寫,僞代碼與具體編程語言無關,不要寫與具體編程語言語法相關的語句(如用malloc分配內存,這樣只能用C語言編程了),僞代碼從意圖層面來解決問題,最終,僞代碼產品代碼最天然的、最好的註釋。針對上面的問題,咱們能夠經過僞代碼這樣解決:程序員

百分制轉五分制:
   若是成績小於60,轉成「不及格」
   若是成績在60與70之間,轉成「及格」
   若是成績在70與80之間,轉成「中等」
   若是成績在80與90之間,轉成「良好」
   若是成績在90與100之間,轉成「優秀」
   其餘,轉成「錯誤」

簡單吧?想用編程來解決問題,首先要用僞代碼代表本身想明白了。
有了僞代碼,咱們用特定編程語言翻譯一下,就是可用的產品代碼了,固然,咱們在這要選用Java,小菜一碟了,翻譯好的MyUtil.java以下:web

public class MyUtil{
   public static String percentage2fivegrade(int grade){
       //若是成績小於60,轉成「不及格」
       if (grade < 60)
           return "不及格";
       //若是成績在60與70之間,轉成「及格」
       else if (grade < 70)
           return "及格";
       //若是成績在70與80之間,轉成「中等」
       else if (grade < 80)
           return "中等";
       //若是成績在80與90之間,轉成「良好」
       else if (grade < 90)
           return "良好";
       //若是成績在90與100之間,轉成「優秀」
       else if (grade < 100)
           return "優秀";
       //其餘,轉成「錯誤」
       else 
           return "錯誤";
   }
}

產品代碼寫完了,若是別人要使用這個代碼,把MyUtil.java拷給他就能夠了。可是做爲負責任的你,確定會擔憂本身的程序會有Bug。若是別人用本身的代碼發現一堆Bugs,那多沒面子!怎麼辦?寫了產品代碼,咱們還要寫測試代碼,證實本身的代碼沒有問題。Java編程時,程序員對類實現的測試叫單元測試。類XXXX單元測試,咱們通常寫建一個XXXXTest的類,針對MyUtil類咱們寫一個MyUtilTest.java的測試模塊,代碼以下:shell

public class MyUtilTest {
public static void main(String[] args) {
        // 百分制成績是50時應該返回五級制的「不及格」
        if(MyUtil.percentage2fivegrade(50) != "不及格")
            System.out.println("test failed!");
        else 
            System.out.println("test passed!");
    }
}

這裏咱們設計了一個測試用例(Test Case)測試用例是爲某個特殊目標而編制的一組測試輸入、執行條件以及預期結果,以便測試某個程序路徑或覈實是否知足某個特定需求。這裏咱們的測試輸入是「50」,預期結果是「不及格」。在Eclipse中運行結果以下,測試結果符合預期:
圖片描述信息編程

只有一組輸入的測試是不充分的,咱們把通常狀況都測試一下,代碼以下:vim

public class MyUtilTest {
    public static void main(String[] args) {
        //測試正常狀況
        if(MyUtil.percentage2fivegrade(55) != "不及格")
            System.out.println("test failed!");
        else if(MyUtil.percentage2fivegrade(65) != "及格")
            System.out.println("test failed!");
        else if(MyUtil.percentage2fivegrade(75) != "中等")
            System.out.println("test failed!");
        else if(MyUtil.percentage2fivegrade(85) != "良好")
            System.out.println("test failed!");
        else if(MyUtil.percentage2fivegrade(95) != "優秀")
            System.out.println("test failed!");
        else 
            System.out.println("test passed!");
    }
}

在Eclipse中運行結果以下,測試結果符合預期:
圖片描述信息設計模式

咱們不能只測試正常狀況,下面看看異常狀況如何,好比輸入爲負分或大於100的成績,代碼以下:微信

public class MyUtilTest {
    public static void main(String[] args) {
        //測試出錯狀況
        if(MyUtil.percentage2fivegrade(-10) != "錯誤")
            System.out.println("test failed 1!");
        else if(MyUtil.percentage2fivegrade(115) != "錯誤")
            System.out.println("test failed 2!");
        else 
            System.out.println("test passed!");
    }
}

運行程序發現負分時與指望不一致,終於找到了一個bug,緣由是判斷不及格時沒有要求成績大於零。咱們修改MyUtil.java,增長對負分的判斷,代碼以下:

public class MyUtil{
   public static String percentage2fivegrade(int grade){
       //若是成績小於0,轉成「錯誤」
       if ((grade < 0))
           return "錯誤";
       //若是成績小於60,轉成「不及格」
       else if (grade < 60)
           return "不及格";
       //若是成績在60與70之間,轉成「及格」
       else if (grade < 70)
           return "及格";
       //若是成績在70與80之間,轉成「中等」
       else if (grade < 80)
           return "中等";
       //若是成績在80與90之間,轉成「良好」
       else if (grade < 90)
           return "良好";
       //若是成績在90與100之間,轉成「優秀」
       else if (grade < 100)
           return "優秀";
       //若是成績大於100,轉成「錯誤」
       else
           return "錯誤";
   }
}

再次運行測試,測試結果符合預期,以下圖所示:
圖片描述信息

測試夠了嗎?還不夠,通常代碼在邊界處最容易出錯,咱們尚未測試邊界狀況,咱們對輸入爲「0,60,70,80,90,100」這些邊界狀況進行測試的代碼以下:

public class MyUtilTest {
    public static void main(String[] args) {
        //測試邊界狀況
        if(MyUtil.percentage2fivegrade(0) != "不及格")
            System.out.println("test failed 1!");
        else if(MyUtil.percentage2fivegrade(60) != "及格")
            System.out.println("test failed 2!");
        else if(MyUtil.percentage2fivegrade(70) != "中等")
            System.out.println("test failed 3!");
        else if(MyUtil.percentage2fivegrade(80) != "良好")
            System.out.println("test failed 4!");
        else if(MyUtil.percentage2fivegrade(90) != "優秀")
            System.out.println("test failed 5!");
        else if(MyUtil.percentage2fivegrade(100) != "優秀")
            System.out.println("test failed 6!");
        else 
            System.out.println("test passed!"); 
    }
}

測試結果以下:
圖片描述信息
咱們發現邊界狀況中輸入100時有一個Bug。咱們修改MyUtil.java,把判斷優秀的條件中包含輸入爲100的狀況,代碼以下:

public class MyUtil{
   public static String percentage2fivegrade(int grade){
       //若是成績小於0,轉成「錯誤」
       if ((grade < 0))
           return "錯誤";
       //若是成績小於60,轉成「不及格」
       else if (grade < 60)
           return "不及格";
       //若是成績在60與70之間,轉成「及格」
       else if (grade < 70)
           return "及格";
       //若是成績在70與80之間,轉成「中等」
       else if (grade < 80)
           return "中等";
       //若是成績在80與90之間,轉成「良好」
       else if (grade < 90)
           return "良好";
       //若是成績在90與100之間,轉成「優秀」
       else if (grade <= 100)
           return "優秀";
       //若是成績大於100,轉成「錯誤」
       else
           return "錯誤";
   }
}

這時測試都符合預期了,咱們把MyUtil.java提供給別人使用時,內心比較有底氣了。那如何保證單元測度是充分的呢?咱們的通常要求是測試代碼要比產品代碼多。如何寫測試,《單元測試之道》提出了Right-BICEP的方法,你們能夠參考一下。
軟件是由多人合做完成的,不一樣人員的工做相互有依賴關係。軟件的不少錯誤都來源於程序員對模塊功能的誤解、疏忽或不瞭解模塊的變化。如何能讓本身負責的模塊功能定義儘可能明確,模塊內部的改變不會影響其餘模塊,並且模塊的質量能獲得穩定的、量化的保證?單元測試就是一個頗有效的解決方案。

(2) TDD(Test Driven Devlopment, 測試驅動開發)

前面的例子,咱們先寫產品代碼,而後再寫測試代碼,經過測試發現了一些Bugs,提升了代碼質量。這有問題嗎?軟件開發從建築中吸收了不少養分,咱們看看一個砌牆的例子,以下圖:
圖片描述信息
工人是「先把牆砌好的,再用繩子測一下牆平不平,直不直,若是不平或不直拆了重砌」,仍是「先用繩子給出平和直的標準,而後靠着繩子砌牆,從而保證了牆砌出來就是又平又直的」呢?答案是不言而喻的了。
拿編程作對比,咱們是該「先寫產品代碼,而後再寫測試代碼,經過測試發現了一些Bugs,修改代碼」,仍是該「先寫測試代碼,而後再寫產品代碼,從而寫出來的代碼就是正確的」呢?固然先寫測試代碼了。這種先寫測試代碼,而後再寫產品代碼的開發方法叫「測試驅動開發」(TDD)。TDD的通常步驟以下:

  • 明確當前要完成的功能,記錄成一個測試列表
  • 快速完成編寫針對此功能的測試用例
  • 測試代碼編譯不經過(沒產品代碼呢)
  • 編寫產品代碼
  • 測試經過
  • 對代碼進行重構,並保證測試經過(重構下次實驗練習)
  • 循環完成全部功能的開發

基於TDD,咱們不會出現過分設計的狀況,需求經過測試用例表達出來了,咱們的產品代碼只要讓測試經過就能夠了。
Java中有單元測試工具JUnit來輔助進行TDD,咱們用TDD的方式把前面百分制轉五分制的例子重寫一次,體會一下有測試工具支持的開發的好處。
打開Eclipse,單擊File->New->Java Project新建一個TDDDemo的Java項目,以下圖:
圖片描述信息
咱們在TDDDemo項目中,把鼠標放到項目名TDDDemo上,單擊右鍵,在彈出的菜單中選定New->Source Folder新建一個測試目錄test,以下圖:
圖片描述信息

圖片描述信息

圖片描述信息

咱們把鼠標放到test目錄上,單擊右鍵,在彈出的菜單中選定New->JUnit Test Case新建一個測試用例類MyUtilTest,以下圖:
圖片描述信息

圖片描述信息

圖片描述信息

圖片描述信息

咱們增長第一個測試用例testNormal,注意測試用例前必定要有註解@Test,測試用例方法名任意,輸入如下代碼:

import org.junit.Test;
import junit.framework.TestCase;
public class MyUtilTest extends TestCase {
    @Test
    public void testNormal() {
        assertEquals("不及格", MyUtil.percentage2fivegrade(55));
        assertEquals("及格", MyUtil.percentage2fivegrade(65));
        assertEquals("中等", MyUtil.percentage2fivegrade(75));
        assertEquals("良好", MyUtil.percentage2fivegrade(85));
        assertEquals("優秀", MyUtil.percentage2fivegrade(95));
    }
}

輸入完畢,Eclipse中以下圖所示:
圖片描述信息

圖中的紅叉說明代碼存在語法錯誤,緣由很簡單,MyUtil類還不存在,類中的percentage2fivegrade方法也不存在,咱們在TDDDemosrc目錄中新建一個MyUtil的類,並實現percentage2fivegrade方法,以下圖所示:
圖片描述信息

圖片描述信息

圖片描述信息

你們能夠看到如今測試代碼沒有語法錯誤了,咱們把鼠標放到MyUtilTest.java上,單擊右鍵,選擇Run as->JUnit Test,以下圖:

圖片描述信息

圖片描述信息

測試結果出現了一個紅條(red bar),說明測試沒經過,紅條上面彙總了測試狀況,運行了一個測試,沒有錯誤,一個測試沒經過。下面緣由說的也很清楚:測試代碼第十行傳入55時,指望結果是「不及格」,代碼返回了「錯誤」,修改MyUtil.Java吧,輸入如下代碼:
圖片描述信息

再運行測試,以下圖所示:
圖片描述信息

測試結果出現了一個綠條(green bar),說明測試經過了。TDD的目標是"Clean Code That Works",TDD的slogan是"Keep the bar green, to Keep the code clean",你們體會一下。

TDD的編碼節奏是:

  • 增長測試代碼,JUnit出現紅條
  • 修改產品代碼
  • JUnit出現綠條,任務完成

咱們增長一個測試異常狀況的用例testException,以下圖:
圖片描述信息

咱們增長一個測試邊界狀況的用例testBoundary,以下圖:
圖片描述信息

如何讓JUnit的gree bar出來,動手實驗一下,以下圖:
圖片描述信息

無論用不用TDD,寫出高質量的測試用例纔是最重要的,如何進行單元測試,你們可參考一下《單元測試之道》這本書。另外,《Agile Java 中文版》展現瞭如何將Java和TDD進行有效的整合,經過TDD驅動項目開發,有興趣的能夠參考。

(二)面向對象三要素

(1)抽象

抽象一詞的本意是指人在認識思惟活動中對事物表象因素的捨棄和對本質因素的抽取。抽象是人類認識復瑣事物和現象時常用的思惟工具,抽象思惟能力在程序設計中很是重要,"去粗取精、化繁爲簡、由表及裏、異中求同"的抽象能力很大程度上決定了程序員的程序設計能力。
抽象就是抽出事物的本質特徵而暫時不考慮他們的細節。對於複雜系統問題人們藉助分層次抽象的方法進行問題求解;在抽象的最高層,可使用問題環境的語言,以歸納的方式敘述問題的解。在抽象的較低層,則採用過程化的方式進行描述。在描述問題解時,使用面向問題和麪向實現的術語。
程序設計中,抽象包括兩個方面,一是過程抽象,二是數據抽象。
咱們舉個例子說明一下。好比有了如下Java代碼:

System.out.println(1);
System.out.println(2);
System.out.println(3);

能夠打印出「1,2,3」,想打引「1,2,3,4」怎麼辦?同窗們的作法大可能是把上面的代碼拷貝下來,再加一行:

System.out.println(1);
System.out.println(2);
System.out.println(3);
System.out.println(4);

這就是沒有學會過程抽象的作法「拷貝粘貼」式開發。解決問題沒?解決了,但有問題,好比想打印出「1..100"怎麼辦?粘貼100行?這兩段代碼有三行重複的代碼,違反了常見的一個編程原則DRY(Don't Repeat Yourself),解決的方法是進行過程抽象,寫一個函數printn:

public void printn(int n){
    for(int i=1;  i<=n; i++)
        System.out.println(n);
}

上面兩段代碼就能夠用;

printn(3);
printn(4);

代替了,打印出「1..100"也很簡單,只要調用printn(100);就好了。

(2)封裝、繼承與多態

面向對象(Object-Oriented)的三要素包括:封裝、繼承、多態。面向對象的思想涉及到軟件開發的各個方面,如面向對象分析(OOA)、面向對象設計(OOD)、面向對象編程實現(OOP)。OOA根據抽象關鍵的問題域來分解系統,關注是什麼(what)。OOD是一種提供符號設計系統的面向對象的實現過程,用很是接近問題域術語的方法把系統構形成「現實世界」的對象,關注怎麼作(how),經過模型來實現功能規範。OOP則在設計的基礎上用編程語言(如Java)編碼。貫穿OOA、OOD和OOP的主線正是抽象。
OOD中建模會用圖形化的建模語言UML(Unified Modeling Language),UML是一種通用的建模語言,咱們實驗中使用umbrello進行建模,Windows中推薦你們使用 StarUML

過程抽象的結果是函數,數據抽象的結果是抽象數據類型(Abstract Data Type,ADT),類能夠做具備繼承和多態機制的ADT。數據抽象纔是OOP的核心和起源。

OO三要素的第一個要素是封裝,封裝就是將數據與相關行爲包裝在一塊兒以實現信息就隱藏。Java中用類進行封裝,好比一個Dog類:

public class Dog {
    private String color;
    public String getColor() {
        return color;
    }
    public void setColor(String color) {
        this.color = color;
    }
    public String bark(){
        return "汪汪";
    }
    public String toString(){
        return "The Dog's color is " + this.getColor() +", and it shouts "+ this.bark() + "!";
    }
}

封裝實際上使用方法(method)將類的數據隱藏起來,控制用戶對類的修改和訪問數據的程度,從而帶來模塊化(Modularity)信息隱藏(Information hiding)的好處;接口(interface)是封裝的準確描述手段。
Dog類經過使用類和訪問控制(private,public)隱藏了屬性color,開放了接口setColor(),getColor(),bark()toStringDog類是一個模塊,咱們能夠經過下面的代碼使用它,測試代碼與運行結果以下:
圖片描述信息

咱們能夠用UML中的類圖來描述類Dog,首先咱們在實驗樓的環境中打開shell,在命令行中輸入umbrello,打開UML建模軟件umbrello,以下圖所示:
圖片描述信息

圖片描述信息

先單擊工具欄上的類圖標,再在class diagram(類圖)中單擊一下,會彈出一個聖誕框,輸入類名Dog,以下圖:
圖片描述信息

圖片描述信息

咱們把鼠標放到Dog類上,單擊右鍵,選擇Properties,在彈出的對話框中的Display中去掉Public Only選項,以下圖:

圖片描述信息

圖片描述信息

咱們把鼠標放到Dog類上,單擊右鍵,選擇New->Attribute,在彈出的對話框中的填好Type,Name,並選好Visibility,以下圖:
圖片描述信息

圖片描述信息

圖片描述信息

咱們把鼠標放到Dog類上,單擊右鍵,選擇New->Operation,在彈出的對話框中的填好Type,Name,並選好Visibility,以下圖:
圖片描述信息

圖片描述信息

圖片描述信息

咱們能夠看到,在UML 裏,一個類的屬性能顯示它的名字,類型,初始化值,屬性也能夠顯示private,public,protected。 類的方法能顯示它們的方法名,參數,返回類型,以及方法的private,public,protected屬性。其中:

  • +表示public
  • #表示 protected
  • -表示 private

使用UML可讓咱們沒必要關注細節。一樣,咱們能夠創建一個Cat類(請你們模仿Dog類實現Cat類),以下圖所示:
圖片描述信息

這時的測試類如如下UML圖所示:
圖片描述信息

注意:UML類圖要展現類之間的靜態關係,AnimalTest類依賴Dog類和Cat類,UML中依賴用帶箭頭的直線表示。
對應的測試代碼和運行結果以下圖所示:
圖片描述信息

咱們看到Dog類和Cat類都有Color屬性和相應的setter和getter方法,明顯違反了前面提到的DRY原則,咱們能夠經過繼承解決這個問題,把Color屬性和相應的setter和getter方法放到父類Animal中,如如下UML較圖所示:
圖片描述信息

請你們注意UML類圖中繼承的表示法,是用一個帶三角的直線指向父類,經過繼承,咱們消除了Dog類和Cat類中的重複代碼,符合DRY的要求。
繼承指一個類的定義能夠基於另一個已經存在的類,即子類基於父類,從而實現父類代碼的重用。既存類稱做基類、超類、父類(base class、super class、parent class),新類稱做派生類、繼承類、子類(derived class、inherited class、child class)。繼承關係表達了」Is a kind of「的關係,稱爲「ISA」關係。繼承的關鍵在於確認子類爲父類的一個特殊類型
。繼承是實現軟件可重用的根基,是提升軟件系統的可擴展性與可維護性的主要途徑。
如上面所示,以封裝爲基礎,繼承能夠實現代碼複用,須要注意的是,繼承更重要的做用是實現多態
面向對象中容許不一樣類的對象對同一消息作出響應,即同一消息能夠根據發送對象的不一樣而採用多種不一樣的行爲方式,咱們稱此現象爲多態性。Java中,多態是指不一樣的類對象調用同一個簽名的成員方法時將執行不一樣代碼的現象。多態是面向對象程序設計的靈活性和可擴展性的基礎
咱們再看看上一個類圖,咱們能夠進一步抽象,把Dog類中的bark()Cat類中的meow()抽象成一個抽象方法shout(),Dog類和Cat類中覆蓋這個方法,如如下UML圖所示:
圖片描述信息
你們注意UML類圖中的Animal類中的shout()方法是抽象方法,是斜體的,Animal類是抽象類,也是斜體的。
對應的代碼以下:

public abstract class Animal {
    private String color;
    public String getColor() {
        return color;
    }
    public void setColor(String color) {
        this.color = color;
    }
    public abstract String shout(); 
}
public class Dog extends Animal{
    public String shout(){
        return "汪汪";
    }
       public String toString(){
        return "The Dog's color is " + this.getColor() +", and it shouts "+ this.shout() + "!";
    }
}
public class Cat extends Animal{
    public String shout(){
        return "喵喵";
    }
    public String toString(){
        return "The Cat's color is " + this.getColor() +", and it shouts "+ this.shout() + "!";
    }
}

測試代碼和運行結果以下:
圖片描述信息

你們注意,這時getInfo只須要一個了,參數爲父類Animal,當方法參數類型爲父類時,能夠傳入子類的對象,如上面第8行所示。你們須要理解並記住「在Java中,當咱們用父類聲明引用,用子類生成對象時,多態就出現了」,如上面第6行所示。

另外,在Umbrello中UML圖是能夠轉化成Java代碼的,有Java代碼也能夠生成UML圖的,你們摸索一下吧。學習UML,《UML精粹》是本不錯的入門書,學會了UML和麪向對象方法,往後不編程也能幫你不少。

(三)設計模式初步

(1)S.O.L.I.D原則

面向對象三要素是「封裝、繼承、多態」,任何面向對象編程語言都會在語法上支持這三要素。如何藉助抽象思惟用好三要素特別是多態仍是很是困難的,S.O.L.I.D類設計原則是一個很好的指導:

  • SRP(Single Responsibility Principle,單一職責原則)
  • OCP(Open-Closed Principle,開放-封閉原則)
  • LSP(Liskov Substitusion Principle,Liskov替換原則)
  • ISP(Interface Segregation Principle,接口分離原則)
  • DIP(Dependency Inversion Principle,依賴倒置原則)

OCP是OOD中最重要的一個原則,OCP的內容是:

  • software entities (class, modules, function, etc.) should open for extension,but closed for modification.
  • 軟件實體(類,模塊,函數等)應該對擴充開放,對修改封閉。

圖片描述信息

對擴充開放(Open For Extension )要求軟件模塊的行爲必須是能夠擴充的,在應用需求改變或須要知足新的應用需求時,咱們要讓模塊以不一樣的方式工做; 對修改封閉(Closed for Modification )要求模塊的源代碼是不可改動的,任何人都不準修改已有模塊的源代碼。 基於OCP,利用面向對象中的多態性(Polymorphic),更靈活地處理變動擁抱變化,OCP能夠用如下手段實現:(1)抽象和繼承,(2)面向接口編程。
好比,在一個圖形系統中,已經存在三個模塊Shape,Square,Circle,以下圖所示:
圖片描述信息

用戶現大須要一個Triangle模塊是一個合理的要求,因爲咱們使用了多態,原先的模塊不須要改變,只要新增長一個模塊Triangle就能夠了,以下圖所示:
圖片描述信息
這個圖形系統是符合OCP原則的。

SRP的內容是:

  • There should never be more than one reason for a class to change
  • 決不要有一個以上的理由修改一個類

圖片描述信息
對象提供單一職責的高度封裝,對象的改變僅僅依賴於單一職責的改變,它基於軟件設計中的高內聚性定義。如何判斷一個類是否是符合SRP原則,咱們看一個例子。下面這個類符合SRP原則嗎?
圖片描述信息
好象全部的方法都是汽車類(Automobile)須要的,咱們能夠經過如下方法對一個類作SRP分析:用類作主語,用方法名作謂語,看看邏輯上能不能說的通,以下圖所示:
圖片描述信息

分析的結果,Automabile類應該分紅四個類,以下圖所示:
圖片描述信息

圖片描述信息

LSP的內容是:

  • Subtypes must be substitutable for their base types
  • Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it
  • 子類必須能夠被其基類所代
  • 使用指向基類的指針或引用的函數,必須可以在不知道具體派生類對象類型的狀況下使用它

圖片描述信息

LSP是Liskov女士提出的:
圖片描述信息
LSP的核心思想是父類型對象能夠被子類型對象所取代。咱們前面舉的Animal,Dog,Cat的那個例子是符合LSP原則的。下面是一個常見的反例,很多Java教材講繼承時都舉這個例子,實際上是錯誤的。
圖片描述信息

請你們想想,Square類爲什麼不能繼承Rectangle類,在數學上好像是沒有什麼問題的呀。LSP告訴你們的一點是不要濫用繼承LSP原則清楚地指出,OOD中「ISA關係」是就行爲功能而言。行爲功能(behavior)不是內在的、私有的,而是外在、公開的,是客戶程序所依賴的接口。

ISP的內容是:

  • Clients should not be forced to depend upon interfaces that they do not use
  • 客戶不該該依賴他們並未使用的接口

圖片描述信息

首先要理解客戶是什麼,前面講到一個XXXX類,要有一個配套的XXXXTest類,XXXXTest中要使用XXXX類,XXXXTest類就能夠看做XXXX類的客戶。咱們看一個例子:
圖片描述信息

接口RowSetManager的功能過多,咱們應該把這個接口分紅多個接口,以便利於複用:
圖片描述信息

DIP的內容是:

  • High level modules should not depend upon low level modules. Both should depend upon abstractions
  • Abstractions should not depend upon details. Details should depend upon abstractions
  • 高層模塊不該該依賴於低層模塊。兩者都應該依賴於抽象
  • 抽象不該該依賴於細節。細節應該依賴於抽象
    圖片描述信息

經過接口或者抽象類,DIP在應用中經過依賴注入的方式實現解耦,重用低級模塊,重用實現,解除依賴。
咱們看一個例子:設想一個簡單的程序,其任務就是實現將鍵盤輸入的字符拷貝到打印機上。進一步假設實現平臺中的操做系統並不支持設備無關性。咱們能夠這樣實現:
圖片描述信息

這樣,上層函數copy依賴下層函數read,write
DIP改變了依賴的方向,要求下面依賴上面,咱們使用抽象類實現DIP,以下圖所示:
圖片描述信息

(2)模式與設計模式

模式是某外在環境(Context) 下﹐對特定問題(Problem)的慣用解決之道(Solution)。模式必須使得問題明晰,闡明爲何用它來求解問題,以及在什麼狀況下有用,什麼狀況下不能起做用,每一個模式因其重複性從而可被複用,自己有本身的名字,有可傳授性,能移植到不一樣情景下。模式能夠看做對一個問題可複用的專家級解決方法
計算機科學中有不少模式:

  • GRASP模式
  • 分析模式
  • 軟件體系結構模式
  • 設計模式:建立型,結構型,行爲型
  • 管理模式: The Manager Pool 實現模式
  • 界面設計交互模式

這裏面最重要的是設計模式,在面向對象中設計模式的地位能夠和麪向過程編程中的數據結構的地位至關。

(3)設計模式實示例

設計模式(design pattern)提供一個用於細化軟件系統的子系統或組件,或它們之間的關係圖,它描述通訊組件的公共再現結構,通訊組件能夠解決特定語境中的一個設計問題。
圖片描述信息
如圖,隨着系統中對象的數量增多,對象之間的交互成指數增加,設計模式能夠幫咱們以最好的方式來設計系統。設計模式背後是抽象和SOLID原則。
設計模式有四個基本要素:

  • Pattern name:描述模式,便於交流,存檔
  • Problem:描述何處應用該模式
  • Solution:描述一個設計的組成元素,不針對特例
  • Consequence:應用該模式的結果和權衡(trade-offs)

咱們先要學習的是GOF的23個設計模式:
圖片描述信息

Java類庫中大量使用設計模式:

  • Factory:java.util.Calendar
  • Compsite:java.awt.Container
  • Decorator:java I/0
  • Iterator:java.util.Enumeration
  • Strategy:java.awt.LayoutManager

咱們經過例子來學習一個設計模式(抽象工廠模式),並瞭解設計模式可能會存在的過分設計問題以及如何避免它。
咱們設計了一個文檔系統,以下圖UML類圖所示:
圖片描述信息

對應的代碼以下:

class Integer { 
   int value;    
   public Integer(){
      value=100;  
   }    
   public void DisplayValue(){
        System.out.println(value);  
   } 
} 
class Document { 
   Integer pi; 
   public Document(){
       pi = new Integer(); 
   } 
   public void DisplayData(){
      pi.DisplayValue();  
   } 
} 
public class MyDoc{ 
   static Document d;
   public static void main(String [] args) { 
        d = new Document(); 
        d.DisplayData(); 
  } 
}

客戶若是要求系統支持Float類,這是一個合理的要求,要支持Float類,Document類要修改兩個地方,這違反了OCP原則,使用多態能夠解決部分問題:
圖片描述信息

對應的代碼以下:

abstract class Data{
    public abstract void DisplayValue(); 
} 
class Integer extends Data { 
  int value; 
   Integer(){
      value=100;  
   }  
   public void DisplayValue(){
       System.out.println(value); 
   }  
}
class Document { 
     Data pd; 
     Document() { 
        pd=new Integer(); 
     } 
     public void DisplayData(){
        pd.DisplayValue(); 
     }     
} 
public class MyDoc {
    static Document d;    
    public static void main(String[] args) {
        d = new Document(); 
        d.DisplayData();      
    }   
}

要支持Float類,Document類要修改構造方法,這還違反了OCP原則。封裝、繼承、多態解決不了問題了,這時須要設計模式了:

圖片描述信息

抽象工廠模式應用以下:
圖片描述信息

對應代碼以下:

// Server Classes 
abstract class Data { 
    abstract public void DisplayValue(); 
}
class Integer extends  Data {    
    int value; 
    Integer() {
         value=100; 
    }  
    public void DisplayValue(){
        System.out.println (value);
    } 
 } 
// Pattern Classes 
abstract class Factory { 
   abstract public Data CreateDataObject(); 
}
class IntFactory extends Factory { 
   public Data CreateDataObject(){
     return new Integer(); 
   } 
} 
//Client classes 
class Document {    
    Data pd; 
    Document(Factory pf){ 
       pd = pf.CreateDataObject(); 
    } 
    public void DisplayData(){
       pd.DisplayValue(); 
   } 
 } 
 //Test class
 public class MyDoc {
    static Document d;
    public static void main(String[] args) {
            d = new Document(new IntFactory()); 
            d.DisplayData(); 
    }   
}

咱們看到經過增長了一層抽象層使代碼符合了OCP原則。代碼有良好的可擴充性、可維護性,代價是代碼多了,效率變低下了。
設計模式初學者容易過分使用它們,致使過分設計,也就是說,遵照DRY和OCP固然好,但會出現YAGNI(You aren't gonna need it, 你不會須要它)問題。
DRY原則和YAGNI原則並不是徹底兼容。前者追求"抽象化",要求找到通用的解決方法;後者追求"快和省",意味着不要把精力放在抽象化上面,由於極可能"你不會須要它"。怎麼平衡呢?有一個Rule of three (三次原則):第一次用到某個功能時,你寫一個特定的解決方法;第二次又用到的時候,你拷貝上一次的代碼(違反了DRY);第三次出現的時候,你才着手"抽象化",寫出通用的解決方法。
設計模式學習先參考一下《深刻淺出設計模式》,這本書可讀性很是好。

除SOLID原則外還有不少其它的面向對象原則。如:

  • "組合替代繼承":這是說相對於繼承,要更傾向於使用組合;
  • "笛米特法則":這是說"你的類對其它類知道的越少越好";
  • "共同封閉原則":這是說"相關類應該打包在一塊兒";
  • "穩定抽象原則":這是說"類越穩定,越應該由抽象類組成";

固然,這些原則並非孤立存在的,而是緊密聯繫的,遵循一個原則的同時也就遵循了另一個或多個原則;反之,違反了其中一個原則也極可能同時就違反了另一個或多個原則。 設計模式是這些原則在一些特定場景的應用結果。所以,能夠把設計模式看做"框架",把OOD原則看做"規範"。 在學習設計模式的過程當中,要常常性的反思,這個設計模式體現了面向對象設計原則中的哪一個或哪一些原則。

(四)練習

1使用TDD的方式設計關實現複數類Complex。

2.實驗報告中統計本身的PSP(Personal Software Process)時間

步驟 耗時 百分比
需求分析
設計
代碼實現
測試
分析總結

3. 實現要有僞代碼,產品代碼,測試代碼。

4.總結單元測試的好處

參考資料

1.《UML精粹
2.《構建之法電子版)》,著者鄒欣Blog
3.《深刻淺出設計模式
4.《解析極限編程
5.《單元測試之道
6.《代碼大全
7.《代碼的抽象三原則

工具

  1. JUnit
  2. umbrello
  3. StarUML

歡迎關注「rocedu」微信公衆號(手機上長按二維碼)

作中教,作中學,實踐中共同進步!

rocedu


相關文章
相關標籤/搜索