標籤:
實驗內容
1. 初步掌握單元測試和TDD
2. 理解並掌握物件導向三要素:封裝、繼承、多態
3. 初步掌握UML建模
4. 熟悉S.O.L.I.D原則
5. 瞭解設計模式
實驗要求
1.沒有Linux基礎的同學建議先學習《Linux基礎入門(新版)》《Vim編輯器》 課程
2.完成實驗、撰寫實驗報告,實驗報告以部落格方式發表在部落格園,注意實驗報告重點是運行結果,遇到的問題(工具尋找,安裝,使用,程式的編輯,調試,運行等)、解決辦法(空洞的方法如“查網路”、“問同學”、“看書”等一律得0分)以及分析(從中可以得到什麼啟示,有什麼收穫,教訓等)。報告可以參考範飛龍老師的指導
實驗過程:
(一)單元測試
(1) 三種代碼
編程是智力活動,不是打字,編程前要把幹什麼、如何幹想清楚才能把程式寫對、寫好。與目前不少同學一說編程就開啟編輯器寫代碼不同,我希望同學們養成一個習慣,當你們想用程式解決問題時,要會寫三種碼:
虛擬碼
產品代碼
測試代碼
我們通過一個例子說明如何寫這三種代碼。
需求:我們要在一個MyUtil類中解決一個百分製成績轉成“優、良、中、及格、不及格”五級製成績的功能。
虛擬碼:
百分制轉五分制:
如果成績小於60,轉成“不及格”
如果成績在60與70之間,轉成“及格”
如果成績在70與80之間,轉成“中等”
如果成績在80與90之間,轉成“良好”
如果成績在90與100之間,轉成“優秀”
其他,轉成“錯誤”
產品代碼:
翻譯好的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 "錯誤";
}
}
測試代碼:
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!");
}
}
實驗:
(2) TDD(Test Driven Devlopment, 測試驅動開發)
這種先寫測試代碼,然後再寫產品代碼的開發方法叫“測試驅動開發”(TDD)。
TDD的一般步驟如下:
- 明確當前要完成的功能,記錄成一個測試清單
- 快速完成編寫針對此功能的測試案例
- 測試代碼編譯不通過(沒產品代碼呢)
- 編寫產品代碼
- 測試通過
- 對代碼進行重構,並保證測試通過(重構下次實驗練習)
- 迴圈完成所有功能的開發
基於TDD,我們不會出現過度設計的情況,需求通過測試案例表達出來了,我們的產品代碼只要讓測試通過就可以了。 Java中有單元測試工具JUnit來輔助進行TDD,我們用TDD的方式把前面百分制轉五分制的例子重寫一次,體會一下有測試載入器支援的開發的好處。 開啟Eclipse,單擊File->New->Java Project建立一個TDDDemo的Java項目,如:
測試結果出現了一個紅條(red bar),說明測試沒通過,紅條上面匯總了測試情況,運行了一個測試,沒有錯誤,一個測試沒通過。下面原因說的也很清楚:測試代碼第十行傳入55時,期望結果是“不及格”,代碼返回了“錯誤”,修改MyUtil.Java
測試結果出現了一個綠條(green bar),說明測試通過了。
TDD的編碼節奏是:
- 增加測試代碼,JUnit出現紅條
- 修改產品代碼
- JUnit出現綠條,任務完成
(二)物件導向三要素(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
我們可以看到,在UML 裡,一個類的屬效能顯示它的名字,類型,初始化值,屬性也可以顯示private,public,protected。 類的方法能顯示它們的方法名,參數,傳回型別,以及方法的private,public,protected屬性。其中:
- +表示public
- #表示 protected
- -表示 private
注意:UML類圖要展示類之間的靜態關係,AnimalTest類依賴Dog類和Cat類,UML中依賴用帶箭頭的直線表示。
對應的測試代碼和運行結果如所示:
請大家注意UML類圖中繼承的標記法,是用一個帶三角的直線指向父類,通過繼承,我們消除了Dog類和Cat類中的重複代碼,符合DRY的要求。
繼承指一個類的定義可以基於另外一個已經存在的類,即子類基於父類,從而實現父類代碼的重用。既存類稱作基類、超類、父類(base class、super class、parent class),新類稱作衍生類別、繼承類、子類(derived class、inherited class、child class)。繼承關係表達了”Is a kind of“的關係,稱為“ISA”關係。繼承的關鍵在於確認子類為父類的一個特殊類型
。繼承是實現軟體可重用的根基,是提高軟體系統的可擴充性與可維護性的主要途徑。
如上面所示,以封裝為基礎,繼承可以實現代碼複用,需要注意的是,繼承更重要的作用是實現多態。
物件導向中允許不同類的對象對同一訊息做出響應,即同一訊息可以根據發送對象的不同而採用多種不同的行為方式,我們稱此現象為多態性。Java中,多態是指不同的類對象調用同一個簽名的成員方法時將執行不同代碼的現象。多態是物件導向程式設計的靈活性和可擴充性的基礎。
另外,在Umbrello中UML圖是可以轉化成Java代碼的,有Java代碼也可以產生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.
- 軟體實體(類,模組,函數等)應該對擴充開放,對修改封閉。
遇到的問題及解決方案
出現的問題:在前兩個實驗時,由於網路太不給力,所以選擇在自己電腦上完成,並。
另外一大問題就是,測試代碼的編寫過程中因為不太熟悉程式的編寫,最後是自己對照著產品代碼來寫的測試代碼,所以測試代碼出現了很多問題,花費了很多時間去修改。
實驗收穫
這次實驗儘管花了很多時間,但是我也收穫了很多。首先,通過這次實驗,我對虛擬機器的使用更加熟悉,也更加適應這種實驗模式。單元測試也協助我提升了自己的能力,一步一步地引導我學會處理可能出現的種種問題,同時也教會我以後在編寫程式的時候要考慮到各種可能性,以提高代碼的安全性。
通過這次實驗,我還接觸到了很多以前沒有聽說過的知識,例如TDD,雖然陌生,處理起來比較吃力,但對我來說還是比較開眼界的。我覺得通過每一次的java實驗,不僅提高了我的學習能力,更培養了持之以恒的意識,雖然有些困難,仍然儘力去做,可能最後還是沒有結果,但是也會去努力一下。
練習1使用TDD的方式設計關實現複數類Complex。
代碼:
package shiyanlou;
//複數列印、相加、相減
public class ComplexTest {
// main方法
public static void main(String[] a) {
Complex b = new Complex(2, 5);
Complex c = new Complex(3, -4);
System.out.println(b + "+" + c + "=" + b.add(c));
System.out.println(b + "-" + c + "=" + b.minus(c));
System.out.println(b + "*" + c + "=" + b.multiply(c));
System.out.println(b + "/" + c + "=" + b.divide(c));
}
}
// Complex類
class Complex {
private double m;// 實部
private double n;// 虛部
public Complex(double m, double n) {
this.m = m;
this.n = n;
}
// add
public Complex add(Complex c) {
return new Complex(m + c.m, n + c.n);
}
// minus
public Complex minus(Complex c) {
return new Complex(m - c.m, n - c.n);
}
// multiply
public Complex multiply(Complex c) { return new Complex(m * c.m - n * c.n, m * c.n + n * c.m);
}
// divide
public Complex divide(Complex c) {
double d = Math.sqrt(c.m * c.m) + Math.sqrt(c.n * c.n);
return new Complex((m * c.m + n * c.n) / d, Math.round((m * c.n - n * c.m) / d));
}
public String toString() {
String rtr_str = "";
if (n > 0)
rtr_str = "(" + m + "+" + n + "i" + ")";
if (n == 0)
rtr_str = "(" + m + ")";
if (n < 0)
rtr_str = "(" + m + n + "i" + ")";
return rtr_str;
}
}
運行結果:
2.實驗報告中統計自己的PSP(Personal Software Process)時間
步驟 |
耗時 |
百分比 |
需求分析 |
0.5h |
10% |
設計 |
1h |
20% |
代碼實現 |
1.5h |
30% |
測試 |
1h |
20% |
分析總結 |
1h |
20% |
3.總結單元測試的好處
(1)使可以放心的修改測試用代碼而不用擔心會影響設計的測試代碼。
(2) 更容易在早期發現問題所在,問題不容易堆積,可以馬上解決。
實驗二 Java物件導向程式設計