哈工大軟體構造Lab2(2022)
(防扒小助手)
本人CSDN部落格:
本人部落格園部落格(同步CSDN):
如果對你有用的話歡迎點贊關注喲!
目錄
3.1.1 Get the code and prepare Git repository
3.1.3 Problem 2: Implement Graph
3.1.4 Problem 3: Implement generic Graph
3.1.6 Before you’re done
3.2 Re-implement the Social Network in Lab1
1、實驗目標概述
本次實驗訓練抽象資料型別(ADT)的設計、規約、測試,並使用面向物件
程式設計(OOP)技術實現 ADT。具體來說:
- 針對給定的應用問題,從問題描述中識別所需的 ADT;
- 設計 ADT 規約(pre-condition、post-condition)並評估規約的質量;
- 根據 ADT 的規約設計測試用例;
- ADT 的泛型化;
- 根據規約設計 ADT 的多種不同的實現;針對每種實現,設計其表示 (representation)、表示不變性(rep invariant)、抽象過程(abstraction
function)- 使用 OOP 實現 ADT,並判定表示不變性是否違反、各實現是否存在表示洩露(rep exposure)
- 測試 ADT 的實現並評估測試的覆蓋度;
- 使用 ADT 及其實現,為應用問題開發程式;
- 在測試程式碼中,能夠寫出 testing strategy 並據此設計測試用例。
2、實驗環境配置
(1)安裝測試用例程式碼覆蓋度外掛
經過網上查閱資料瞭解到,在IDEA中已經集成了程式碼覆蓋度外掛JaCoCo,切換方式如下:
點選Edit Configuration,彈出如下視窗:
在Code Coverage一欄點選Modify勾選紅框中的選項,則會彈出切換程式碼覆蓋度工具的選項:
通過諮詢軟體構造課程老師與助教老師,瞭解到使用IDEA自帶的程式碼覆蓋度工具即可,不需要切換到JaCoCo,因此下文程式碼覆蓋度測試均使用IDEA自帶的coverage runner進行測試。
2、GitHub Lab2倉庫的URL地址(Lab2-學號)
略。
3、實驗過程
3.1 Poetic Walks
該任務主要是通過實現一個圖的模組來練習ADT的規約設計和ADT的不同實現。
(1)完善Graph介面類,並運用泛型的思想,將String拓展為泛型L類;
(2)實現Graph介面類:以邊和點兩種方式實現介面;
(3)利用實現的Graph類,應用圖的思想,實現GraphPoet類。如果輸入的文字的兩個單詞之間存在橋接詞,則插入該橋接詞;若存在多個單一橋接詞,則選取邊權重較大者。
3.1.1 Get the code and prepare Git repository
從要求檔案中對應網址下載得到實驗程式碼,建立好project,進入目錄,開啟Git bush
依次輸入:
git init
git remote add origin [email protected]:ComputerScienceHIT/HIT-Lab2-120L022408.git
git pull origin master
git add .
git commit -m “init”
git push origin master
3.1.2 Problem 1: Test Graph <String>
測試Graph的靜態方法。
為了方便測試Graph的多種實現,在 GraphInstanceTest 中測試了例項方法。
編寫測試用例主要利用等價類劃分的思想進行測試,測試策略如下:
分別編寫覆蓋以上條件的測試用例。
執行GraphStaticTest得到測試結果如下:
3.1.3 Problem 2: Implement Graph <String>
3.1.3.1 Implement ConcreteEdgesGraph
(1)Edge類實現
定義兩個private String型別的變數source和target存放每個邊的起止點
定義一個private int型別的變數weight儲存這條邊的權重(長度)
private final String source, target;
private final int weight;
關於AF,RI和rep exposure:
① 構造器 constructor
構造方法,使用上述三個資料域宣告一個新的邊
public Edge(L source_new, L target_new, int weight_new)
{
this.source = source_new;
this.target = target_new;
this.weight = weight_new;
checkRep();
}
② 檢查表示不變數 checkRep
檢查表示不變數,其中source和target必須非空,weight必須大於0
public void checkRep()
{
assert source != null;
assert target != null;
assert weight > 0;
}
③ get方法
get_Source:返回source域
get_Target:返回target域
get_Weight:返回weight域
④ toString方法
返回一個字串表明這條邊是從哪個source到哪個target,其weight是多少。
public String toString()
{
return source.toString() + "->" + target.toString() + "\t權重為" + weight + '\n';
}
(2)ConcreteEdgesGraph實現
vertices和edges分別記錄當前graph所含有的點和邊
private final Set<String> vertices = new HashSet<>();
private final List<Edge<String>> edges = new ArrayList<>();
關於AF,RI和rep exposure:
① add方法
public boolean add(String vertex)
如果頂點不為空,新增一個頂點。如果在vertices的Set集合中成功添加了vertex,則返回true。
② Set方法
public int set(String source, String target, int weight)
輸入source,target,weight,確定一條有向邊。
具體做法:如weight!=0,移去可能已經存在的相同起始點的邊,然後加入新的邊,如weight=0,尋找可能已經存在的相同起始點的邊,刪去。
③ remove方法
public boolean remove(String vertex)
從vertices中刪去給定的vertex點,遍歷edges,尋找該vertex是否為某條邊的起點或者終點,刪去相應的邊。在使用迭代器遍歷時要使用iterator.remove方法保證安全性。
④ vertices方法
public Set<String> vertices()
返回vertices集合。注意做到safety from rep exposure ,使用Collections.unmodifiableSet()方法。
⑤ sources方法
public Map<String, Integer> sources(String target)
引數:target。根據傳入的target引數尋找以targe為終點的邊。返回一個鍵值對為(點,權重)的map。
實現:建立一個map,利用迭代器遍歷edges,如果某個edge的edge.get_Target()和傳入引數target相等,則將該邊的source和weight存入map中。
⑥ targets方法
public Map<String, Integer> targets(String source)
引數:source。根據傳入的source引數尋找以source為起點的邊。返回一個鍵值對為(點,權重)的map。
實現:建立一個map,利用迭代器遍歷edges,如果某個edge的edge.get_Source()和傳入引數source相等,則將該邊的target和weight存入map中。
⑦ 檢查表示不變數 checkRep
思路:n個點,最多構成n*(n-1)條有向邊,因此存在這種不可變的數學關係
⑧ toString方法
對每條邊呼叫toString方法,整合起來。
public String toString()
{
String s = "";
for (Edge<L> e : edges)
{
s = s + e.toString();
}
return s;
}
(3)ConcreteEdgesGraphTest測試
JUnit測試結果如下:
測
試覆蓋率:
3.1.3.2 Implement ConcreteVerticesGraph
(1)Vertex類實現
定義兩個private String型別的變數source和target存放每個邊的起止點
定義一個private int型別的變數weight儲存這條邊的權重(長度)
private final String name;
private final Map<String, Integer> sources;
private final Map<String, Integer> targets;
關於AF,RI和rep exposure:
① 構造器 constructor
構造方法,傳入引數name建立新的點。
public Vertex(String name)
{
this.name = name;
sources = new HashMap<>();
targets = new HashMap<>();
}
② 檢查表示不變數 checkRep
檢查表示不變性,各邊weight的值應該永遠大於0。
③ get方法
get_Name:返回source域
get_Sources:返回weight域
get_Target:返回Targets域
④ set方法
set_Target:為當前點新增一個target,
如果weight為0,刪去當前點的target,成功返回刪去target的weight,不存在返回0;如果weight不為0,為當前點新增一個target,長度為weight,如果該點已存在,返回舊的weight,否則返回0
set_Source:為當前點新增一個source,
如果weight為0,刪去當前點的source,成功返回刪去source的weight,不存在返回0;如果weight不為0,為當前點新增一個source,長度為weight,如果該點已存在,返回舊的weight,否則返回0
⑤ remove方法
remove_Source:刪去當前點的指定source
public int remove_Source(String source)
{
Integer weight = sources.remove(source);
return weight == null ? 0 : weight;
}
remove_Target:刪去當前點的指定target
public int remove_Target(String target)
{
Integer weight = targets.remove(target);
return weight == null ? 0 : weight;
}
⑥ toString方法
返回一個字串表明這個頂點的資訊
public String toString()
{
return String.format("Vertex %s has %d sources and %d targets", this.get_Name().toString(), this.get_Sources().size(), this.get_Targets().size());
}
(2)ConcreteVerticesGraph實現
使用如下資料型別儲存頂點的資料:
private final List<Vertex<String>> vertices = new ArrayList<>();
關於AF,RI和rep exposure:
① 檢查表示不變數 checkRep
所有點的標識不能為空
private void checkRep()
{
assert vertices != null;
}
② add方法
public boolean add(String vertex)
引數:vertex,判斷vertices中無重複點就加入
③ Set方法
public int set(String source, String target, int weight)
引數:source, target, weight。先將可能不在vertices中的source點和target加入vertices。隨後遍歷vertices,找到source對它增加一個target,找到target為它增加一個source,並設定距離。
④ remove方法
public boolean remove(String vertex)
引數:vertex。遍歷vertices,如果當前點是vertex,刪去(使用iterator.remove方法),如果不是,檢查它的source和target是否包含vertex,如果有刪去。
⑤ vertices方法
遍歷vertices,找到每個點對應的string,新增進set即可。使用防禦性拷貝:
public Set<String> vertices()
{
Set<String> set = new HashSet<>();
for (Vertex<String> v : vertices)
{
set.add(v.get_Name());
}
return set;
}
⑥ sources方法
public Map<String, Integer> sources(String target)
引數:target。根據傳入的target引數尋找以targe為終點的邊。返回一個鍵值對為(點,權重)的map。
實現:建立一個map,利用迭代器遍歷edges,如果某個edge的edge.get_Target()和傳入引數target相等,則返回target對應的源點圖。
⑦ targets方法
public Map<String, Integer> targets(String source)
引數:source。根據傳入的source引數尋找以source為起點的邊。返回一個鍵值對為(點,權重)的map。
實現:建立一個map,利用迭代器遍歷edges,如果某個edge的edge.get_Source()和傳入引數source相等,則返回source對應的目標點圖。
⑧ toString方法
列印當前頂點圖的頂點數量:
public String toString()
{
return String.format("This graph has %d vertices", this.vertices.size());
}
(3)ConcreteEdgesGraphTest測試
JUnit測試結果如下:
測試覆蓋率:
3.1.4 Problem 3: Implement generic Graph<L>
3.1.4.1 Make the implementations generic
將具體類的宣告更改為:
public class ConcreteEdgesGraph<L> implements Graph<L> { ... }
class Edge<L> { ... }
和
public class ConcreteVerticesGraph<L> implements Graph<L> { ... }
class Vertex<L> { ... }
更新兩個實現以支援任何型別的頂點標籤,使用佔位符L代替String。
充分利用IDEA的智慧改錯功能快速修改成泛型實現。
3.1.4.2
Implement Graph.empty()選擇ConcreteEdgesGraph來實現Graph.empty()
測試全部通過:
3.1.5 Problem 4: Poetic walks
3.1.5.1 Test GraphPoet
關於測試策略:
具體測試:
3.1.5.2
Implement GraphPoet首先宣告:
private final Graph<String> graph = new ConcreteEdgesGraph<String>();
關於AF,RI和rep exposure:
① 檢查表示不變數 checkRep
所有點的標識不能為空
private void checkRep()
{
assert graph != null;
}
② GraphPoet方法
引數:corpus檔案路徑。開啟檔案,讀取檔案輸入,識別序列,構建圖結構。
具體:利用BufferedReader.readLine方法讀取全部輸入後用string.split以空格劃分,儲存在陣列中,隨後每次取相鄰元素,在圖中新增邊。
③ poem方法
引數:input。
利用相同方法分割輸入字串,宣告一個StringBuilder儲存返回結果。每次讀取一個詞,然後以當前詞為source,下一個詞為target,在graph中尋找符合此條件的邊,記錄權值,結束後選擇權值最大的,利用StringBuilder. Append方法,將節點名字加入字串。
④ toString方法
呼叫ConcreteEdgesGraph的toString方法,輸出圖結構
public String toString()
{
return graph.toString();
}
3.1.5.3
語料庫為泰戈爾經典名句集錦
輸入輸出如下:
3.1.6
Before you’re done通過Git提交當前版本到GitHub上你的Lab2倉庫。
git add .
git commit -m "P1 Finished"
git push -u origin master
專案的目錄結構樹狀示意圖。
3.2
Re-implement the Social Network in Lab1這個實驗是基於在Poetic Walks中定義的Graph及其兩種實現,重新實現Lab1中的 FriendshipGraph類。我們需要儘可能複用ConcreteEdgesGraph或 ConcreteVerticesGraph中已經實現的add()和set()方法,而不是從零開始。另外基於所選定的 ConcreteEdgesGraph 或 ConcreteVerticesGraph的rep來實現,而不能修改父類的rep。
3.2.1 FriendshipGraph類
(1)設計思路
繼承ConcreteEdgesGraph<Person>,並在類中增加一些對非法情況的判斷,例如頂點已經存在,邊已經存在或找不到對應頂點等。
public class FriendshipGraph extends ConcreteEdgesGraph<Person>
(2)方法實現
構造一個ArrayList型別的變數person_list儲存頂點列表
private final ArrayList<String> person_list = new ArrayList<String>();
① public boolean addVertex(Person people)
這個函式是為把引數新增到圖中,作為圖的一個頂點,直接呼叫父類的this.add()即可。呼叫過程中檢查頂點列表中是否已出現引數對應的頂點,若重複則列印錯誤資訊並返回false,成功新增頂點則返回true
② public boolean addEdge(Person people1, Person people2)
構建圖的要素,在圖中新增邊。先呼叫 this.vertices().contains()方法來判斷所新增邊的頂點是否存在,再判斷兩頂點之間是否已有邊連線,若條件滿足,則呼叫this.set()方法設定邊,權重初始化為1並返回true,其餘情況返回false。
③ public int getDistance(Person People1, Person People2)
獲取兩個頂點之間距離的函式,題目要求返回最短距離,因此採用廣度遍歷的方式,此處需要用到Queue的資料結構,並且設定了一個List來存放已經訪問過的person。
3.2.2
Person類Person類根據FriendshipGraph類的需求編寫的。它用於描述每個成員的性質,主要是例項化姓名的構造方法,getName()方法,判斷姓名是否重複的isSameName方法。
public class Person
{
private final String Name;
public Person (String Name)
{
this.Name = Name;
}
public String getName()
{
return this.Name;
}
public boolean isSameName(String Name)
{
return this.Name.equals(Name);
}
}
3.2.3
客戶端main()main函式主體內容即為實驗指導書給定的內容:先new一個FriendshipGraph類的物件,然後新增頂點,新增邊。
輸出錯誤型別在實現FriendshipGraph類時已輸出,故此處不需要再次判斷是否出現錯誤。
① 正常輸出的測試結果
② 註釋掉rachel -> ross後的測試結果
3.2.4 測試用例
測試策略:
根據劃分的等價類設計測試用例
測試結果與覆蓋度報告:
3.2.5 提交至Git倉庫
通過Git提交當前版本到GitHub上的Lab2倉庫。
git add .
git commit -m "P1 P2 first finished"
git push -u origin master
本專案的目錄結構樹狀示意圖:
4 實驗進度記錄
請使用表格方式記錄你的進度情況,以超過半小時的連續程式設計時間為一行。
日期 |
時間段 |
計劃任務 |
實際完成情況 |
2022-05-23 |
8:00-11:30 |
瀏覽報告,檢視MIT相關內容要求 |
按計劃完成 |
2022-05-23 |
15:00-19:00 |
完成P1的GraphStaticTest類測試 |
按計劃完成 |
2022-05-24 |
9:00-11:00 |
完成P1的ConcreteEdgesGraph類編寫 |
按計劃完成 |
2022-05-24 |
15:00-20:00 |
完成P1的ConcreteVerticesGraph類 |
按計劃完成 |
2022-05-25 |
8:15-11:00 |
實現泛型Graph<L>的轉換 |
按計劃完成 |
2022-05-25 |
13:00-14:00 |
瀏覽Poetic Walks的編寫要求 |
按計劃完成 |
2022-05-25 |
21:00-23:00 |
嘗試實現Poetic Walks |
遇到困難,延期完成 |
2022-05-26 |
9:00-17:00 |
完成Poetic Walks的test和Implement |
按計劃完成 |
2022-05-27 |
8:00-18:00 |
完成P2的Social Network的改寫 |
按計劃完成 |
5 實驗過程中遇到的困難與解決途徑
遇到的難點 |
解決途徑 |
不瞭解IDEA如何實現程式碼覆蓋率測試
|
查閱網上資料後發現相應解決方案因IDE版本迭代已發生改變。自己摸索解決了實現程式碼覆蓋率測試外掛切換的問題。 |
對面向test的程式設計思想理解不夠深入
|
複習《軟體構造》課程的PPT,上網查閱了相關資料並諮詢了同學,完成了對測試程式碼的編寫。 |
對規約的要求不夠理解
|
通過學習模仿,嘗試自己編寫相應規約並實現之。 |
6 實驗過程中收穫的經驗、教訓、感想
6.1 實驗過程中收穫的經驗和教訓
經驗:
加深了自己對於泛型的理解和認識,提高了程式碼編寫、ADT設計的能力。編寫test測試檔案時,有些方法的測試也能覆蓋到其他的方法,避免重複測試增加工作量。
教訓:
在設計多個類並使之互相配合的方面做得不好,編寫程式碼的邏輯性有待提高。
6.2 針對以下方面的感受
(1)面向ADT的程式設計和直接面嚮應用場景程式設計,你體會到二者有何差異?
我感覺,對於面向ADT的程式設計,類是其主要特點,程式執行過程中,先由主函式進入,定義一些類,根據需要,執行
,過程的概念被淡化了。而直接面嚮應用場景程式設計的抽象程度不高,雖然邏輯清晰但是程式碼思路混亂,不利於實現。
(2)使用泛型和不使用泛型的程式設計,對你來說有何差異?
泛型程式設計可以使程式碼被很多不同型別的物件所重用,並使程式碼具有更好的可讀性。
(3)在給出ADT的規約後就開始編寫測試用例,優勢是什麼?你是否能夠適應這種測試方式?
優勢是不考慮程式碼的內部實現,只需考慮是否完成了規約中指定的功能。作為java語言的初學者來說我很不適應這種測試方式。
(4)P1設計的ADT在多個應用場景下使用,這種複用帶來什麼好處?
提高了程式碼的利用率,減輕程式設計工作量。
(5)為ADT撰寫specification, invariants, RI, AF,時刻注意ADT是否有rep exposure,這些工作的意義是什麼?你是否願意在以後程式設計中堅持這麼做?
使編寫的程式碼更加安全和可讀性更強。願意這麼做。
(6)關於本實驗的工作量、難度、deadline。
我認為,考慮到實驗時間,與其他專業課的複習時間與考試時間有大量衝突,因此顯得本實驗工作量十分巨大,難度也很高,deadline十分緊張。
(7)《軟體構造》課程進展到目前,你對該課程有何體會和建議?
希望減少工作量,增加課時安排,增加動手實驗分數,減少筆試考試分數,合理安排課程開展時間,可以在剛開學時開課或者在小學期開課,更有利於學生能力的提升。