實驗二 Java面向對象程序設計
實驗內容
1. 初步掌握單元測試和TDD
2. 理解並掌握面向對象三要素:封裝、繼承、多態
3. 初步掌握UML建模
4. 熟悉S.O.L.I.D原則
5. 了解設計模式
實驗要求
1.沒有Linux基礎的同學建議先學習《Linux基礎入門(新版)》《Vim編輯器》 課程
2.完成實驗、撰寫實驗報告,實驗報告以博客方式發表在博客園,注意實驗報告重點是運行結果,遇到的問題(工具查找,安裝,使用,程序的編輯,調試,運行等)、解決辦法(空洞的方法如“查網絡”、“問同學”、“看書”等一律得0分)以及分析(從中可以得到什么啟示,有什么收獲,教訓等)。報告可以參考范飛龍老師的指導
3. 嚴禁抄襲,有該行為者實驗成績歸零,並附加其他懲罰措施。
4. 請大家先在實驗樓中的~/Code目錄中用自己的學號建立一個目錄,代碼和UML圖要放到這個目錄中,截圖中沒有學號的會要求重做,然后跟着下面的步驟練習。
實驗步驟
(一)單元測試
(1) 三種代碼
編程是智力活動,不是打字,編程前要把干什么、如何干想清楚才能把程序寫對、寫好。與目前不少同學一說編程就打開編輯器寫代碼不同,我希望同學們養成一個習慣,當你們想用程序解決問題時,要會寫三種碼:
- 偽代碼
- 產品代碼
- 測試代碼
我們通過一個例子說明如何寫這三種代碼。
需求:我們要在一個
MyUtil類中解決一個百分制成績轉成“優、良、中、及格、不及格”五級制成績的功能。
我們先寫偽代碼,偽代碼可以用漢語寫,推薦大家用英語寫,偽代碼與具體編程語言無關,不要寫與具體編程語言語法相關的語句(如用malloc分配內存,這樣只能用C語言編程了),偽代碼從意圖層面來解決問題,最終,偽代碼是產品代碼最自然的、最好的注釋。針對上面的問題,我們可以通過偽代碼這樣解決:
百分制轉五分制: 如果成績小於60,轉成“不及格” 如果成績在60與70之間,轉成“及格” 如果成績在70與80之間,轉成“中等” 如果成績在80與90之間,轉成“良好” 如果成績在90與100之間,轉成“優秀” 其他,轉成“錯誤”
簡單吧?想用編程來解決問題,首先要用偽代碼表明自己想明白了。
有了偽代碼,我們用特定編程語言翻譯一下,就是可用的產品代碼了,當然,我們在這要選用Java,小菜一碟了,翻譯好的MyUtil.java如下:
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的測試模塊,代碼如下:
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中運行結果如下,測試結果符合預期:

只有一組輸入的測試是不充分的,我們把一般情況都測試一下,代碼如下:
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方法也不存在,我們在TDDDemo的src目錄中新建一個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()和toString。Dog類是一個模塊,我們可以通過下面的代碼使用它,測試代碼與運行結果如下:

我們可以用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.《代碼的抽象三原則》
工具
歡迎關注“rocedu”微信公眾號(手機上長按二維碼)
做中教,做中學,實踐中共同進步!

-
版權聲明:自由轉載-非商用-非衍生-保持署名| Creative Commons BY-NC-ND 3.0
