1.在文章開頭給出教學班級和可克隆的 Github 項目地址。
| 項目 | 內容 |
|---|---|
| 這個作業屬於哪個課程 | 2020春季計算機學院軟件工程(羅傑 任健) |
| 這個作業的要求在哪里 | 作業要求 |
| 班級 | 006 |
| 項目地址 | https://github.com/syncline0605/IntersectPairProject |
2.在開始實現程序之前,在下述 PSP 表格記錄下你估計將在程序的各個模塊的開發上耗費的時間。
| PSP2.1 | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
|---|---|---|---|
| Planning | 計划 | ||
| · Estimate | · 估計這個任務需要多少時間 | 10 | |
| Development | 開發 | ||
| · Analysis | · 需求分析 (包括學習新技術) | 30 | |
| · Design Spec | · 生成設計文檔 | 10 | |
| · Design Review | · 設計復審 (和同事審核設計文檔) | 10 | |
| · Coding Standard | · 代碼規范 (為目前的開發制定合適的規范) | 10 | |
| · Design | · 具體設計 | 90 | |
| · Coding | · 具體編碼 | 300 | |
| · Code Review | · 代碼復審 | 100 | |
| · Test | · 測試(自我測試,修改代碼,提交修改) | 300 | |
| Reporting | 報告 | ||
| · Test Report | · 測試報告 | 30 | |
| · Size Measurement | · 計算工作量 | 10 | |
| · Postmortem & Process Improvement Plan | · 事后總結, 並提出過程改進計划 | 20 | |
| 合計 | 920 |
3.看教科書和其它資料中關於 Information Hiding,Interface Design,Loose Coupling 的章節,說明你們在結對編程中是如何利用這些方法對接口進行設計的。
Information Hiding
David Parnas在1972年最早提出信息隱藏的觀點。他指出:代碼模塊應該采用定義良好的接口來封裝,這些模塊的內部結構應該是程序員的私有財產,外部是不可見的。
Interface Design
接口的設計原則:
- 依賴倒置原則:高層模塊不應該依賴底層模塊,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。
- 接口隔離原則:客戶端不應該依賴它不需要的接口;一個類對另一個類的依賴應該建立在最小的接口上。
Loose Coupling
耦合的強度依賴於:一個模塊對另一個模塊的調用、一個模塊向另一個模塊傳遞的數據量、一個模塊施加到另一個模塊的控制的多少、模塊之間接口的復雜程度等。
松耦合意味着模塊與模塊之間可分拆、少依賴。
我們在結對編程時,在圖形模塊中調用計算模塊的接口來進行計算。對於圖形界面的編寫者而言,無需知道計算模塊的細節。兩個模塊可以各自獨立進行開發。
4.計算模塊接口的設計與實現過程。
我們的獨到之處在於針對線段與射線的新功能的實現基本都是對原有的直線相關函數的調用,計算交點時先將線段或射線轉化為它們所在的直線,直線計算出交點后再判斷該點是否在線段或射線上,極大地簡化了代碼邏輯。
因為考慮到封裝的幾何對象僅保存數據,不涉及方法,因此用struct封裝幾何對象
struct Point {//點
double x;
double y;
double length;
bool operator ==(const Point& b) const noexcept
{
if (compareDouble(x - b.x) == 0 && compareDouble(y - b.y) == 0) return true;
return false;
}
bool operator <(const Point& b) const noexcept
{
if (compareDouble(x - b.x) == 0 && compareDouble(y - b.y) < 0) return true;
if (compareDouble(x - b.x) < 0) return true;
return false;
}
};
typedef Point Vector; //向量
struct Line { //直線
Point p1, p2;
};
struct Segment { //線段
Point p1, p2;
};
struct Ray { //射線
Point start, direction;
};
struct Circle { //圓
Point center;
double r;
};
通過命令行處理函數,獲得輸入輸出文件的名稱。從輸入文件中得到所有的幾何對象,將同類的幾何對象存入同一個vector中。將全體交點的Point集合存入一個set。
vector<Line> lineSet;
vector<Segment> segmentSet;
vector<Ray> raySet;
vector<Circle> circleSet;
set<Point> pointSet;
計算交點的calPoint函數在同一個vector內部進行或是在兩個不同類vector間進行。計算交點的getPoint函數在兩個幾何對象間進行,返回計算出的交點個數:NOCROSS,ONECROSS,TWOCROSS,MANYCROSS(無數個交點)。
計算交點的函數定義如下
int getPoint(Line l1, Line l2, Point& crossPoint) noexcept;
int getPoint(Line l, Segment s, Point& crossPoint) noexcept;
int getPoint(Line l, Ray r, Point& crossPoint) noexcept;
int getPoint(Line l, Circle c, pair<Point, Point>& crossPair) noexcept;
int getPoint(Segment s1, Segment s2, Point& crossPoint) noexcept;
int getPoint(Segment s, Ray r, Point& crossPoint) noexcept;
int getPoint(Segment s, Circle c, pair<Point, Point>& crossPair) noexcept;
int getPoint(Ray r1, Ray r2, Point& crossPoint) noexcept;
int getPoint(Ray r, Circle c, pair<Point, Point>& crossPair) noexcept;
int getPoint(Circle c1, Circle c2, pair<Point, Point>& crossPair) noexcept;
int calPoint(vector<Line>& lineSet, set<Point>& pointSet);
int calPoint(vector<Line>& lineSet, vector<Segment>& segmentSet, set<Point>& pointSet);
int calPoint(vector<Line>& lineSet, vector<Ray>& raySet, set<Point>& pointSet);
int calPoint(vector<Line>& lineSet, vector<Circle>& circleSet, set<Point>& pointSet);
int calPoint(vector<Segment>& segmentSet, set<Point>& pointSet);
int calPoint(vector<Segment>& segmentSet, vector<Ray>& raySet, set<Point>& pointSet);
int calPoint(vector<Segment>& segmentSet, vector<Circle>& circleSet, set<Point>& pointSet);
int calPoint(vector<Ray>& raySet, set<Point>& pointSet);
int calPoint(vector<Ray>& raySet, vector<Circle>& circleSet, set<Point>& pointSet);
int calPoint(vector<Circle>& circleSet, set<Point>& pointSet);
int calPoint(vector<Line>& lineSet, vector<Segment>& segmentSet, vector<Ray>& raySet, vector<Circle>& circleSet, set<Point>& pointSet);
將線段或射線轉換為直線、判斷點是否在一線段或射線范圍內的關鍵函數:
//將線段轉化成對應的直線
//將橫坐標較小的點作為p1,若兩點橫坐標相同則將縱坐標較小的點作為p1
Line segmentToLine(Segment s) {
Line l;
if ((s.p1.x < s.p2.x) || ((s.p1.x == s.p2.x) && (s.p1.y < s.p2.y))) {
l.p1 = s.p1;
l.p2 = s.p2;
} else {
l.p1 = s.p2;
l.p2 = s.p1;
}
return l;
}
//將射線轉化成對應的直線
Line rayToLine(Ray r) {
Line l{ r.start, r.direction };
return l;
}
//判斷一個點是否在一線段的坐標范圍內
int pointIfOnSeg(Point p, Line l)
{
if (l.p1.x == l.p2.x) {
if ((p.y >= l.p1.y) && (p.y <= l.p2.y)) {
return ON;
} else {
return NOTON;
}
} else {
if ((p.x >= l.p1.x) && (p.x <= l.p2.x)) {
return ON;
} else {
return NOTON;
}
}
}
//判斷一個點是否在一射線的坐標范圍內
int pointIfOnRay(Point p, Line l)
{
if (l.p2.x < l.p1.x) {
//若射線指向負方向
if (p.x <= l.p1.x) {
return ON;
} else {
return NOTON;
}
} else if (l.p2.x == l.p1.x && l.p2.y < l.p1.y) {
//若射線指向正下方
if (p.y <= l.p1.y) {
return ON;
} else {
return NOTON;
}
} else if (l.p2.x == l.p1.x && l.p2.y > l.p1.y) {
//若射線指向正上方
if (p.y >= l.p1.y) {
return ON;
} else {
return NOTON;
}
} else {
//若射線指向正方向
if (p.x >= l.p1.x) {
return ON;
} else {
return NOTON;
}
}
}
5.畫出 UML 圖顯示計算模塊部分各個實體之間的關系。

