實驗二: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
回到目錄
實驗二:Java面向對象程序設計