軟體工程結對項目作業
1.簡介
項目 | 内容 |
---|---|
這個作業屬于哪個課程 | 班級部落格 |
這個作業的要求在哪裡 | 作業要求 |
我在這個課程的目标是 | 系統地提升軟體工程能力 |
這個作業在哪個具體方面幫助我實作目标 | 掌握結對開發流程, 積累合作經驗 |
教學班級 | 006 |
Github項目位址 | https://github.com/Eadral/SE_Pair_Project |
2.PSP
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | ||
· Estimate | · 估計這個任務需要多少時間 | 10 | |
Development | 開發 | ||
· Analysis | · 需求分析 (包括學習新技術) | 30 | 20 |
· Design Spec | · 生成設計文檔 | 5 | 15 |
· Design Review | · 設計複審 (和同僚稽核設計文檔) | ||
· Coding Standard | · 代碼規範 (為目前的開發制定合适的規範) | 70 | |
· Design | · 具體設計 | 60 | |
· Coding | · 具體編碼 | 600 | 500 |
· Code Review | · 代碼複審 | ||
· Test | · 測試(自我測試,修改代碼,送出修改) | 120 | 150 |
Reporting | 報告 | ||
· Test Report | · 測試報告 | ||
· Size Measurement | · 計算工作量 | ||
· Postmortem & Process Improvement Plan | · 事後總結, 并提出過程改進計劃 | 90 | |
合計 | 1045 | 1055 |
3.接口設計
Information Hiding
封裝(Encapsulation)性是面向對象程式設計方法的一個重要特性。封裝包含兩層含義,一是将抽象得到的有關資料和操作代碼相結合,形成一個有機的整體,對象之間相對獨立,互不幹擾。二是封裝将對象封閉保護起來,對象中某些部分對外隐蔽,隐藏内部的實作細節,隻留下一些接口接收外界的消息,與外界聯系,這種方法稱為資訊隐蔽(Information Hiding)。
封裝保證了類具有較好的獨立性,防止外部程式破壞類的内部資料,使得程式維護修改比較容易。對應用程式的修改僅限于類的内部,因而可以将應用程式修改帶來的影響減少到最低限度。
https://zhuanlan.zhihu.com/p/66046072
資訊隐藏主要通過封裝實作,防止外部改變類内的資料。
對于求交點的類,主要需要封裝的是其内部的Line,Circle,Point這些資料,隻能通過相應的Add, Get,Del方法來擷取這些資訊,這樣實作了資訊隐藏,有利于之後的維護。
Interface Design
接口應該清晰易懂,職責明确,易于維護。
編寫文檔
文檔描述了接口的請求參數,傳回參數,以及可能引發的異常。
迪米特法則(最小原則)
盡可能的減少接口的參數數量,不添加無用的參數。
同時接口保持單一職責,隻完成一個工作,這樣有利于維護。
對于這次作業,我們用更多數量的接口來代替複雜的接口,遵守了迪米特法則,使得維護和對接都更加簡單。
Loose Coupling
一個松耦合的系統中的每一個元件對其他獨立元件的定義所知甚少或一無所知。子範圍包括類、接口、資料和服務之間的耦合。
松耦合系統中的元件能夠被提供相同服務的替代實作所替換。松耦合系統中的元件不太受相同的平台、語言、作業系統或建構環境的限制。
https://zh.wikipedia.org/wiki/%E6%9D%BE%E8%80%A6%E5%90%88
松耦合的目标是最小化依賴,這樣各個元件直接更容易替換,具有很高的擴充性和靈活性,同時也有助于提高可維護性。
在封裝的基礎上松耦合比較容易實作,但是與封裝不同之處在于松耦合可能有着滿足其他元件而非現有元件的需求,是以不能僅僅是隐藏資訊,還要以良好封裝的形式允許各種形式的增删改查。
為了實作松耦合,在這次項目中對所有的資料類(Line,Circle)都增加了Add,Del,Get方法,用于給各個子產品提供功能。
4.計算子產品接口的設計與實作過程
設計思路
這次新增的功能是支援射線和線段,可以把射線和線段當作線的特殊情況,隻需要在求出交點後判斷點是否在射線或線段上即可,整體的代碼結構、類、函數幾乎不變,隻是增加了關于射線和線段的處理。
涉及的類有Line,Circle,Point,以及一個用于解題的Solver。
其中Line,Circle,Point都是不變對象,使用struct實作,其中Point涉及比較,是以會重載比較運算符,并且在這裡需要進行精度判斷。
Solver需要擷取以上類的對象資訊進行求解,這裡選擇Solver去組合Line,Circle,和Point。Solver還需要進行IO操作,提供Input和Output接口。
為了友善其他子產品,提供了多種Input和Output接口:
-
: 讀取純文字(按照輸入資料格式處理)Input(char *str)
-
,AddLine(...)
DelLine(...)
: 對象接口(以Line為例,其他類似)GetLines(...)
-
GetIntersections(...)
: 用于擷取交點資料(結果)GetIntersectionNumber(...)
Solve流程
- 從Input接口擷取輸入,建構Line和Circle數組。
- 求交點:周遊Line,Circle資料,兩兩求交點。交點插入到Point數組中。
- 這裡會進行Ray和Segment的判斷。通過分類讨論交點與線坐标大小的關系,判斷交點是否線上段或射線上。
- 對Point數組進行排序去重。
- 從Output接口輸出答案。