6.計算模塊接口部分的性能改進。記錄在改進計算模塊性能上所花費的時間,描述你改進的思路,並展示一張性能分析圖(由VS 2015/2017的性能分析工具自動生成),並展示你程序中消耗最大的函數。
使用一個隨機生成的包含一千多條數據的測試集。

很明顯,程序將大多數時間用在了set的元素插入上。
除此之外,基於確定的精確值對equals意義的重寫也占用了大量時間

7.看 Design by Contract,Code Contract 的內容,描述這些做法的優缺點,說明你是如何把它們融入結對作業中的。
優點:
- 規定了前置、后置和不變量約束條件,如果不滿足這些約束,程序將拋出異常。事先規定好這些約束條件,能夠對程序的正確性有所保證。
- 提高程序的規范性,使程序更加便於閱讀和理解。
缺點:
- 如果想要關閉這些約束條件,需要專門編寫一個模塊來關閉它。
- 運行時性能會受到影響。
我們在結對作業中並沒有專門用一個模塊去規定約束條件,但對程序中需要規定的條件也進行了約束,如點的坐標需要在(-100000,100000)范圍內,如果不滿足,則拋出異常。
8.計算模塊部分單元測試展示。展示出項目部分單元測試代碼,並說明測試的函數,構造測試數據的思路。並將單元測試得到的測試覆蓋率截圖,發表在博客中。要求總體覆蓋率到 90% 以上,否則單元測試部分視作無效。
如上文的模塊設計部分展示的函數定義,計算模塊分為L-L,L-R,L-S,L-C,S-S,S-R,S-C,R-R,R-C,C-C幾個函數,只要分別覆蓋了這些這些計算交點的getPoint函數,就能覆蓋其調用的其他計算函數。為了覆蓋所有calPoint函數,再編寫一個較大的測試集測試包含所有類別的完整calPoint函數即可實現較好的覆蓋率。
//測試圓與圓交點的多種情況,除保證計算交點個數正確外,還要保證計算的交點坐標正確
TEST_METHOD(Circle_Circle_TwoCross)
{
Circle c1;
Circle c2;
c1.center.x = 0; c1.center.y = 0; c1.r = 2;
c2.center.x = 2; c2.center.y = 0; c2.r = 2;
Point realPoint1, realPoint2;
pair<Point, Point> testPair;
realPoint1.x = 1; realPoint1.y = 1.73205081;
realPoint2.x = 1; realPoint2.y = -1.73205081;
Assert::IsTrue(getPoint(c1, c2, testPair) == 2);
Assert::IsTrue(((realPoint1 == testPair.first) && (realPoint2 == testPair.second)) || ((realPoint2 == testPair.first) && (realPoint1 == testPair.second)));
}
TEST_METHOD(Circle_Circle_OneCross)
{
Circle c1;
Circle c2;
c1.center.x = 0; c1.center.y = 0; c1.r = 2;
c2.center.x = 4; c2.center.y = 0; c2.r = 2;
Point realPoint1, realPoint2;
pair<Point, Point> testPair;
realPoint1.x = 2; realPoint1.y = 0;
Assert::IsTrue(getPoint(c1, c2, testPair) == 1);
Assert::IsTrue(realPoint1 == testPair.first);
}
TEST_METHOD(Circle_Circle_NoCross)
{
Circle c1;
Circle c2;
c1.center.x = 0; c1.center.y = 0; c1.r = 2;
c2.center.x = 5; c2.center.y = 0; c2.r = 2;
Point realPoint1, realPoint2;
pair<Point, Point> testPair;
Assert::IsTrue(getPoint(c1, c2, testPair) == 0);
}
TEST_METHOD(TestAll)
{
vector<Line> lineSet;
vector<Segment> segmentSet;
vector<Ray> raySet;
vector<Circle> circleSet;
set<Point> pointSet;
Line l1, l2; Segment s1, s2, s3; Ray r1, r2, r3; Circle c1, c2;
l1.p1.x = 1; l1.p1.y = 2; l1.p2.x = 3; l1.p2.y = 3;
l2.p1.x = 4; l2.p1.y = 2; l2.p2.x = 2; l2.p2.y = -4;
lineSet.push_back(l1); lineSet.push_back(l2);
s1.p1.x = -5; s1.p1.y = 1; s1.p2.x = 5; s1.p2.y = -3;
s2.p1.x = 5; s2.p1.y = 3; s2.p2.x = 2; s2.p2.y = 2;
s3.p1.x = 6; s3.p1.y = -5; s3.p2.x = 6; s3.p2.y = 6;
segmentSet.push_back(s1); segmentSet.push_back(s2);
segmentSet.push_back(s3);
r1.start.x = 5; r1.start.y = 2; r1.direction.x = 3; r1.direction.y = 4;
r2.start.x = 2; r2.start.y = -4; r2.direction.x = -1; r2.direction.y = -1;
r3.start.x = -10; r3.start.y = 1; r3.direction.x = 10; r3.direction.y = 1;
raySet.push_back(r1); raySet.push_back(r2); raySet.push_back(r3);
c1.center.x = 2; c1.center.y = 3; c1.r = 4;
c2.center.x = 5; c2.center.y = 2; c2.r = 2;
circleSet.push_back(c1); circleSet.push_back(c2);
Assert::AreEqual(calPoint(lineSet, segmentSet, raySet, circleSet, pointSet), 31);
}
覆蓋率截圖,計算模塊GeoCalculate.pp的覆蓋率達到了90%:

