課程:Java實驗 班級:201352 姓名:程涵 學号:20135210
成績: 指導教師:婁佳鵬 實驗日期:15.05.05
實驗密級: 預習程度: 實驗時間:
儀器組次: 必修/選修:選修 實驗序号:2
實驗名稱: Java面向對象程式設計
實驗目的與要求:
1. 初步掌握單元測試和TDD
2. 了解并掌握面向對象三要素:封裝、繼承、多态
3. 初步掌握UML模組化
4. 熟悉S.O.L.I.D原則
5. 了解設計模式
實驗要求
1.沒有Linux基礎的同學建議先學習《Linux基礎入門(新版)》《Vim編輯器》 課程
2.完成實驗、撰寫實驗報告,實驗報告以部落格方式發表在部落格園,注意實驗報告重點是運作結果,遇到的問題(工具查找,安裝,使用,程式的編輯,調試,運作等)、解決辦法(空洞的方法如“查網絡”、“問同學”、“看書”等一律得0分)以及分析(從中可以得到什麼啟示,有什麼收獲,教訓等)。報告可以參考範飛龍老師的指導
3. 嚴禁抄襲,有該行為者實驗成績歸零,并附加其他懲罰措施。
實驗儀器:
名稱 | 型号 | 數量 |
PC | 1 | |
實驗内容、步驟與體會(附紙):
(一)單元測試
(1) 三種代碼
程式設計是智力活動,不是打字,程式設計前要把幹什麼、如何幹想清楚才能把程式寫對、寫好。與目前不少同學一說程式設計就打開編輯器寫代碼不同,我希望同學們養成一個習慣,當你們想用程式解決問題時,要會寫三種碼:
- 僞代碼
- 産品代碼
- 測試代碼
我們通過一個例子說明如何寫這三種代碼。
需求:我們要在一個 MyUtil
類中解決一個百分制成績轉成“優、良、中、及格、不及格”五級制成績的功能。
我們設計了一個
測試用例(Test Case)
,
測試用例
是為某個特殊目标而編制的一組測試輸入、執行條件以及預期結果,以便測試某個程式路徑或核實是否滿足某個特定需求。這裡我們的測試輸入是“50”,預期結果是“不及格”。在Eclipse中運作結果如下,測試結果符合預期:
隻有一組輸入的測試是不充分的,我們把一般情況都測試一下,
在Eclipse中運作結果如下,測試結果符合預期:
我們不能隻測試正常情況,下面看看異常情況如何,比如輸入為負分或大于100的成績,
運作程式發現負分時與期望不一緻,終于找到了一個bug,原因是判斷不及格時沒有要求成績大于零。我們修改
MyUtil.java
,增加對負分的判斷
再次運作測試,測試結果符合預期,如下圖所示:
測試夠了嗎?還不夠,一般代碼在邊界處最容易出錯,我們還沒有測試邊界情況,我們對輸入為“0,60,70,80,90,100”這些邊界情況進行測試,
測試結果如下:
我們發現邊界情況中輸入100時有一個Bug。我們修改
MyUtil.java
,把判斷優秀的條件中包含輸入為100的情況,
這時測試都符合預期了,我們把
MyUtil.java
提供給别人使用時,心裡比較有底氣了。那如何保證單元測度是充分的呢?我們的一般要求是
測試代碼
要比
産品代碼
多。如何寫測試,《單元測試之道》提出了
Right-BICEP
的方法,大家可以參考一下。
軟體是由多人合作完成的,不同人員的工作互相有依賴關系。軟體的很多錯誤都來源于程式員對子產品功能的誤解、疏忽或不了解子產品的變化。如何能讓自己負責的子產品功能定義盡量明确,子產品内部的改變不會影響其他子產品,而且子產品的品質能得到穩定的、量化的保證?單元測試就是一個很有效的解決方案
(2) TDD(Test Driven Devlopment, 測試驅動開發)
先寫
測試代碼
,然後再寫
産品代碼
的開發方法叫“測試驅動開發”(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驅動項目開發,有興趣的可以參考。
(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的核心和起源。
封裝實際上使用方法(method)将類的資料隐藏起來,控制使用者對類的修改和通路資料的程度,進而帶來子產品化(Modularity)和資訊隐藏(Information hiding)的好處;接口(interface)是封裝的準确描述手段。
- +表示public
- #表示 protected
- -表示 private
繼承指一個類的定義可以基于另外一個已經存在的類,即子類基于父類,進而實作父類代碼的重用。既存類稱作基類、超類、父類(base class、super class、parent class),新類稱作派生類、繼承類、子類(derived class、inherited class、child class)。繼承關系表達了”Is a kind of“的關系,稱為“ISA”關系。繼承的關鍵在于确認子類為父類的一個特殊類型
。繼承是實作軟體可重用的根基,是提高軟體系統的可擴充性與可維護性的主要途徑。
如上面所示,以封裝為基礎,繼承可以實作代碼複用,需要注意的是,繼承更重要的作用是實作多态。
面向對象中允許不同類的對象對同一消息做出響應,即同一消息可以根據發送對象的不同而采用多種不同的行為方式,我們稱此現象為多态性。Java中,多态是指不同的類對象調用同一個簽名的成員方法時将執行不同代碼的現象。多态是面向對象程式設計的靈活性和可擴充性的基礎。
(三)設計模式初步
(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)面向接口程式設計。
-
(2)模式與設計模式
模式是某外在環境(Context) 下﹐對特定問題(Problem)的慣用解決之道(Solution)。模式必須使得問題明晰,闡明為什麼用它來求解問題,以及在什麼情況下有用,什麼情況下不能起作用,每個模式因其重複性進而可被複用,本身有自己的名字,有可傳授性,能移植到不同情景下。模式可以看作對一個問題可複用的專家級解決方法。 計算機科學中有很多模式:
- GRASP模式
- 分析模式
- 軟體體系結構模式
- 設計模式:建立型,結構型,行為型
- 管理模式: The Manager Pool 實作模式
- 界面設計互動模式
- …
-
(3)設計模式實示例
設計模式(design pattern)提供一個用于細化軟體系統的子系統或元件,或它們之間的關系圖,它描述通信元件的公共再現結構,通信元件可以解決特定語境中的一個設計問題。
我們看到通過增加了一層抽象層使代碼符合了OCP原則。代碼有良好的可擴充性、可維護性,代價是代碼多了,效率變低下了。
設計模式初學者容易過度使用它們,導緻過度設計,也就是說,遵守DRY和OCP當然好,但會出現YAGNI(You aren't gonna need it, 你不會需要它)問題。
DRY原則和YAGNI原則并非完全相容。前者追求"抽象化",要求找到通用的解決方法;後者追求"快和省",意味着不要把精力放在抽象化上面,因為很可能"你不會需要它"。怎麼平衡呢?有一個Rule of three (三次原則):第一次用到某個功能時,你寫一個特定的解決方法;第二次又用到的時候,你拷貝上一次的代碼(違反了DRY);第三次出現的時候,你才着手"抽象化",寫出通用的解決方法。
設計模式學習先參考一下《深入淺出設計模式》,這本書可讀性非常好。
除SOLID原則外還有很多其它的面向對象原則。如:
- "組合替代繼承":這是說相對于繼承,要更傾向于使用組合;
- "笛米特法則":這是說"你的類對其它類知道的越少越好";
- "共同封閉原則":這是說"相關類應該打包在一起";
- "穩定抽象原則":這是說"類越穩定,越應該由抽象類組成";
當然,這些原則并不是孤立存在的,而是緊密聯系的,遵循一個原則的同時也就遵循了另外一個或多個原則;反之,違反了其中一個原則也很可能同時就違反了另外一個或多個原則。 設計模式是這些原則在一些特定場景的應用結果。是以,可以把設計模式看作"架構",把OOD原則看作"規範"。 在學習設計模式的過程中,要經常性的反思,這個設計模式展現了面向對象設計原則中的哪個或哪一些原則。
(四)練習
1使用TDD的方式設計關實作複數類Complex。
僞代碼:
無輸入 --- 則複數的實部為0,虛部為0
僅輸入實部 --- 複數的實部為所輸入的實部,虛部為0
僅輸入虛部 --- 複數的實部為0,虛部為所輸入的虛部
實部虛部都輸入 --- 複數的實部與虛部對應相應輸入的實部虛部。
加法 --- 複數的實部與實部相加,虛部與虛部相加 ,即輸出p1.rePart+p2.rePart,p1.imPart+p2.imPart
減法 --- 複數的實部與實部相減,虛部與虛部相減 , 即輸出 p1.rePart-p2.rePart,p1.imPart-p2.imPart
産品代碼:
package Exercise;
public class Complex
{
double rePart,imPart;
Complex()
this.rePart=0;
this.imPart=0;
}
Complex(double rePart)
this.rePart=rePart;
Complex(double rePart,double imPart)
this.imPart=imPart;
Complex Jia(Complex p1,Complex p2)
Complex p =new Complex(p1.rePart+p2.rePart,p1.imPart+p2.imPart);
return p;
Complex Jian(Complex p1,Complex p2)
Complex p =new Complex(p1.rePart-p2.rePart,p1.imPart-p2.imPart);
return p;
void Print()
System.out.println("複數的值為:");
if(this.imPart!=0)
System.out.println(this.rePart+"+"+this.imPart+"i");
else System.out.println(this.rePart);
}
測試代碼:
public class Test
public static void main(String[] args)
Complex c=new Complex();
Complex c1=new Complex(2,7);
Complex c2=new Complex(5,2);
c1.Print();
c2.Print();
System.out.println("這兩複數和為:");
System.out.println((c.Jia(c1, c2).rePart+"+"+c.Jia(c1, c2).imPart+"i").toString());
System.out.println("這兩複數差為:");
System.out.println(c.Jian(c1, c2).rePart+"+"+c.Jian(c1, c2).imPart+"i");
2.實驗報告中統計自己的PSP(Personal Software Process)時
步驟 | 耗時 | 百分比 |
需求分析 | 5 | 6.25% |
設計 | 10 | 12.5% |
代碼實作 | 50 | 62.5% |
測試 | ||
分析總結 | 6.25% |
3. 實作要有僞代碼,産品代碼,測試代碼。
4.總結單元測試的好處
1.加快項目程序,并且是程式的設計更加完善。
2.通過簡單的事務復原功能在生産環境上做基于真實資料的測試而不用擔心會産生不必要的資料。
3. 讓自己負責的子產品功能定義明确,子產品與子產品之間内部的改變不會産生影響。