北京電子科技學院(BESTI)
實?????驗????報?????告
課程:Java程序設計 班級:1352??姓名:潘俊洋? 學號:20135230
成績:???????????? 指導教師:婁嘉鵬 ?????實驗日期:2015.5.4
實驗密級:???????? 預習程度:??????? ?實驗時間:
儀器組次:?? ???????必修/選修:??????? ??? 實驗序號:2
實驗名稱: ? ? ? ?Java面向對象程序設計
?
??
實驗儀器:
名稱 | 型號 | 數量 |
PC | ? | 1 |
? | ? | ? ? |
實驗內容
1. 初步掌握單元測試和TDD
2. 理解并掌握面向對象三要素:封裝、繼承、多態
3. 初步掌握UML建模
4. 熟悉S.O.L.I.D原則
5. 了解設計模式
實驗要求
1.沒有Linux基礎的同學建議先學習《Linux基礎入門(新版)》《Vim編輯器》 課程
2.完成實驗、撰寫實驗報告,實驗報告以博客方式發表在博客園,注意實驗報告重點是運行結果,遇到的問題(工具查找,安裝,使用,程序的編輯,調試,運行等)、解決辦法(空洞的方法如“查網絡”、“問同學”、“看書”等一律得0分)以及分析(從中可以得到什么啟示,有什么收獲,教訓等)。報告可以參考范飛龍老師的指導
3. 嚴禁抄襲,有該行為者實驗成績歸零,并附加其他懲罰措施。
4.?請大家先在實驗樓中的~/Code
目錄中用自己的學號建立一個目錄,代碼和UML圖要放到這個目錄中,截圖中沒有學號的會要求重做,然后跟著下面的步驟練習。
實驗步驟
(一)單元測試
編程是智力活動,不是打字,編程前要把干什么、如何干想清楚才能把程序寫對、寫好。與目前不少同學一說編程就打開編輯器寫代碼不同,我希望同學們養成一個習慣,當你們想用程序解決問題時,要會寫三種碼:
- 偽代碼
- 產品代碼
- 測試代碼
?
我們先寫偽代碼
,偽代碼可以用漢語寫,推薦大家用英語寫,偽代碼與具體編程語言無關,不要寫與具體編程語言語法相關的語句(如用malloc分配內存
,這樣只能用C語言編程了),偽代碼
從意圖層面來解決問題,最終,偽代碼
是產品代碼
最自然的、最好的注釋。想用編程來解決問題,首先要用偽代碼
表明自己想明白了。 有了偽代碼
,我們用特定編程語言翻譯一下,就是可用的產品代碼
了。產品代碼
寫完了,如果別人要使用這個代碼,把MyUtil.java
拷給他就可以了。但是作為負責任的你,肯定會擔心自己的程序會有Bug。如果別人用自己的代碼發現一堆Bugs,那多沒面子!怎么辦?寫了產品代碼
,我們還要寫測試代碼
,證明自己的代碼沒有問題。Java編程時,程序員對類實現的測試叫單元測試
。類XXXX
的單元測試
,我們一般寫建一個XXXXTest
的類,針對MyUtil
類我們寫一個MyUtilTest.java
的測試模塊。
這時測試都符合預期了,我們把MyUtil.java
提供給別人使用時,心里比較有底氣了。那如何保證單元測度是充分的呢?我們的一般要求是測試代碼
要比產品代碼
多。如何寫測試,《單元測試之道》提出了Right-BICEP
的方法,大家可以參考一下。 軟件是由多人合作完成的,不同人員的工作相互有依賴關系。軟件的很多錯誤都來源于程序員對模塊功能的誤解、疏忽或不了解模塊的變化。如何能讓自己負責的模塊功能定義盡量明確,模塊內部的改變不會影響其他模塊,而且模塊的質量能得到穩定的、量化的保證?單元測試就是一個很有效的解決方案。
(2) TDD(Test Driven Devlopment, 測試驅動開發)
我們先寫產品代碼
,然后再寫測試代碼
,通過測試發現了一些Bugs,提高了代碼質量。這有問題嗎?軟件開發從建筑中吸取了很多營養
工人是“先把墻砌好的,再用繩子測一下墻平不平,直不直,如果不平或不直拆了重砌”,還是“先用繩子給出平和直的標準,然后靠著繩子砌墻,從而保證了墻砌出來就是又平又直的”呢?答案是不言而喻的了。 拿編程做對比,我們是該“先寫產品代碼
,然后再寫測試代碼
,通過測試發現了一些Bugs,修改代碼”,還是該“先寫測試代碼
,然后再寫產品代碼
,從而寫出來的代碼就是正確的”呢?當然先寫測試代碼了。這種先寫測試代碼
,然后再寫產品代碼
的開發方法叫“測試驅動開發”(TDD)。TDD的一般步驟如下:
- 明確當前要完成的功能,記錄成一個測試列表
- 快速完成編寫針對此功能的測試用例
- 測試代碼編譯不通過(沒產品代碼呢)
- 編寫產品代碼
- 測試通過
- 對代碼進行重構,并保證測試通過(重構下次實驗練習)
- 循環完成所有功能的開發
基于TDD,我們不會出現過度設計的情況,需求通過測試用例表達出來了,我們的產品代碼
只要讓測試通過就可以了。
?
需求:我們要在一個MyUtil
類中解決一個百分制成績轉成“優、良、中、及格、不及格”五級制成績的功能。
?
圖中的紅叉說明代碼存在語法錯誤,原因很簡單,MyUtil
類還不存在,類中的percentage2fivegrade方法也不存在,我們在TDDDemo
的src
目錄中新建一個MyUtil
的類,并實現percentage2fivegrade方法
測試結果出現了一個綠條(green bar),說明測試通過了。TDD的目標是"Clean Code That Works",TDD的slogan是"Keep the bar green, to Keep the code clean"
?
?
(二)面向對象三要素
(1)抽象
抽象一詞的本意是指人在認識思維活動中對事物表象因素的舍棄和對本質因素的抽取。抽象是人類認識復雜事物和現象時經常使用的思維工具,抽象思維能力在程序設計中非常重要,"去粗取精、化繁為簡、由表及里、異中求同"的抽象能力很大程度上決定了程序員的程序設計能力。
抽象就是抽出事物的本質特征而暫時不考慮他們的細節。對于復雜系統問題人們借助分層次抽象的方法進行問題求解;在抽象的最高層,可以使用問題環境的語言,以概括的方式敘述問題的解。在抽象的較低層,則采用過程化的方式進行描述。在描述問題解時,使用面向問題和面向實現的術語。 程序設計中,抽象包括兩個方面,一是過程抽象,二是數據抽象。
(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中用類進行封裝
我們可以用UML中的類圖來描述類Dog
,首先我們在實驗樓的環境中打開shell,在命令行中輸入umbrello
,打開UML建模軟件umbrello
我們可以看到,在UML 里,一個類的屬性能顯示它的名字,類型,初始化值,屬性也可以顯示private,public,protected。 類的方法能顯示它們的方法名,參數,返回類型,以及方法的private,public,protected屬性。其中:
- +表示public
- #表示 protected
- -表示 private
使用UML可以讓我們不必關注細節。同樣,我們可以建立一個Cat
類
?
請大家注意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
類中覆蓋這個方法
?
(三)設計模式初步
(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
,如下圖所示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
- 子類必須可以被其基類所代
- 使用指向基類的指針或引用的函數,必須能夠在不知道具體派生類對象類型的情況下使用它請大家想一想,
Square
類為何不能繼承Rectangle
類,在數學上好像是沒有什么問題的呀。LSP
告訴大家的一點是不要濫用繼承,LSP
原則清楚地指出,OOD中“ISA關系”是就行為功能而言。行為功能(behavior)不是內在的、私有的,而是外在、公開的,是客戶程序所依賴的接口。(2)模式與設計模式
模式是某外在環境(Context) 下﹐對特定問題(Problem)的慣用解決之道(Solution)。模式必須使得問題明晰,闡明為什么用它來求解問題,以及在什么情況下有用,什么情況下不能起作用,每個模式因其重復性從而可被復用,本身有自己的名字,有可傳授性,能移植到不同情景下。模式可以看作對一個問題可復用的專家級解決方法。 計算機科學中有很多模式:
- GRASP模式
- 分析模式
- 軟件體系結構模式
- 設計模式:創建型,結構型,行為型
- 管理模式: The Manager Pool 實現模式
- 界面設計交互模式
- …
這里面最重要的是設計模式,在面向對象中設計模式的地位可以和面向過程編程中的數據結構的地位相當。
(3)設計模式實示例
設計模式(design pattern)提供一個用于細化軟件系統的子系統或組件,或它們之間的關系圖,它描述通信組件的公共再現結構,通信組件可以解決特定語境中的一個設計問題。如圖,隨著系統中對象的數量增多,對象之間的交互成指數增長,設計模式可以幫我們以最好的方式來設計系統。設計模式背后是抽象和SOLID原則。 設計模式有四個基本要素:
- Pattern name:描述模式,便于交流,存檔
- Problem:描述何處應用該模式
- Solution:描述一個設計的組成元素,不針對特例
- Consequence:應用該模式的結果和權衡(trade-offs)
-
我們設計了一個文檔系統,如下圖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();
}
} -
抽象工廠模式應用如下:
對應代碼如下:
// 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。
-
- 偽代碼
-
復數類ComplexNumber的方法:
ComplexNumber():構造函數,將實部和虛部都置為0
ComplexNumber(double r,double i):構造函數,通過參數r,i初始化實部和虛部
complexAdd(ComplexNumber c):當前復數對象與參數對象相加,所得的結果也是復數,返回給此方法調用者
complexMinus(ComplexNumber c):當前復數對象與參數對象相減,所得的結果也是復數,返回給此方法調用者
complexMulti(ComplexNumber c):當前復數對象與參數對象相乘,所得的結果也是復數,返回給此方法調用者 - 測試代碼
-
public class Test
{
public static void main(String args[])
{
ComplexNumber cNumber_1 =
new ComplexNumber(3,-5);
ComplexNumber
cNumber_2 =
new ComplexNumber(2,2);
double d = 10.0;
System.out.println(cNumber_1.toString() + " 加 "
+ cNumber_2.toString() + "
等于 "
+ cNumber_1.complexAdd(cNumber_2).toString());
System.out.println(cNumber_1.toString() + " 加 "
+ d + " 等于 "
+
cNumber_1.complexAdd(d).toString());
System.out.println();System.out.println(cNumber_1.toString() + " 減 "
+ cNumber_2.toString() + "
等于 "
+ cNumber_1.complexMinus(cNumber_2).toString());
System.out.println(cNumber_1.toString() + " 減 "
+ d + " 等于 "
+
cNumber_1.complexMinus(d).toString());
System.out.println();System.out.println(cNumber_1.toString() + " 乘 "
+ cNumber_2.toString() + "
等于 "
+ cNumber_1.complexMulti(cNumber_2).toString());
System.out.println(cNumber_1.toString() + " 乘 "
+ d + " 等于 "
+
cNumber_1.complexMulti(d).toString());
System.out.println();System.out.println(cNumber_1.toString() + " 除 "
+ cNumber_2.toString() +
"等于"
+ cNumber_1.complexDivision(cNumber_2).toString());
System.out.println(cNumber_1.toString() + " 除 "
+ d + " 等于 "
+
cNumber_1.complexDivision(d).toString());
}
}
class ComplexNumber
{
//復數運算方法
//加
ComplexNumber complexAdd(ComplexNumber
c)
{
return new ComplexNumber(
this.m_dRealPart +
c.getRealPart(),
this.m_dImaginPart +
c.getImaginaryPart());
}
ComplexNumber complexAdd(double c)
{
return new ComplexNumber(
this.m_dRealPart + c,
this.m_dImaginPart);
}
//減
ComplexNumber complexMinus(ComplexNumber
c)
{
return new ComplexNumber(
this.m_dRealPart -
c.getRealPart(),
this.m_dImaginPart -
c.getImaginaryPart());
}
ComplexNumber complexMinus(double c)
{
return new ComplexNumber(
this.m_dRealPart - c,
this.m_dImaginPart);
}
//乘
ComplexNumber complexMulti(ComplexNumber
c)
{
return new ComplexNumber(
this.m_dRealPart * c.getRealPart()- this.m_dImaginPart * c.getImaginaryPart(),
this.m_dRealPart *
c.getImaginaryPart()
+ this.m_dImaginPart *
c.getRealPart());
}
ComplexNumber complexMulti(double c)
{
return
new ComplexNumber(
this.m_dRealPart * c, this.m_dImaginPart *
c);
}
//除
ComplexNumber complexDivision(ComplexNumber c){
return
new ComplexNumber((this.m_dRealPart*c.getRealPart()
+this.m_dImaginPart*c.getImaginaryPart())
/(c.getRealPart()*c.getRealPart()+c.getImaginaryPart()*c.getImaginaryPart()),(this.m_dImaginPart*c.getRealPart()
-this.m_dRealPart*c.getImaginaryPart())
/(c.getRealPart()*c.getRealPart()+c.getImaginaryPart()*c.getImaginaryPart()));}
ComplexNumber complexDivision(double c){
return new
ComplexNumber(this.m_dRealPart/c,this.m_dImaginPart/c);
}
//toString()
public String toString()
{
return "(" + m_dRealPart + " + "
+
m_dImaginPart + " i" + ")";
}
}- 產品代碼
-
public class Test
{
public static void main(String args[])
{
ComplexNumber cNumber_1 =
new ComplexNumber(3,-5);
ComplexNumber
cNumber_2 =
new ComplexNumber(2,2);
double d = 10.0;
System.out.println(cNumber_1.toString() + " 加 "
+ cNumber_2.toString() + "
等于 "
+ cNumber_1.complexAdd(cNumber_2).toString());
System.out.println(cNumber_1.toString() + " 加 "
+ d + " 等于 "
+
cNumber_1.complexAdd(d).toString());
System.out.println();System.out.println(cNumber_1.toString() + " 減 "
+ cNumber_2.toString() + "
等于 "
+ cNumber_1.complexMinus(cNumber_2).toString());
System.out.println(cNumber_1.toString() + " 減 "
+ d + " 等于 "
+
cNumber_1.complexMinus(d).toString());
System.out.println();System.out.println(cNumber_1.toString() + " 乘 "
+ cNumber_2.toString() + "
等于 "
+ cNumber_1.complexMulti(cNumber_2).toString());
System.out.println(cNumber_1.toString() + " 乘 "
+ d + " 等于 "
+
cNumber_1.complexMulti(d).toString());
System.out.println();System.out.println(cNumber_1.toString() + " 除 "
+ cNumber_2.toString() +
"等于"
+ cNumber_1.complexDivision(cNumber_2).toString());
System.out.println(cNumber_1.toString() + " 除 "
+ d + " 等于 "
+
cNumber_1.complexDivision(d).toString());
}
}
//復數運算方法
//加
ComplexNumber complexAdd(ComplexNumber
c)
{
return new ComplexNumber(
this.m_dRealPart +
c.getRealPart(),
this.m_dImaginPart +
c.getImaginaryPart());
}
ComplexNumber complexAdd(double c)
{
return new ComplexNumber(
this.m_dRealPart + c,
this.m_dImaginPart);
}
//減
ComplexNumber complexMinus(ComplexNumber
c)
{
return new ComplexNumber(
this.m_dRealPart -
c.getRealPart(),
this.m_dImaginPart -
c.getImaginaryPart());
}
ComplexNumber complexMinus(double c)
{
return new ComplexNumber(
this.m_dRealPart - c,
this.m_dImaginPart);
}
//乘
ComplexNumber complexMulti(ComplexNumber
c)
{
return new ComplexNumber(
this.m_dRealPart * c.getRealPart()- this.m_dImaginPart * c.getImaginaryPart(),
this.m_dRealPart *
c.getImaginaryPart()
+ this.m_dImaginPart *
c.getRealPart());
}
ComplexNumber complexMulti(double c)
{
return
new ComplexNumber(
this.m_dRealPart * c, this.m_dImaginPart *
c);
}
//除
ComplexNumber complexDivision(ComplexNumber c){
return
new ComplexNumber((this.m_dRealPart*c.getRealPart()
+this.m_dImaginPart*c.getImaginaryPart())
/(c.getRealPart()*c.getRealPart()+c.getImaginaryPart()*c.getImaginaryPart()),(this.m_dImaginPart*c.getRealPart()
-this.m_dRealPart*c.getImaginaryPart())
/(c.getRealPart()*c.getRealPart()+c.getImaginaryPart()*c.getImaginaryPart()));}
ComplexNumber complexDivision(double c){
return new
ComplexNumber(this.m_dRealPart/c,this.m_dImaginPart/c);
}
//toString()
public String toString()
{
return "(" + m_dRealPart + " + "
+
m_dImaginPart + " i" + ")";
}
} - 單元測試的好處
-
熟悉單元測試技術,了解相關的基本原理;
掌握代碼,積累代碼編寫經驗,積累調試經驗,積累分析問題、解決問題的經驗;
訓練動手能力,單元測試代碼不是業務代碼,開發、維護過程中不需要特別關注質量要求,底限是達到驗證業務代碼邏輯性的目的,因而比修改代碼要省心、省事;
不需要準備項目運行環境,單元測試代碼在運行時的外部依賴比較少,執行驗證、調試代碼的代價會很低;
降低新手程序員進入項目的門檻,有助于積累信心。
項目過程中寫單元測試的好處
一邊寫代碼,一邊檢查代碼中的小錯誤或者小疏忽,提前解決代碼中可能存在的筆誤;
為了讓單元測試代碼更好寫,需要花點心思在思考類和方法的結構,好處是可以有效的提升代碼的可測試性,否則設計和結構不理想時,單元測試代碼寫作時也會比較麻煩,需要打很多樁;
在集成測試前,有機會做驗證模塊內部的邏輯正確性,避免在聯調時花費過多的時間來解決小問題,提高聯調的效率;
單元測試代碼對運行環境依賴小,不需要特別準備復雜的環境,外部模塊的行為和表現是可控制的,易于模擬異常場景;
記錄已實現的特性,代碼即是最好的文檔,設計良好的單元測試代碼有助于后續維護,降低后期修改引入問題的可能性。
維護或者學習代碼過程中寫單元測試的好處
熟悉已有代碼的邏輯,變量的變化方式,代碼的運行軌跡,提高學習代碼的效率,降低通過看代碼來學習代碼的負擔;
幫助重構,提高重構的成功率;
輔助問題分析,遇到問題時,可以借助分析單元測試代碼來了解模塊的一般行為
-