9.計算模塊部分異常處理說明。在博客中詳細介紹每種異常的設計目標。每種異常都要選擇一個單元測試樣例發布在博客中,並指明錯誤對應的場景。
設計了以下幾類異常:
- 非法輸入格式
- 輸入幾何對象不足
- 文件尾部輸入過多內容
- 坐標范圍超出限制
- 命令行參數是其他字符
- 交點個數為無數個
非法輸入格式
當遇到不符合要求的輸入數據時,如文件開始未出現n、出現非法字符、幾何對象的數據不正確時,拋出異常 illegalInputPattern
單元測試樣例:





輸入幾何對象不足
從文件的首行獲得n,但是當在文件之后輸入數據不足n個時,拋出異常notEnoughInputElement
單元測試樣例:

文件尾部輸入過多內容
當讀入n和n個幾何對象的數據后,文件尾還有多余內容,拋出異常TooManyInputElements
單元測試樣例:

坐標范圍超出限制
對於每個輸入數據,判斷是否在(-100000,100000)范圍內,如果不在,則拋出異常outRangeException。當圓的輸入半徑小與或等於0時,也會拋出這一異常。
單元測試樣例:

命令行參數是其他字符
若命令行參數不正確,則拋出異常commandException。
單元測試樣例:


交點個數為無窮個
若兩個圖形有無數個交點,則拋出異常infException。
單元測試樣例:

