2020BUAA軟工結伴項目作業
17373010 杜博玮
項目 | 内容 |
---|---|
這個作業屬于哪個課程 | 2020春季計算機學院軟體工程(羅傑 任健) |
這個作業的要求在哪裡 | 結伴項目作業 |
我在這個課程的目标是 | 學習軟體工程,培養工程開發能力、團隊協作能力,開闊視野 |
這個作業在哪個具體方面幫助我實作目标 | 通過結伴進行項目來進一步實踐,提高軟工水準 |
教學班級 005
項目位址 https://github.com/17373432/Pairng-Project
PSP表格:
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | ||
Estimate | 估計這個任務需要多少時間 | 15 | 10 |
Development | 開發 | ||
Analysis | 需求分析 (包括學習新技術) | 540 | 510 |
Design Spec | 生成設計文檔 | 45 | 40 |
Design Review | 設計複審 | 20 | |
Coding Standard | 代碼規範 (為目前的開發制定合适的規範) | ||
Design | 具體設計 | 90 | 75 |
Coding | 具體編碼 | 450 | |
Code Review | 代碼複審 | 60 | |
Test | 測試(自我測試,修改代碼,送出修改) | 150 | 180 |
Reporting | 報告 | ||
Test Reporting | 測試報告 | ||
Size Measurement | 計算工作量 | ||
Postmortem & Process Improvement Plan | 事後總結, 并提出過程改進計劃 | 25 | 30 |
合計 | 1555 | 1415 |
3. 看教科書和其它資料中關于 Information Hiding,Interface Design,Loose Coupling 的章節,說明你們在結對程式設計中是如何利用這些方法對接口進行設計的。
Information Hiding,即資訊隐藏,在子產品内需要進行封裝,提供給外部的接口需要保證其他子產品調用時不必關心其中的實作細節。這一點在我們的GUI與Core的接口中表現最為明顯,GUI向core子產品提供輸入流,即可得到圖中交點的數組,并不需要使用core子產品中的任何資料結構。
Interface Design,接口設計。我們在接口設計中主要保證輸入盡可能簡潔,各子產品分工明确,不會産生不同的子產品進行類似的工作的情況。
Loose Coupling,即松耦合。我們在設計過程中保證子產品之間隻通過子產品自身定義的接口進行調用,而不是一個子產品直接通路另一個子產品内部的資料,進而盡可能減少各子產品之間的依賴關系。
4.子產品接口的設計與實作過程。設計包括代碼如何組織,比如會有幾個類,幾個函數,他們之間關系如何,關鍵函數是否需要畫出流程圖?說明你的算法的關鍵(不必列出源代碼),以及獨到之處。
- 主類:将各個類聯系起來,完成基本功能以及錯誤處理。
- 屬性:
- 線的集合,用于存儲所有的直線、線段、射線的資訊,用vector儲存。
- 圓的集合,用于儲存所有的圓的資訊,用vector儲存。
- 點的集合,用于儲存所有交點的資訊,用unordered_set儲存。
- 其他用于輔助判斷資料是否合法的屬性。
- 方法:求解交點,判斷兩條線是否重合等方法。
- 屬性:
- Line類:采用一般式\(Ax+By+C=0\),避免了斜率為正無窮的讨論。
- 屬性:基本的A,B,C的值,線的種類,線的兩個端點坐标,以及其他簡化計算的屬性。
- 方法:求與直線的交點,判斷點是否線上上,以及構造函數,傳回屬性的值,重載小于運算符等方法。
- Point類:
- 屬性:橫坐标x,縱坐标y,交于此點的直線集合。
- 方法:構造函數,傳回屬性的值,添加經過這個點的直線,重載小于、等于運算符等方法。
- Circle類:
- 屬性:圓心(使用Point),半徑,以及其他簡化計算的屬性。
- 方法:求與直線的交點,求與圓的交點,以及構造函數,傳回屬性的值,重載小于運算符等方法。
關鍵函數:
-
判斷兩條線是否重合:
因為對于線來說隻有截距相同才可能會重合,是以事先儲存線的截距到線的映射,通過截距将線分組,這樣隻用同一組的線是否重合,具體判斷如下:
result = false; if (兩條線都為線段) { if (某線段某端點在另一條線段兩端點之間) { if (兩線段隻有一個點p重合) { pointSet.insert(p); } else { result = true; } } } else if (兩條線都為射線) { if (某射線端點在另一條射線上) { if (兩射線隻有一個點p重合) { pointSet.insert(p); } else { result = true; } } } else if (兩條線分别為一條射線與一條線段) { if (線段某端點在射線上) { if (兩條線隻有一個點p重合) { pointSet.insert(p); } else { result = true; } } } return result;
僞代碼看起來很簡單,但實作起來十分繁瑣,這個函數寫了140多行,通過畫圖将各種情況畫出來,然後再寫的代碼,這樣寫條理十厘清楚,也并沒有寫出bug。
更重要一點,在這裡将所有截距相同的線的交點全部算出來了,是以之後算線與線的交點時可以不必考慮平行的線的交點,可直接複用上一次代碼,也節省了計算。
-
判斷點是否線上上:
因為我設計的射線和線段隻是在直線的基礎上加了一個屬性來判斷線的種類,是以算點是否線上上是通過先假定該線為直線,計算出直線的交點坐标(x, y),之後需比較坐标是否在範圍内即可,具體實作過程如下:
這裡我一開始隻傳了橫坐标bool isOnLine(double x, double y) { bool result = true; if (是線段) { if (線段的兩端點橫坐标不相等) { result = (x是否線上段兩端點橫坐标之間); } else { result = (y是否線上段兩縱坐标之間); } } else if (是射線) { if (構造射線的兩端點橫坐标不相等) { result = (x是否在射線延伸的一邊); } else { result = (y是否在射線延伸的一邊); } } return result; }
,認為隻用x
x
就能判斷點是否線上上,這樣就導緻了當線段或者射線兩端點橫坐标相同時,我會把這些交點全當成線上上的點(應為兩端點橫坐标相同,是以交出來的點橫坐标一定在兩點之間),這樣就導緻我的點算多了,我花了很久才定位出這個bug,是以說不能為了省時間沒考慮全面就想當然的寫代碼,否則debug浪費的時間就更多。
另外,我在這個函數中加入了點是否在直線上的判斷後,最終求得的點數量變少了,從理論上進行分析加不加這個判斷對結果應該是沒有影響的,通過調試才發現浮點誤差通過加減乘除運算之後,誤差會變得更大,導緻超過了一開始設定的浮點精度\(10^{-12}\),是以才判斷出計算出直線的交點不在直線上的情況,通過電腦驗算,誤差達到了\(0.03\),說明四則運算對浮點誤差的影響很大,然而我隻考慮了比較時的浮點誤差,沒有考慮計算時誤差會變化的情況,這個地方雖然可以不用加點是否在直線上的判斷進而忽略這個問題,但這個問題是普遍存在的,也不好避免,我想到最後也隻有用不同的精度:計算前取高精度,比較時用低精度。但這樣也不能完全解決問題,這次作業資料範圍是\(10^5\),假設
類型精度為\(10^{-15}\),計算直線時直線的參數\(a,b\)的範圍會變成\(10^5\),精度變成\(10^{-14}\),\(c\)的範圍變成\(10^{10}\),精度變成\(10^{-9}\),然後在計算交點時,分子的範圍會變成\(10^{15}\),精度變成\(10^{-3}\),這已經到了無法忽視的地步了,因為沒考慮精度變化,是以很有可能兩個點相同但是判為不同或者是不同的點判為相同的情況。這個問題我并沒有想到一個好的方法解決。double
-
浮點數的hash函數:
注意浮點數在hash前要保留有效數字,防止浮點誤差對hash值産生影響。
-
接口:
通過讀取資料資訊,傳回交點個數或者抛出異常。
一開始的想法是設計extern "C" void __declspec(dllexport) getPoints(vector<pair<double, double>> & points, stringstream & in);
,addObj()
這些接口的,但是考慮到删除可能和重新導入一遍資料的複雜度差不了多少,是以幹脆就将删除操作定義為先删除資料再将資料重新導入,這樣設計了之後,感覺單單為了一個增加操作而在deleteObj()
上再寫一遍錯誤處理不值得,是以索性就把增加操作也換成導入資料資訊,最後将幾個接口合而為一,變成上述單一的接口,雖然在改動時開銷更大,但是實作起來簡單了許多,也減少了代碼的備援。GUI
5. 閱讀有關 UML 的内容:https://en.wikipedia.org/wiki/Unified_Modeling_Language。畫出 UML 圖顯示計算子產品部分各個實體之間的關系(畫一個圖即可)。