UML
6.性能改進
本次作業的計算流程和上次是完全一樣的,在上次作業中嘗試了數種方法并最終确定了這樣的方案:兩兩對象求交點,将交點加入數組,最後對交點數組排序去重,得到答案。
這種計算流程可維護性高。并且通過測試發現,相比Hash表,連結清單等資料結構都有着性能優勢。利用C++ vector的原地構造(emplace_back)和排序算法對緩存的利用,這種方案可以達到較好的性能,是以本次作業依然使用了這種方案。
這次的性能分析發現熱點是求交點的部分,大量時間花費在了乘法運算上。
觀察代碼後發現存在一些重複計算,是以将部分計算在Line構造時就完成計算,并盡可能進行複用,減少重複計算。
Line(const int x1, const int y1, const int x2, const int y2) noexcept
: x1(x1), y1(y1), x2(x2), y2(y2)
{
dx = (long long)x1 - (long long)x2;
dy = (long long)y1 - (long long)y2;
x2y1 = (long long)x2 * (long long)y1;
x1y2 = (long long)x1 * (long long)y2;
x2y1_x1y2 = x2y1 - x1y2;
}
- 代碼轉換:
=>a.x2 * a.y1 - a.x1 * a.y2
a.x2y1_x1y2
40000條線資料,消耗最大函數是求交點
7.Design by Contract, Code Contract
契約式設計的主要目的是希望程式員能夠在設計程式時明确地規定一個子產品單元(具體到面向對象,就是一個類的執行個體)在調用某個操作前後應當屬于何種狀态。
契約式設計強調三個概念:前置條件,後置條件和不變式。前置條件發生在每個操作(方法,或者函數)的最開始,後置條件發生在每個操作的最後,不變式實際上是前置條件和後置條件的交集。違反這些操作會導緻程式抛出異常。
https://www.zhihu.com/question/19864652
在過去的課程中接觸過契約式設計,我認為有以下好處:
- 能夠提醒程式員去仔細思考這個子產品應該做什麼(核心的功能),不應該做什麼(是否有副作用),以及相應的錯誤情況(減少bug)。
但是也存在着缺點, 嚴格的契約式設計很難實作:
- 不變式的描述很困難,使用邏輯表述的不變式本身很容易寫錯。
- 這些條件的自動化測試比較難實作。
今天大部分的API應該是通過注釋來一定程度上的表示“契約”。
這次的結對作業中,我們主要是通過文檔中的異常部分來描述:輸入的錯誤情況(前置條件),計算中可能導緻的異常(後置條件),不變式則主要靠單元測試維護。
8.單元測試
單元測試在上次作業的基礎上進行擴充,按照粒度和功能分為以下幾類:
1. 交點測試:測試Solver的Line-Line,Line-Circle,Circle-Circle求交點方法分别測試,斷言交點數量。
交點測試用于驗證交點計算函數的正确性。
TEST_METHOD(Cross)
{
stringstream sin;
stringstream sout;
Solver solver(sin, sout);
solver.LineLineIntersect(
Line(0, 0, 1, 1),
Line(0, 1, 1, 0)
);
Assert::AreEqual(solver.GetAns(), 1);
}
2. 錯誤測試:對錯誤輸入,異常情況進行測試,斷言輸出的錯誤類型。
錯誤測試用于驗證對錯誤的處理情況
TEST_METHOD(InvalidIdentifierTest)
{
stringstream sin;
stringstream sout;
Solver solver(sin, sout);
sin << R"(
1
X 0 0 1 1
)" << endl;
Assert::ExpectException<CoreException>([&] {solver.Solve();});
}
3. API測試:調用API并測試正确性。
API測試用于驗證API是否正确實作和封裝。
TEST_METHOD(APITest) {
Clear();
char buf[] = "2\nL 0 0 1 1\nL0 1 1 0\n";
Input(buf);
Assert::AreEqual(1, GetIntersectionsSize());
double xs[5], ys[5];
GetIntersections(xs, ys, 1);
Assert::AreEqual(0.5, xs[0]);
Assert::AreEqual(0.5, ys[0]);
}
4. End-to-End測試:向Solver提供輸入字元流,對輸出字元流進行斷言。
這類測試用于驗證整個程式流程的正确性
TEST_METHOD(Test6)
{
stringstream sin;
stringstream sout;
Solver solver(sin, sout);
sin << R"(
4
C 3 3 3
S 2 4 3 2
L -1 4 5 2
R 2 5 -1 2
)" << endl;
solver.Solve();
int ans;
sout >> ans;
Assert::AreEqual(5, ans);
}
總共83個單元測試
覆寫率97.17%
9.異常處理說明
N值格式錯誤
第一行有且僅有一個整數,否則即報錯
*#06#
L 0 0 1 1
N值非法
N<1時報錯
幾何對象辨別符格式錯誤
無辨別符,或包含非法字元/字元串,大小寫敏感
1
X 0 0 1 1
幾何對象描述錯誤
輸入格式錯誤,包括行内輸入過多/過少,數字輸入非法
2
L 0 0 1 1
C 4 3 1 2
輸入坐标分量超限
包括線的兩點坐标分量和圓心坐标分量
2
L 0 0 1 100000
R 4 3 1 2
線的兩點坐标重合
兩點坐标不能相同
1
L 0 0 0 0
圓的半徑非法
半徑值必須為正,且在規定範圍内
2
L 0 0 1 1
C 4 2 0
輸入幾何對象過少
少于N值
2
L 0 0 1 1
輸入幾何對象過多
多于N值
2
L 0 0 1 1
C 0 2 1
R 1 1 3 4
有無窮多交點
1
L 0 0 1 1
L 2 2 3 3
10.界面子產品設計
界面部分是使用WPF實作的,通過dll調用核心功能。
主要功能
核心功能通過界面上的3個按鈕進行操作:
- Import:導入純文字輸入檔案
- Add:按照輸入框指定的内容添加對象
- Remove:按照輸入框指定的内容删除對象
繪制的處理邏輯是單向綁定,使用單向綁定是為了降低UI上的狀态維護工作,把核心功能全部集中在核心子產品中,這樣有助于降低UI與核心的耦合,側重于核心子產品的靈活性,也有助于其他子產品的擴充。
單項綁定的具體實作是在每次點選按鈕後都會對畫布和右側清單進行重新整理,具體流程如下:
- 描述界面的xml中綁定相應單機事件的函數。
<Button Click="ButtonAdd">Add</Button>
- 回調函數會調用API完成添加操作,并使用Draw()進行繪制。
private void ButtonAdd(object sender, RoutedEventArgs e) {
// ...
AddLine(x1, y1, x2, y2);
Draw();
}
- 從核心讀取對象,并進行繪制
private void Draw() {
listView.Items.Clear();
DrawCircles();
DrawLines();
DrawRays();
DrawSections();
DrawIntersections();
}
GUI特性
- 清單選擇:選擇右側清單中的對象可以即時更改下方輸入框的内容,便于使用者進行删除操作。
- 左側的畫布可以使用滑鼠滾輪進行縮放,也可以拖拽移動。
11.界面子產品與計算子產品對接
界面子產品與計算子產品使用dll導出的API進行對接。
C++編寫的核心導出API:
INTERSECT_API void Clear();
INTERSECT_API void Input(char *input);
INTERSECT_API void AddLine(int x1, int y1, int x2, int y2);
INTERSECT_API void RemoveLine(int x1, int y1, int x2, int y2);
// ...
INTERSECT_API int GetIntersectionsSize();
INTERSECT_API void GetIntersections(double* xs, double* ys, int size);
C#進行DLL調用:
[DllImport("intersect_core.dll")]
public static extern void Clear();
[DllImport("intersect_core.dll")]
public static extern void Input([MarshalAs(UnmanagedType.LPStr)]string input);
[DllImport("intersect_core.dll")]
public static extern void AddLine(int x1, int y1, int x2, int y2);
[DllImport("intersect_core.dll")]
public static extern void RemoveLine(int x1, int y1, int x2, int y2);
// ....
[DllImport("intersect_core.dll")]
public static extern int GetIntersectionsSize();
[DllImport("intersect_core.dll")]
public static extern void GetIntersections(double[] xs, double[] ys, int size);
按照上文的邏輯調用這些API,就完成了對接。
實作功能截圖
12.結對過程
實時協作通過VS Live Share和騰訊會議完成,Live Share用于觀看代碼,通過騰訊會議進行語音。
零散的時間裡通過微信和Git完成協助。
13. 結對總結
結對程式設計 | 我 | 17373072 | |
---|---|---|---|
優點 | 1. 有助于互相學習程式設計經驗和好的代碼實踐 2. 互相監督, 互相糾錯, 互相複審,有助于減少bug | 1. 有一定代碼能力, 寫代碼效率較高 2. debug能力較好 | 1. 對需求分析仔細認真 2. 對細節考慮的比較周到 3. 編寫代碼認真,合作較好 |
缺點 | 1. 雙方習慣上的差異可能導緻合作上出現差錯,導緻效率降低 2. 頻繁的交流成本較高,時間等因素難以滿足結對的需求 | 1. 對需求分析的不夠細緻 | 1. 對bug的發現有些不足 |
松耦合
我們與(17373071, 17373078)組進行了子產品互換。
最開始對接的時候有嚴重的問題,因為我們的設計思路完全不同。我們組的思路是UI隻作為一個顯示,其餘的計算和狀态維護等都由核心子產品完成,而對方的思路是核心隻有一個求交點的功能,其餘的狀态資料以及添加修改都由UI完成。
我認為一個系統複雜的部分特别是狀态維護應該由單一子產品完成,這樣複雜度集中在了一個子產品,其他子產品就比較簡單,很容易擴充,也比較友善維護。拿這次項目來說,核心應該完成盡可能多的内容,UI隻需要進行顯示,這樣不管是桌面UI,Web UI,都隻需要完成顯示部分,也是隻能由UI完成的部分。否則每個UI都維護一套狀态,實際上就重複實作了邏輯,沒有做到複用。
經過讨論後,對方組也使用了核心維護狀态的方法。這裡再次感謝該組進一步的将API向我們的靠攏,最後我們兩組的API幾乎完全一緻了,可以很容易的實作對接。
交換dll後依然能夠正常工作。
交換效果
代碼檢查
通過 Code Quality Analysis 的檢查,沒有警告。
總結
這次作業有兩方面的收獲:
-
熟悉了接口開發的流程
過去自己寫個人項目較多,對接口考慮的較少,這次不但熟悉了dll的使用,同時也思考了應該如何去設計良好的接口。另外在互換子產品的時候,我們開始因為API不同難以對接,看來在各種工程中都應該是接口先行,否則後果就是必須重構。
-
熟悉了結對開發
在合作中我主要到了代碼複審的重要性。在一個人寫代碼的時候,對整個項目都是十分熟悉的,但是在多人開發時,如果不進行代碼複審,就會導緻對代碼不熟悉,可能導緻錯誤的了解以及引入bug。
本次項目中開始了簡單的合作和對接,積累了一定的經驗。期待之後的團隊項目中收獲更多。