10.界面模塊的詳細設計過程。在博客中詳細介紹界面模塊是如何設計的,並寫一些必要的代碼說明解釋實現過程。
界面模塊使用Qt Creator進行開發,設計了兩個窗口,一個是主界面dialog,另一個是畫圖界面new_window。
主界面如圖所示:

接下來逐一介紹所實現的四個功能:
-
打開文件
void Dialog::readFile()函數使用了
getOpenFileName方法獲取文件路徑,並打開文件,用readLine方法逐行讀取文件內容,並用split方法對字符串按空格進行分割,存儲到相應的結構中。最后使用ui->label->setText方法將讀取的內容顯示在界面上。點擊“打開文件”按鈕並選擇文件后效果如圖所示:

-
添加圖形
void Dialog::addone()函數使用
ui->text->toPlainText方法獲取文本框中的字符串,用split方法分割並存儲到相應的結構中。最后使用ui->label_2->setText方法將所添加的數據顯示在界面上。點擊“添加”按鈕后效果如圖所示:

-
刪除圖形
void Dialog::deleteone()函數使用
ui->text->toPlainText方法獲取文本框中的字符串,用split方法分割,找到該數據對應的元素並刪除。最后使用ui->label_2->setText方法將所刪除的數據顯示在界面上。點擊“刪除”按鈕后效果如圖所示:

