實驗二:Java面向物件程式設計
實驗二 Java面向物件程式設計
目錄
[一、單元測試和TDD](#first)
[任務一:實現百分制成績轉成“優、良、中、及格、不及格”五級製成績的功能](#firstTask)
[任務二:以TDD的方式研究學習StringBuffer](#SecondTask)
[二、面向物件三要素:封裝、繼承、多型](#Second)
[任務三:使用StarUML對實驗二中的程式碼進行建模](#ThirdTask)
[三、設計模式](#third)
[任務四:對MyDoc類進行擴充,讓其支援Long類,初步理解設計模式](#FourthTask)
[附:練習](#exercise)
[任務五:以TDD的方式開發一個複數類](#FifthTask)
[四、實驗過程中遇到的問題及解決:](#prob)
[五、實驗體會與總結](#summary)
[六、參考資料](#reference)
一、單元測試和TDD
用程式解決問題時,要學會寫以下三種程式碼:- 虛擬碼;
- 產品程式碼;
- 測試程式碼;
正確的順序是:①先寫虛擬碼,通過虛擬碼來理清程式設計的思路;②然後寫“測試程式碼”,通過測試程式碼來保證實現產品的預期功能;③最後寫“產品程式碼”,通過寫產品程式碼來實現預期功能,即:程式設計實現。這種開發方法叫做“測試驅動開發”(TDD)。TDD 的一般步驟如下: - 明確當前要完成的功能,記錄成一個測試列表;
- 快速完成編寫針對此功能的測試用例;
- 測試程式碼編譯不通過,因為此時還沒有編寫產品程式碼;
- 編寫產品程式碼;
- 測試通過;
- 對程式碼進行重構,並且保證測試通過(重構下次練習);
- 迴圈以上操作步驟,直至完成所有功能的開發。
基於TDD,可以有效避免過度開發的現象,因為我們只需要讓測試通過即可。
回到目錄
任務一:實現百分制成績轉成“優、良、中、及格、不及格”五級製成績的功能
以這個任務為例,我們來對TDD方法進行一次小小的實踐。
首先要明白自己的程式需要進行哪些操作?要實現什麼目標,即:要實現什麼功能?虛擬碼可以使我理清思路。
百分制轉五分制:
如果成績小於60,轉成“不及格”
如果成績在60與70之間,轉成“及格”
如果成績在70與80之間,轉成“中等”
如果成績在80與90之間,轉成“良好”
如果成績在90與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 "錯誤";
}
}
產品程式碼是為使用者提供的,為了保證產品程式碼的正確性,我們需要對自己的程式來進行測試,測試時要儘量去考慮所有可能的情況,來判斷結果是否合乎要求。即:我們需要去編寫測試程式碼。
根據我現在的理解,測試程式碼就是用if 語句在加上在各個對應的if語句中去呼叫System.out.println()
,來判斷輸出是否合乎預期,所以測試程式碼如下:測試程式碼的特點是:①if和elseif中放著的是錯誤情況的條件,而結果正確是隻放在最後的else分支中;②不僅僅要編寫正確情況下的測試程式碼,也得編寫錯誤情況下的測試程式碼,還得編寫邊界情況對應的測試程式碼,這三個情況必不可少!具體見以下程式碼。
public class MyUtilTest {
public static void main(String[] args) {
//測試正常情況
if(MyUtil.percentage2fivegrade(55) != "不及格")
System.out.println("test failed!In right situation.");
else if(MyUtil.percentage2fivegrade(65) != "及格")
System.out.println("test failed!In right situation.");
else if(MyUtil.percentage2fivegrade(75) != "中等")
System.out.println("test failed!In right situation.");
else if(MyUtil.percentage2fivegrade(85) != "良好")
System.out.println("test failed!In right situation.");
else if(MyUtil.percentage2fivegrade(95) != "優秀")
System.out.println("test failed!In right situation.");
else
System.out.println("test passed!In right situation.");
//測試出錯情況
if(MyUtil.percentage2fivegrade(-10) != "錯誤")
System.out.println("test failed 1! In error situation.");
else if(MyUtil.percentage2fivegrade(115) != "錯誤")
System.out.println("test failed 2! In error situation.");
else
System.out.println("test passed!In error situation.");
//測試邊界情況
if(MyUtil.percentage2fivegrade(0) != "不及格")
System.out.println("test failed 1!In border situation.");
else if(MyUtil.percentage2fivegrade(60) != "及格")
System.out.println("test failed 2!In border situation.");
else if(MyUtil.percentage2fivegrade(70) != "中等")
System.out.println("test failed 3!In border situation.");
else if(MyUtil.percentage2fivegrade(80) != "良好")
System.out.println("test failed 4!In border situation.");
else if(MyUtil.percentage2fivegrade(90) != "優秀")
System.out.println("test failed 5!In border situation.");
else if(MyUtil.percentage2fivegrade(100) != "優秀")
System.out.println("test failed 6!In border situation.");
else
System.out.println("test passed!In border situation.");
}
}
建築工人人是“先把牆砌好的,再用繩子測一下牆平不平,直不直,如果不平或不直拆了重砌”,還是“先用繩子給出平和直的標準,然後靠著繩子砌牆,從而保證了牆砌出來就是又平又直的”呢?答案是不言而喻的了。
拿程式設計做對比,我們是該“先寫產品程式碼,然後再寫測試程式碼,通過測試發現了一些Bugs,修改程式碼”,還是該“先寫測試程式碼,然後再寫產品程式碼,從而寫出來的程式碼就是正確的”呢?當然先寫測試程式碼了。這種先寫測試程式碼,然後再寫產品程式碼的開發方法叫“測試驅動開發”(TDD)。TDD的一般步驟如下:
①明確當前要完成的功能,記錄成一個測試列表
②快速完成編寫針對此功能的測試用例
③測試程式碼編譯不通過(沒產品程式碼呢)
④編寫產品程式碼
⑤測試通過
⑥對程式碼進行重構,並保證測試通過(重構下次實驗練習)
⑦迴圈完成所有功能的開發
於TDD,我們不會出現過度設計的情況,需求通過測試用例表達出來了,我們的產品程式碼只要讓測試通過就可以了。
回到目錄
任務二:以TDD的方式研究學習StringBuffer
這個任務主要鍛鍊我們自己寫JUnit測試用例的能力。給出的程式如下:
public static void main(String [] args){
StringBuffer buffer = new StringBuffer();
buffer.append('S');
buffer.append("tringBuffer");
System.out.println(buffer.charAt(1));
System.out.println(buffer.capacity());
System.out.println(buffer.length());
System.out.println(buffer.indexOf("tring"));
System.out.println("buffer = " + buffer.toString());
首先,需要對這個程式進行改寫,寫成上面的產品程式碼那種型別的(有返回值的),以便於進行測試。
那麼如何來進行改寫呢,參考狄同學的部落格可知,思路就是:先思考哪些方法需要測試?
有四個:charAt()
、capacity()
、length()
、indexOf()
。明確了哪些方法需要測試之後,接下來就開始改寫產品程式碼,即:在產品程式碼中,分別為這四個方法來加上各自的返回值,這樣就可以與測試程式碼中的斷言來進行比較了。修改後的產品程式碼如下:
public class StringBufferDemo{
StringBuffer buffer = new StringBuffer();
public StringBufferDemo(StringBuffer buffer){
this.buffer = buffer;
}
public Character charAt(int i){
return buffer.charAt(i);
}
public int capacity(){
return buffer.capacity();
}
public int length(){
return buffer.length();
}
public int indexOf(String buf) {
return buffer.indexOf(buf);
}
}
從程式碼上我們可以看到,我們想要測試的方法都有一個返回值,這個返回值是通過呼叫我們想要測試的方法得到的。測試程式碼如下所示:
public class StringBufferDemoTest {
StringBuffer a = new StringBuffer("StringBuffer");// Test a string which has 12 character
StringBuffer b = new StringBuffer("StringBufferStringBuffer");// Test a string which has 24 character
StringBuffer c = new StringBuffer("StringBufferStringBufferStringBuffer");// Test a string which has 36 character
@Test
public void testcharAt() {
assertEquals('S',a.charAt(0));
assertEquals('g',b.charAt(5));
assertEquals('r',c.charAt(11));
}
@Test
public void testcapacity() {
assertEquals(28,a.capacity());
assertEquals(40,b.capacity());
assertEquals(52,c.capacity());
}
@Test
public void testlength() {
assertEquals(12,a.length());
assertEquals(24,b.length());
assertEquals(36,c.length());
}
@Test
public void testindexOf() {
assertEquals(0,a.indexOf("Str"));
assertEquals(5,b.indexOf("gBu"));
assertEquals(10,c.indexOf("er"));
}
}
[回到目錄](#index)
二、面向物件三要素:封裝、繼承、多型
面向物件(Object-Oriented)的三要素包括:封裝、繼承、多型。面向物件的思想涉及到軟體設計開發的各個方面,如:面向物件分析(OOA)、面向物件設計(OOD)、面向物件程式設計實現(OOP)。其中:OOA根據抽象關鍵的問題域來分解問題,即:關注是什麼(what)。OOD是一種提供符號設計系統的面向物件的實現過程,用非常接近問題域術語的方法把系統構造成“現實世界”的物件,即:關注怎麼做,通過模型來實現功能規範。OOP則在設計的基礎上用程式語言如:JAVA來編碼。貫穿OOA、OOD、OOP的主線正是抽象。抽象一詞的本意是指人在認識思維活動中對事物表象因素的捨棄和對本質因素的抽取。抽象是人類認識複雜事物和現象時經常使用的思維工具,抽象思維能力在程式設計中非常重要,"去粗取精、化繁為簡、由表及裡、異中求同"的抽象能力很大程度上決定了程式設計師的程式設計能力。
抽象就是抽出事物的本質特徵而暫時不考慮他們的細節。對於複雜系統問題人們藉助分層次抽象的方法進行問題求解;在抽象的最高層,可以使用問題環境的語言,以概括的方式敘述問題的解。在抽象的較低層,則採用過程化的方式進行描述。在描述問題解時,使用面向問題和麵向實現的術語。
程式設計中,抽象包括兩個方面,一是過程抽象,二是資料抽象。程式設計的重要原則之一:DRY
任務三:使用StarUML對實驗二中的程式碼進行建模
[回到目錄](#index)
UML是一種通用的建模語言,可以非常直觀的表示出各個結構之間的關係。
三、設計模式
面向物件三要素是“封裝、繼承、多型”,任何面向物件程式語言都會在語法上支援這三要素。如何藉助抽象思維用好這三要素,特別是多型還是非常困難的,S.O.L.I.D類設計原則是一個很好的指導:
- S:SRP(Single Responsibility Principle, 單一職責原則);
- O:OCP (Open-Closed Principle, 開放-封閉原則) ;
- L:LSP (Liskov Substitusion Principle, Liskov 替換原則);
- I:ISP (Interface Segregation Principle, 介面分離原則);
- D:DIP (Dependency Inversion Principle, 依賴倒置原則)。
下面,通過具體的題目來學習設計模式。
任務四:對MyDoc類進行擴充,讓其支援Long類,初步理解設計模式
OCP 是OOD 中最重要的一個原則,要求軟體實體(類、模組、函式等)應該對擴充開放,對修改封閉。也就是說:軟體模組的行為必須是可以被擴充的,在應用需求改變或者需要滿足新的應用需求時,我們要讓模組以不同的方式工作,同時,模組的原始碼是不可被改動的,任何人都不許修改已有模組的原始碼。OCP可以用以下手段實現:(1)抽象和繼承;(2)面向介面程式設計。以下面這道題目為例,已有的支援Int型的程式碼如下:
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();
}
}
設計模式初學者容易過度使用它們,導致過度設計,也就是說,遵守DRY和OCP當然好,但會出現YAGNI(You aren't gonna need it, 你不會需要它)問題。
DRY原則和YAGNI原則並非完全相容。前者追求"抽象化",要求找到通用的解決方法;後者追求"快和省",意味著不要把精力放在抽象化上面,因為很可能"你不會需要它"。怎麼平衡呢?有一個Rule of three (三次原則):第一次用到某個功能時,你寫一個特定的解決方法;第二次又用到的時候,你拷貝上一次的程式碼(違反了DRY);第三次出現的時候,你才著手"抽象化",寫出通用的解決方法。
設計模式學習先參考一下《深入淺出設計模式》,這本書可讀性非常好。改寫後的程式碼如下:支援了Long類
我們看到,通過增加一層抽象層程式碼:Factory(),使得程式碼符號了OCP 原則,即:沒有對原始碼進行修改,在原始碼的基礎上增加抽象層程式碼,實現多型,由多型來實現需求。
回到目錄
附:練習
任務五:以TDD的方式開發一個複數類Complex
通過以上的學習,我們已經可以基本熟練的應用TDD方法了,並跟隨TDD方法的節奏設計出虛擬碼、產品程式碼以及測試程式碼了,這個任務算是對以上內容的回顧。
TDD編碼的節奏是:
- 增加測試程式碼,JUnit出現紅條,顯示不通過測試;
- 修改產品程式碼;
- JUnit出現綠條,產品程式碼通過測試,任務完成。
實驗要求如下:
// 定義屬性並生成getter,setter
double RealPart;
double ImagePart;
// 定義建構函式
public Complex()
public Complex(double R,double I)
//Override Object
public boolean equals(Object obj)
public String toString()
// 定義公有方法:加減乘除
Complex ComplexAdd(Complex a)
Complex ComplexSub(Complex a)
Complex ComplexMulti(Complex a)
Complex ComplexDiv(Complex a)
首先,我們來寫虛擬碼:
①要有屬性:RealPart以及ImagePart;
②要有方法:setter and getter;
③define the Constructor;(There are two kinds of Constructor);
④Override object: equals() and toString();
⑤Define method:add();subtract();multiple();divide();
接下來,測試程式碼如下:
接下來,產品程式碼如下:
我覺得,在實際過程中,反倒是要先寫出產品程式碼裡面的函式名,就是,先想話你要測試哪些函式,具體的函式體可以先不寫,然後再一鍵去生成JUnit測試程式碼,這樣方便一些。就是得去解決下面這個,測試函式名不規範的問題!
四、實驗過程中遇到的問題及解決:
問題一:JUnit 使用方法不太熟悉,根據網上看到的,生成JUnit測試程式碼有兩種方法,一種是直接用預設模板給你生成,一種是用點選類名前面的小燈泡的方法來生成JUnit。前一種方法在寫測試程式碼時不能呼叫assertEquals();方法,只能呼叫assert();方法。後面那種行,但是後面那種方法生成的原是測試程式碼中方法名稱又不是太符合規範,得自己主動去修改。
解決方法:??五、實驗體會與總結
這個實驗二自己做了很久很久,參考了狄同學的很多方面。自己也在參考的同時經過了思考,最後一個任務是自己的我自己完成的,經過這個實驗,感覺自己接觸到了一些很多新的有趣的概念,讓我對測試這項工作有了一個新的理解:測試是為了保證任務的完成,而且是不超額完成,並不是僅僅為了保證最終產品的正確!六、參考資料
[參考部落格]https://www.cnblogs.com/Vivian517/p/6741501.html#SAN
回到目錄