6.計算子產品接口部分的性能改進。記錄在改進計算子產品性能上所花費的時間,描述你改進的思路,并展示一張性能分析圖(由VS 2015/2017的性能分析工具自動生成),并展示你程式中消耗最大的函數。
這是\(6000\)個資料,最終交點個數為\(10146161\)個點的性能分析圖:
因為隻有一個接口,是以這個接口開銷最大:
7. 看 Design by Contract,Code Contract 的内容:
- http://en.wikipedia.org/wiki/Design_by_contract
- http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx
描述這些做法的優缺點,說明你是如何把它們融入結對作業中的。
Design by Contract,即契約式設計,主要是規定接口中各參數的格式,保證一個函數的輸入和輸出都在我們的期望值之内。
優點:我們可以通過JML等比較規範化的注釋來直接驗證程式正确性,有助于代碼的測試;使功能需求更為明确,代碼品質更高。
缺點:這種方式會使程式的開銷增大;這種程式設計方式依賴于程式設計語言的支援;程式設計方式沒有統一的規格,可能會導緻代碼風格的混亂;有時程式的輸入輸出無法準确确定,難以使用契約式設計。
這次結對作業中我們在GUI的設計中就借助了契約式設計,将輸入框使用正規表達式進行規範,強迫使用者輸入符合我們程式要求的輸入。
8. 計算子產品部分單元測試展示。展示出項目部分單元測試代碼,并說明測試的函數,構造測試資料的思路。并 将單元測試得到的測試覆寫率截圖,發表在部落格中。要求總體覆寫率到 90% 以上,否則單元測試部分視作無效。
部分單元測試代碼以及測試的函數如下所示。
TEST_METHOD(TestMethod14)
{
//測試平行于x軸直線,測試line類
Proc p;
string s = "2 L 1 0 2 0\n L -1 1 -1 2";
std::stringstream ss(s);
p.process(ss);
vector <pair<double, double>> result;
p.getPointSet(result);
Assert::AreEqual(1, (int)result.size());
}
TEST_METHOD(TestMethod15)
{
//測試直線、線段、射線之間沖突,測試line類
Proc p;
string s = "7 L 1 3 4 2\n L -1 4 5 2\n S 2 4 3 2\n R 2 5 -1 2\n C 3 3 3\n C 2 2 1\n C 3 2 2";
std::stringstream ss(s);
p.process(ss);
vector <pair<double, double>> result;
p.getPointSet(result);
Assert::AreEqual(20, (int)result.size());
}
TEST_METHOD(TestMethod16)
{
//大型暴力測試,主要測試proc類
Proc p;
string s = "34\nL 1 3 4 2\nL -1 4 5 2\nS 2 4 3 2\nR 2 5 -1 2\nC 3 3 3\nC 2 2 1\nC 3 2 2\nL 99999 99999 -99999 -99999\nL -99998 99998 99998 -99998\nR 0 99 -1 100\nS 0 99 1 98\nS 2 97 1 98\nS 2 97 3 96\nS 4 95 3 96\nS 4 95 5 94\nS 6 93 5 94\nR 99 0 100 -1\nR 99 0 100 1\nR 0 99 -1 -100\nS 0 -99 1 -98\nS 1 -98 2 -97\nS 99 0 98 -1\nS 3 -96 4 -95\nS 2 -97 3 -96\nS 99 0 98 1\nS 11 88 10 89\nS 12 87 11 88\nS 10000 10000 99999 10000\nS 10000 9999 10000 10000\nR 8888 8888 8888 8889\nS 1245 1245 1244 1247\nS 1244 1244 1243 1246\nS 2444 2444 2443 2447\nS 2442 2442 2443 2445\n\n\n\n\n\n\n\n";
std::stringstream ss(s);
p.process(ss);
vector <pair<double, double>> result;
p.getPointSet(result);
Assert::AreEqual(54, (int)result.size());
}
我們構造單元測試的思路主要是首先針對小資料,特定情況,對特定情況的測試大概覆寫完全後進行更大規 模的測試。
單元測試通過截圖:
單元測試覆寫率截圖:
9. 計算子產品部分異常處理說明。在部落格中詳細介紹每種異常的設計目标。每種異常都要選擇一個單元測試樣例釋出在部落格中,并指明錯誤對應的場景。
公共父類:
class InputException :public exception {
private:
string msg;
public:
InputException(string e) {
msg = e;
}
const char* what() const throw() {
cout << msg << endl;
return msg.data();
}
string getMsg() const {
return msg;
}
};
N不在範圍内:
\(1<=N<=500000\)
class NumberException :public InputException {
public:
NumberException() :
InputException("N is out of range!\n") {};
const char* what() const throw() {
cout << getMsg() << endl;
return getMsg().data();
}
};
樣例1:
輸出1:
N is out of range!
樣例2:
500001
輸出2:
N is out of range!
參數不在範圍内:
範圍為 \((-100000, 100000)\)
class OutOfRangeException :public InputException {
public:
OutOfRangeException(int i, string str) :
InputException("Line " + to_string(i) + ": \"" + str + "\"\nThe parameter(s) is(are) out of range!\n") {};
const char* what() const throw() {
cout << getMsg() << endl;
return getMsg().data();
}
};
樣例3:
4
C 3 3 3
S 222222 4 3 2
L -1 4 5 2
R 2 5 -1 2
輸出3:
Line 2: "S 222222 4 3 2"
The parameter(s) is(are) out of range!
構造線的兩點重合:
class CoincideException :public InputException {
public:
CoincideException(int i, string str) :
InputException("Line " + to_string(i) + ": \"" + str + "\"\nThe two points coincide!\n") {};
const char* what() const throw() {
cout << getMsg() << endl;
return getMsg().data();
}
};
樣例4:線段兩點重合
4
C 3 3 3
S 2 4 2 4
L -1 4 5 2
C 2 5 1
輸出4:
Line 2: "S 2 4 2 4"
The two points coincide!
圖形重疊:
class CoverException :public InputException {
public:
CoverException(int i, string str) :
InputException("Line " + to_string(i) + ": \"" + str + "\"\nOverlap with added drawings!\n") {};
const char* what() const throw() {
cout << getMsg() << endl;
return getMsg().data();
}
};
樣例5:圓與圓重合
5
C 3 3 3
S 2 4 3 2
L -1 4 5 2
R 2 5 -1 2
C 3 3 3
輸出5:
Line 5: "C 3 3 3"
Overlap with added drawings!
樣例6:以線段與線段重合為例
5
C 3 3 3
S 2 4 4 0
L -1 4 5 2
R 2 5 -1 2
S 3 2 5 -2
輸出6:
Line 5: "S 3 2 5 -2"
Overlap with added drawings!
對象數目不等于N:
class NumOfObjException :public InputException {
public:
NumOfObjException() :
InputException("The number of geometric objects is not equal to N!\n") {};
const char* what() const throw() {
cout << getMsg() << endl;
return getMsg().data();
}
};
樣例7:對象數大于N
3
C 3 3 3
S 2 4 3 2
L -1 4 5 2
R 2 5 -1 2
輸出7:
The number of geometric objects is not equal to N!
樣例8:對象數小于N
5
C 3 3 3
S 2 4 3 2
L -1 4 5 2
R 2 5 -1 2
輸出8:
The number of geometric objects is not equal to N!
輸入格式錯誤:
class FormatException :public InputException {
public:
FormatException(int i, string str) :
InputException("Line " + to_string(i) + ": \"" + str + "\"\nThe format of input is illgal!\n") {};
const char* what() const throw() {
cout << getMsg() << endl;
return getMsg().data();
}
};
樣例9:參數有前導零
4
C 3 3 3
S 2 4 3 2
L -01 4 5 2
R 2 5 -1 2
輸出9:
Line 3: "L -01 4 5 2"
The format of input is illgal!
樣例10:辨別字母小寫
4
C 3 3 3
s 2 4 3 2
L -1 4 5 2
R 2 5 -1 2
輸出10:
Line 2: "s 2 4 3 2"
The format of input is illgal!
圓的半徑不大于零:
class LessThanZeroException :public InputException {
public:
LessThanZeroException(int i, string str) :
InputException("Line " + to_string(i) + ": \"" + str + "\"\nRadius of circle must be greater than zero!\n") {};
const char* what() const throw() {
cout << getMsg() << endl;
return getMsg().data();
}
};
樣例11:半徑小于0
4
C 3 3 3
S 2 4 3 2
L -1 4 5 2
C 2 5 -1
輸出11:
Line 4: "C 2 5 -1"
Radius of circle must be greater than zero!
樣例12:半徑等于0
4
C 3 3 3
S 2 4 3 2
L -1 4 5 2
C 2 5 0
輸出12:
Line 4: "C 2 5 0"
Radius of circle must be greater than zero!
空檔案:
class EmptyFileException :public InputException {
public:
EmptyFileException() :
InputException("The input file is empty!\n") {};
const char* what() const throw() {
cout << getMsg() << endl;
return getMsg().data();
}
};
樣例13:
輸出13:
The input file is empty!
檔案第一行不是N:
class NoNException :public InputException {
public:
NoNException() :
InputException("The first line of input file is not a number!\n") {};
const char* what() const throw() {
cout << getMsg() << endl;
return getMsg().data();
}
};
樣例14:
C 3 3 3
S 2 4 2 5
L -1 4 5 2
C 2 5 1
輸出14:
The first line of input file is not a number!
指令行參數有誤:
樣例15:
./intersect.exe
輸出15:
Right Format: intersect.exe -i <path to input file> -o <path to output file>
檔案無法打開:
樣例16:
./intersect.exe -i dir -o output.txt
輸出16:
Input File Cannot Open!
最後錯誤處理的單元測試:
10. 界面子產品的詳細設計過程。在部落格中詳細介紹界面子產品是如何設計的,并寫一些必要的代碼說明解釋實作過程。
界面子產品利用VS的Qt插件開發。GUI共含4個視窗。分别如下所示:
第一個視窗為主視窗,在其中我們進行互動、輸出圖像資訊。剩下三個視窗全部用來進行手動導入資料。
主視窗的圖像繪制是我們自己手繪的,因為沒有找到很好的繪制圖像的類。我們将paintEvent函數重定向到widget元件中,使widget元件收到重繪的指令後執行widgetPaint函數對整個畫面重新繪制。
繪制過程中比較重要的是坐标與像素之間的轉換。
double QtPairProject::transferX(double x) {
int pointx = 20;
int width = length - 20 - pointx;
double kx = (double)width / (x_max - x_min );
return pointx + kx * (x - x_min);
}
double QtPairProject::transferY(double y) {
int pointy = wide - 20;
int height = wide - 40;
double ky = (double)height / (y_max - y_min );
return pointy - (y - y_min) * ky + offset;
}
互動控件主要控制以下功能,修改坐标軸的範圍、從檔案導入資料、手動導入資料、删除lineWidget中被選中的資料、清空lineWidget,坐标圖和輸出欄、以及繪制交點功能。右下角的輸出框用于程式運作情況的輸出,例如異常報告、繪制交點情況回報。
其餘三個視窗用于手動導入資料。在這四個視窗之間我們借助signals從子視窗向父視窗發送資訊。
例如在主視窗QtPairProject.h中使用
signals:
void sendsignal(QString);
在QtPairProject.cpp中使用
void QtPairProject::OnOpenButtonClick() {
win->show();
}
void QtPairProject::getData(QString data) {
this->show();
}
接收子視窗向父視窗發送的資訊。
在子視窗NewQtGuiClass.h中定義
public slots:
void sendData();
在NewQtGuiClass.cpp中使用
NewQtGuiClass::NewQtGuiClass(QWidget *parent)
: QWidget(parent)
{
ui.setupUi(this);
connect(ui.pushButton, SIGNAL(clicked()), this, SLOT(pushButton()));
}
void NewQtGuiClass::sendData() {
emit sendsignal(buf);
buf = "";
this->close();
}
向父視窗傳遞資訊。
在子視窗的資料輸入以及修改坐标範圍的輸入中,我們采用了正規表達式檢驗以及整型數驗證控件來限制輸入的範圍。
QRegExp rx("^-?\\d{1,6}$");
QRegExpValidator* v = new QRegExpValidator(rx, this);
QIntValidator *intValidator = new QIntValidator;
intValidator->setRange(-100000, 100000);
ui.lineEdit->setValidator(v);
ui.lineEdit_2->setValidator(v);
intValidator->setRange(0, 100000);
ui.lineEdit_3->setValidator(intValidator);
11. 界面子產品與計算子產品的對接。詳細地描述 UI 子產品的設計與兩個子產品的對接,并在部落格中截圖實作的功能。
我們通過輸入流來實作界面子產品與計算子產品的對接。
extern "C" void __declspec(dllexport) getPoints(vector<pair<double, double>> & points, stringstream & in);
即GUI元件将輸入打包成控制台輸入的格式通過流傳輸給計算子產品,由計算子產品傳回計算得到的交點數組。
實作的功能:
修改坐标範圍:
從檔案導入:
手動導入
删除所選資料
繪制交點
清空:
12. 描述結對的過程,提供兩人在讨論的結對圖像資料(比如 Live Share 的截圖)。關于如何遠端進行結對參見作業最後的注意事項。
我們的結對過程主要通過QQ的螢幕共享以及語音進行,同時由于網絡及工作時間的差别,我們也經常通過微信進行交流。
螢幕共享:
微信交流:
13. 看教科書和其它參考書,網站中關于結對程式設計的章節,例如:http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html ,
說明結對程式設計的優點和缺點。同時描述結對的每一個人的優點和缺點在哪裡(要列出至少三個優點和一個缺點)。
結對程式設計優點:
優點在于随時都在進行測試,代碼的錯誤率低。同時開發效率在有隊友監督的情況下也得到了很大的提高,連摸魚的時間都沒了。
整個程式設計過程都是給自己的極限壓力測試,整個項目做下來感覺收獲頗多。
結對程式設計缺點:
剛開始的磨合期有些磕磕絆絆,配合上不夠舒服。
兩人的寫代碼習慣不同時可能會有些别扭。
如果兩人的程式設計能力差距太差就會變成較強的人的一個人的舞台,失去了結對程式設計的優勢。
每個人的優點:
杜博玮:經常與對方進行溝通、善于查找資料、有耐心
陳馳:代碼能力強、細心、工作效率高
每個人的缺點:
杜博玮:代碼錯誤處理不夠好
陳馳:有一些腼腆