-
繪制圖形和交點
void Dialog::open()函數打開新窗口new_window,重寫
paintEvent方法,進行圖像繪制。繪圖方法:
- 線:drawLine
- 圓:drawEllipse
- 點:drawPoint
圖形繪制效果如圖所示:

11.界面模塊與計算模塊的對接。詳細地描述 UI 模塊的設計與兩個模塊的對接,並在博客中截圖實現的功能。
計算交點需要用到calPoint方法,它的接口是五個結構體的集合,分別代表直線、線段、射線、圓和點。我的想法是把界面模塊中的結構體也包裝成相同的形式,直接調用該方法進行計算即可。將Release版的動態鏈接庫和相關頭文件添加到項目中,即可直接調用該方法。

功能實現展示
從文件中添加3個圖形,並手動添加1個圖形(射線),如圖所示:

點擊“繪制圖形和交點”按鈕后效果如圖所示:

從文件中添加4個圖形,並手動刪除1個圖形(線段),如圖所示:

點擊“繪制圖形和交點”按鈕后效果如圖所示:

12.描述結對的過程,提供兩人在討論的結對圖像資料(比如 Live Share 的截圖)。
我們使用了qq的共享屏幕功能,結合在微信上討論,來完成結對編程。

13.看教科書和其它參考書,網站中關於結對編程的章節,說明結對編程的優點和缺點。同時描述結對的每一個人的優點和缺點在哪里(要列出至少三個優點和一個缺點)。
結對編程的優點
- 有隨時的復審和交流,能夠減少程序中的錯誤,提高程序的初始質量,節省以后修改、測試的時間
- 兩人一起配合,是一個互相督促的過程,比起一個人單干更不容易開小差和拖延
- 能夠互相學習到對方的優點和長處,提高編程能力
結對編程的缺點
- 兩人可能會產生分歧和矛盾
- 如果團隊的人員不能保證充足的結對編程時間,成員需要經常等待,浪費時間
- 對新手來說,需要一定的時間來適應這種開發方式,不適合時間緊迫的項目
隊友的優點
- 代碼風格很好,編程能力很強,注釋清楚,一目了然
- 善於溝通,一起討論解決問題,采納彼此的意見,我們在結對編程的過程中沒有發生任何爭執或矛盾,合作十分愉快
- 態度非常積極,始終在積極地合作完成項目,沒有拖延和鴿子的情況
隊友的缺點
- 有時會出現一些粗心小bug,但都能夠立刻發現並改正
我的優點
-
學習能力較強,面對新的問題,能夠找到解決問題的方法並付諸實施
-
態度也很積極,不拖延、沒有浪費彼此的時間
-
編程時比較細心、認真
我的缺點
- 代碼風格較差,寫注釋較少
14.在你實現完程序之后,在附錄提供的PSP表格記錄下你在程序的各個模塊上實際花費的時間。
| PSP2.1 | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
|---|---|---|---|
| Planning | 計划 | ||
| · Estimate | · 估計這個任務需要多少時間 | 10 | 10 |
| Development | 開發 | ||
| · Analysis | · 需求分析 (包括學習新技術) | 30 | 60 |
| · Design Spec | · 生成設計文檔 | 10 | 10 |
| · Design Review | · 設計復審 (和同事審核設計文檔) | 10 | 10 |
| · Coding Standard | · 代碼規范 (為目前的開發制定合適的規范) | 10 | 10 |
| · Design | · 具體設計 | 90 | 150 |
| · Coding | · 具體編碼 | 300 | 450 |
| · Code Review | · 代碼復審 | 100 | 200 |
| · Test | · 測試(自我測試,修改代碼,提交修改) | 300 | 450 |
| Reporting | 報告 | ||
| · Test Report | · 測試報告 | 30 | 30 |
| · Size Measurement | · 計算工作量 | 10 | 10 |
| · Postmortem & Process Improvement Plan | · 事后總結, 並提出過程改進計划 | 20 | 10 |
| 合計 | 920 | 1400 |
