交點求解大師——結對項目做業

交點求解大師——結對項目做業

1、做業要求簡介

本文是北京航空航天大學計算機學院軟件工程課程的結對項目做業,在本次做業中,兩位同窗一組,以結對編程的方式共同完成一項需求。html

結對編程是軟件工程中的一種開發方法,兩我的肩並肩坐在一塊兒,共用一塊屏幕和一份鍵盤鼠標,共寫一份代碼。兩我的有不一樣的分工,領航者負責指明方向,執行者負責動手寫代碼,在兩人的默契配合下,造成一種無間隙的代碼複審模式,使開發出的程序質量更高。python

項目 內容
本做業屬於北航軟件工程課程 博客園班級博客
做業要求請點擊連接查看 結對項目做業
班級:006 Sample
GitHub項目地址 IntersectProject
GUI項目地址 IntersectionGUI
同組同窗博客連接 eitbar
我在這門課程的目標是 得到成爲一名軟件工程師的能力
這個做業在哪一個具體方面幫助我實現目標 實踐結對編程

2、PSP表格

PSP2.1 Personal Software Process Stages 預估耗時(分鐘) 實際耗時(分鐘)
Planning 計劃 60 50
· Estimate · 估計這個任務須要多少時間 60 50
Development 開發 1800 1630
· Analysis · 需求分析 (包括學習新技術) 600 600
· Design Spec · 生成設計文檔 60 40
· Design Review · 設計複審 (和同事審覈設計文檔) 60 60
· Coding Standard · 代碼規範 (爲目前的開發制定合適的規範) 20 20
· Design · 具體設計 60 50
· Coding · 具體編碼 600 500
· Code Review · 代碼複審 600(同時) 500(同時)
· Test · 測試(自我測試,修改代碼,提交修改) 400 360
Reporting 報告 300 360
· Test Report · 測試報告 30 30
· Size Measurement · 計算工做量 30 30
· Postmortem & Process Improvement Plan · 過後總結, 並提出過程改進計劃 240 300
合計 2160 2040

3、接口思想與接口設計

(一)接口設計原則

  • 單一指責:一個接口就幹一個事
  • 數據格式:輸入輸出定義清晰簡單,不能產生歧義,最好爲傻瓜式的,讓調用者毫不會錯用,有些經驗的用戶能夠直接使用API而不須要閱讀文檔。
  • 方便易用:包括命名清晰簡潔,最小驚訝原則

(二)接口實現

基於上述原則,咱們設計了以下兩個函數做爲計算核心模塊的接口,能夠自由的與命令行和GUI進行對接。知足上述三條設計原則。這兩個函數都會調用計算核心模塊,不一樣之處是輸入輸出格式。c++

#ifdef IMPORT_DLL
#else
#define IMPORT_DLL extern "C" _declspec(dllimport) //指的是容許將其給外部調用
#endif

IMPORT_DLL int guiProcess(std::vector<std::pair<double,double>> *points, std::string msg);

IMPORT_DLL void cmdProcess(int argc, char *argv[]);
  • guiProcess函數傳入符合格式的字符串,交點集用指針返回,交點個數用返回值返回。
    • 由數據格式的設計原則,只使用c++標準庫中的容器,而不用自定義數據類型。如CPoint要轉成std::pair<double,double>表示,保證調用者清晰理解。
    • 輸入採用特定格式的字符串,符合方便易用原則,若是格式錯誤,由核心處理並拋出異常
  • cmdProcess爲了命令行調用方便,採用需求規定的命令行輸入格式進行輸入輸出。

注:這兩個函數並非分別針對gui和cmd,只是輸入輸出格式不一樣,均可以任意調用,保證鬆耦合。git

4、5、計算模塊設計文檔與UML

(一)PipeLine

PreProcess

  • ReadShape:讀取文件接收所有輸入的直線和圓
  • Shape construct:根據輸入構建形狀對象,計算直線斜率。
  • Classified by Slope:按斜率將直線分組存起來。
  • 【新增】CalcIns Same Slope:處理在同一直線上的線段、射線的共端點狀況。

CalcIntersect

  • CalcLines:計算全部直線、射線、線段之間的交點:
    • 依次考慮每一個平行組,按每條線遍歷計算交點。平行組內的線不用計算交點。
    • 查交點表,若是存在,就能夠不求同一交點的其餘線了。
      交點表:Map<點,Vector<線>>
      維護交點表:新增的交點加入交點表,線加入表中對應的線集
    • 射線和線段的交點還要知足在射線和線段範圍內纔有效
  • CalcCircles:全部線算完後,再一個個遍歷圓。
    • 暴力求其與以前圖形的所有交點
    • 一樣須要考慮射線和線段的範圍問題

(二)類間關係圖(UML)

  • CIntersect類:實現控制流,方法包含輸入計算兩圖形交點計算交點總數
  • CShape類:圖形類基類,爲每一個圖形實例建立惟一id,並記錄圖形的類型
  • 【新增】CLine類:繼承圖形類基類,做用爲表示直線、線段、射線的代數方程參數。
  • CCircle類:繼承圖形類基類,做用爲表示圓的代數方程參數。
  • 直線方程兩種表示方法
    • 通常方程:\(Ax + By +C = 0\)
    • 斜截方程:\(y = kx + b\)
    • 圓方程兩種表示
      • 通常方程: \(x^2 + y^2 + Dx + Ey +F = 0\)
      • 標準方程: \((x-x_0)^2 + (y-y_0)^2 = r^2\)
  • CSlope類和CBias類:爲解決斜率無窮大設計,isInf和isNan爲true時表示直線的斜率爲無窮,此時k和b的具體值無效。因爲要按斜率分組,採用C++STL的unordered_setunordered_map,CSlope要實現Hash方法等於運算符
  • CPoint類:表示交點,做爲map的key,一樣須要實現Hash方法等於運算符

(三)關鍵函數

  • inputShapes: 處理輸入函數,直線、射線、線段按斜率分組,放到map<double, set<CLine>>_k2lines中,圓直接放到set<CCircle>_circles裏。github

    【新增】:上一次需求中直線和直線平行不可能產生有限交點,可是這次需求新增的線段和射線就可能產生共線相交在端點的狀況,是符合需求說明書的狀況,須要特殊考慮。在此函數中實現,後續就能夠正常按照平行分組計算了。正則表達式

  • calcShapeInsPoint:求兩個圖形交點的函數,分三種狀況,返回點的vector。算法

    • 直線與直線
    • 直線與圓
    • 圓與圓
  • cntTotalInsPoint: 求全部焦點的函數,按先直線後圓的順序依次遍歷求焦點。已經遍歷到的圖形加入一個over集中。編程

    • 直線兩個剪枝方法:
      • 砍平行:依次加入每一個平行組,不需計算組內直線交點,只需遍歷over集中其它不平行直線。
      • 砍共點:倘若ABC共點,按ABC的順序遍歷,先計算了AB,交點爲P;以後計算AC時發現交點也是P,則無需計算BC交點。方法爲維護_insp2shapes這個map<CPoint, vector<CShape>>數據結構,爲交點到通過它的線集的映射。
    • 再依次遍歷圓,暴力求焦點。加到_insp2shapes
    • 函數返回_insp2shapes.size()即爲交點個數。

(四)【新增】代碼說明

具體說明本次需求【新增】的重要代碼canvas

1. 判斷求交公式算出的交點是否在射線、線段範圍內
// require: cx cy belongs to the line
// return: if point in shape range
bool CLine::crossInRange(double cx, double cy) const
{
	if (type() == "Line") { // 統一接口,直線返回true
		return true;
	}
	else if (type() == "Ray") { // 射線
		if (k().isInf()) { // 斜率無窮,比較y值,dcmp爲浮點數比較函數,定義見下
			if (dcmp(cy, y1())*dcmp(y2(), y1()) != -1) {
				return true;
			}
		}
		else { // 正常狀況,比較x
			if (dcmp(cx, x1())*dcmp(x2(), x1()) != -1) {
				return true;
			}
		}
		return false;
	}
	else { // 線段
		... //相似於射線,代碼略
	}
}
// 浮點數比較函數,相等返回0,前者大返回1,不然返回-1
#define EPS 1e-10
int dcmp(double d1, double d2) {
	if (d1 - d2 > EPS) {
		return 1;
	}
	else if (d2 - d1 > EPS) {
		return -1;
	}
	else {
		return 0;
	}
}
2. 浮點數hash方法

浮點數因爲有浮點偏差,直接用hash<double>的值將不一樣,因此應該截取某精度,轉換成整形進行hash。設計模式

#define COLLISION 100000.
class SlopeHash
{
public:
	std::size_t operator()(const CSlope& s) const
	{ // 乘精度,四捨五入,轉整形,算Hash
		unsigned long long val = dround2ull(s.val() * COLLISION);
		return std::hash<bool>()(s.isInf()) + (std::hash<unsigned long long>()(val) << 16);
	}
};

5、計算模塊接口部分的性能改進

咱們隨機生成了8000條几何圖形數據用於性能測試,其中直線、線段、射線、圓各2000個,最終計算獲得交點數爲18175002個交點。隨機數生成模塊爲python中random包。

(一)第一版性能測試

VS2019中使用性能探查器,分析CPU利用率以下:

能夠看出,耗費時間最多的函數,是計算輸入全部圖形的交點總數的函數cntTotalInsPoint,進入函數入內部查看分析結果:

與我的項目相似,耗費時間最多的仍然是記錄交點的數據結構set對交點的插入。因爲已經使用unordered_set相比於本來的set時間複雜度已經低了不少,所以固有的數據結構維護時間不可避免。其次咱們還注意到計算兩個圖形間交點的函數calcShapeInsPoint佔用了較大的時間開銷。進入該函數中查看分析結果:

能夠看出判斷點是否在線段或射線上,花費時間較多,判斷點是否在交點上的函數,內部是這樣的:

仔細想一想後,發現其實徹底沒有必要再這個函數內部再判斷一次點是否在線端或射線所處直線上,由於咱們自己會用到這個函數的場景,就是先計算出線段或射線所在直線與其餘直線的交點,再判斷交點是否在線段或射線上,所以,這個判斷屬於畫蛇添足,是一個能夠優化的點。

(二)優化後性能分析

刪除點是否在線上的判斷後,再次使用性能分析,結果以下:

能夠發現總時間下降了4~5s,再次進入calcShapeInsPoint函數中查看

能夠發現crossInRange函數已經再也不是花費最多的部分,說明仍是頗有效果的。其他部分優化也都相似,不斷對比分析,刪除冗餘計算結果。

6、契約式設計

契約式設計一種設計計算機軟件的方法。這種方法要求軟件設計者爲軟件組件定義正式的,精確的而且可驗證的接口,這樣,爲傳統的抽象數據類型又增長了先驗條件、後驗條件和不變式。這種方法的名字裏用到的「契約」是一種比喻,由於它和商業契約的狀況有點相似。

在面向對象課程中,咱們就接觸過契約式設計,將邏輯約束在設計時定義好,編碼實現時只須要遵照契約式設計就能夠寫出正確的代碼,減小了出錯的可能性。利用一些現有的軟件,還能夠利用契約式設計作代碼正確性的形式證實。

從上次做業開始,個人重要pipeline函數就採用了契約式設計模式,下面舉例說明。

//算直線和圓的交點的函數
// calculate Intersections of one circ and one line
// need: para1 is CCirc, para2 is CLine
// return a vector of intersections. size can be 0,1,2.
std::vector<CPoint> calcInsCircLine(const CShape& circ, const CShape& line)
{	... }
//算任意兩圖形交點的函數
// calculate all intersect points of s1 and s2
// return the points as vector
// need: s1, s2 should be CLine or CCircle.
// special need: if s1, s2 are CLine. They cannot be parallel.
std::vector<CPoint> CIntersect::calcShapeInsPoint(const CShape& s1, const CShape& s2) const
{ ... }
//計算交點總數的函數
// the main pipeline: loop the inputs and fill in _insp2shapes or _insPoints
// return the total count of intersect points
// need: _k2lines and _circles have been filled
int CIntersect::cntTotalInsPoint() { ... }

在設計時我都是先寫出契約,以後的代碼實現中嚴格遵照契約中的約束,好比使用calcShapeInsPoint函數時就必須遵照下面約束,不然將產生錯誤。

// special need: if s1, s2 are CLine. They cannot be parallel.

7、單元測試

首先展現使用OpenCppCoverage插件測試獲得代碼覆蓋率爲:99%

其中部分Uncovered line是因爲VS強大的編譯優化,把一些函數在內聯處理了,因此執行不到。

在詢問了一些同窗而且到網上查詢以後,咱們仍是沒有找到如何直接將VS的單元測試項目加入OpenCppCoverage的代碼覆蓋檢測中,所以咱們選擇將單元測試代碼手動從單元測試項目中移入主函數中。主要測試結構以下:

int main() {
    ...
    ...
    //從單元測試項目中轉移至此的單元測試1
    ...
    //從單元測試項目中轉移至此的單元測試2
    ...
}

單元測試中,主要包括文件讀寫的測試,對一些關鍵函數如計算交點等的測試,對異常的測試。

部分單元測試代碼展現以下:

  • 測試圖形之間交點的計算:該部分比較繁雜,須要考慮的狀況有如下幾種,我的項目中已經出現過的就再也不放測試代碼,與上次相似

    • 直線與直線僅有一個交點、沒有交點,其中須要包括直線斜率不存在、爲0、其餘的狀況

    • 直線與圓有兩個交點、一個交點、沒有交點,其中須要包括直線斜率不存在、爲0、其餘的狀況

    • 直線與線段有一個交點、沒有交點,其中須要包括直線與線段平行、不平行有一個交點、不平行沒有交點的狀況。如下例子爲一個交點的狀況:

      CLine t1 = CLine(0, 2, 0, 0, "Seg");
      CLine t2 = Cline(3, 2, 4, 2);
      ans = ins.calcShapeInsPoint(t1, t2);
      Assert::AreEqual(1, (int)ans.size());
      Assert::AreEqual(true, CPoint(0, 2) == ans[0]);
    • 直線與射線有一個交點、沒有交點,其中須要包括直線與射線平行、不平行有一個交點、不平行沒有交點的狀況。如下例子爲沒有交點的狀況:

      CLine t1 = CLine(0, 2, 0, 0, "Ray");
      CLine t2 = Cline(3, 2, 3, 4);
      ans = ins.calcShapeInsPoint(t1, t2);
      Assert::AreEqual(0, (int)ans.size());
    • 圓與圓有兩個交點、一個交點、沒有交點,其中須要包括外離、外切、相交、內切、內含的狀況

    • 圓與線段有兩個交點、一個交點、沒有交點,其中須要包含線段所在直線與圓相離、相切、相交以及不一樣交點數的狀況。如下例子爲兩個交點的狀況:

      CCircle c = CCircle(0, 0, 2);
      CLine t1 = Cline(2, 0, 0, 2, "Seg");
      ans = ins.calcShapeInsPoint(t1, c);
      Assert::AreEqual(2, (int)ans.size());
      Assert::AreEqual(true, CPoint(0, 2) == ans[0]);
      Assert::AreEqual(true, CPoint(2, 0) == ans[1]);
    • 圓與射線有兩個交點、一個交點、沒有交點,其中須要包含射線所在直線與圓相離、相切、相交以及不一樣交點數的狀況。如下例子爲一個交點的狀況:

      CCircle c = CCircle(0, 0, 2);
      CLine t1 = Cline(0, 0, 0, 2, "Ray");
      ans = ins.calcShapeInsPoint(c, t1);
      Assert::AreEqual(1, (int)ans.size());
      Assert::AreEqual(true, CPoint(0, 2) == ans[0]);
    • 線段與線段有一個交點、沒有交點,其中須要包含兩個線段所在直線之間的各類關係以及不一樣交點數的狀況,須要特別考慮線段與線段共線時端點重合的狀況。如下例子爲共線時一個交點的狀況:

      CLine t1 = Cline(0, 0, 0, 2, "Seg");
      CLine t2 = Cline(0, -2, 0, 0, "Seg");
      ans = ins.calcShapeInsPoint(t1, t2);
      Assert::AreEqual(1, (int)ans.size());
      Assert::AreEqual(true, CPoint(0, 0) == ans[0]);
    • 線段與射線有一個交點、沒有交點,其中須要包含線段與射線所在直線之間的各類關係以及不一樣交點數的狀況,須要特別考慮線段與射線共線時端點重合的狀況。如下例子爲共線時一個交點的狀況:

      CLine t1 = Cline(0, 2, 0, 0, "Seg");
      CLine t2 = Cline(0, 0, 0, -2, "Ray");
      ans = ins.calcShapeInsPoint(t1, t2);
      Assert::AreEqual(1, (int)ans.size());
      Assert::AreEqual(true, CPoint(0, 0) == ans[0]);
    • 射線與射線有一個交點、沒有交點,其中須要包含兩個射線所在直線之間的各類關係以及不一樣交點數的狀況,須要特別考慮射線與射線共線時端點重合的狀況。如下例子爲共線時沒有交點的狀況:

      CLine t1 = Cline(0, 0, 2, 0, "Ray");
      CLine t2 = Cline(-1, 0, -2, 0, "Ray");
      ans = ins.calcShapeInsPoint(t1, t2);
      Assert::AreEqual(0, (int)ans.size());
  • 交點數統計測試:測試各類狀況下,如不一樣圖形之間有重合交點、圖形數量不多、圖形數量不少等,交點統計是否正確。舉例:

    TEST_METHOD(TestMethod10)
    {
    	ifstream fin("../test/test10.txt");
    	if (!fin) {
    		Assert::AreEqual(132, 0);
    	}
    	CIntersect ins;
    	ins.inputShapes(fin);
    	int cnt = ins.cntTotalInsPoint();
    	Assert::AreEqual(433579, cnt);
    }
  • 異常單元測試:對各類異常狀況的單元測試,主要是經過構造異常數據傳入輸入函數,捕捉其拋出的異常與預期異常的信息進行比較,將在第八章中詳細說明,這裏僅放出幾個樣例。

    • 測試射線與射線重合的狀況(其中之一)

      TEST_METHOD(TestMethod_RR1)
      {
      	string strin = "2\nR 0 0 5 5\nR 6 6 -1 -1\n";
      	ShapeCoverException s(2, "R 6 6 -1 -1", "R 0 0 5 5");
      	InputHandlerException std = s;
      	istringstream in(strin);
      	if (!in) {
      		Assert::AreEqual(132, 0);
      	}
      	CIntersect ins;
      	try {
      		ins.inputShapes(in);
      		Assert::AreEqual(132, 0);
      	}
      	catch (InputHandlerException e) {
      		Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0);
      	}
      }
    • 測試射線與線段重合的狀況(其中之一)

      TEST_METHOD(TestMethod_SR1)
      {
      	string strin = "3\nS 0 0 0 4\nS 0 0 4 0\nR 0 -2 0 -1";
      	ShapeCoverException s(3, "R 0 -2 0 -1", "S 0 0 0 4");
      	InputHandlerException std = s;
      	istringstream in(strin);
      	if (!in) {
      		Assert::AreEqual(132, 0);
      	}
      	CIntersect ins;
      	try {
      		ins.inputShapes(in);
      		Assert::AreEqual(132, 0);
      	}
      	catch (InputHandlerException e) {
      		Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0);
      	}
      }
    • 測試線段與線段重合的狀況(其中之一)

      TEST_METHOD(TestMethod_SS1)
      {
      	string strin = "3\nS 0 -1 0 1\nS 0 3 0 6\nS 0 0 0 2\n";
      	ShapeCoverException s(3, "S 0 0 0 2", "S 0 -1 0 1");
      	InputHandlerException std = s;
      	istringstream in(strin);
      	if (!in) {
      		Assert::AreEqual(132, 0);
      	}
      	CIntersect ins;
      	try {
      		ins.inputShapes(in);
      		Assert::AreEqual(132, 0);
      	}
      	catch (InputHandlerException e) {
      		Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0);
      	}
      }

8、異常處理

計算模塊異常處理大體分類以下,均繼承c++標準異常:

  • 輸入異常:
    • 有關圖形數量的異常:如沒法讀入NN不符合規範、N與圖形數量不匹配等
    • 有關圖形的異常:如輸入圖形爲非法格式、數字範圍超出約束、圖形重複輸入、圖形與已有圖形重合、線類圖形輸入的兩點重合等
  • 計算異常:目前並無發現須要特殊捕捉的計算過程當中產生的標準異常以外的異常,待將來拓展。
  • 文件讀寫異常:沒法打開文件、文件不存在等異常。

異常類的UML圖以下:

關鍵異常詳細介紹:

  • ShapeNumberException

    當沒法從輸入流中讀入N、讀入的N範圍不符合規範、N與實際輸入的圖形數量不匹配時,拋出該異常。

    該異常構造方式與標準異常相同,傳入錯誤信息字符串,可使用what()函數獲取錯誤信息,獲取到的錯誤信息與傳入錯誤信息相同。如:

單元測試舉例以下:

TEST_METHOD(TestMethod_N6) {
	string strin = "1\nL 1 2 3 4\nC 1 1 2";
	ShapeNumberException s("The number of graphics is larger than N.");
	InputHandlerException std = s;
	istringstream in(strin);
	if (!in) {
		Assert::AreEqual(132, 0);
	}
	CIntersect ins;
	try {
		ins.inputShapes(in);
		Assert::AreEqual(132, 0);
	}
	catch (InputHandlerException e) {
		Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0);
	}
}
  • IllegalFormatException

    當讀入過程當中某行既不是空行、也不符合規定的輸入格式即(L <x1> <y1> <x2> <y2>R <x1> <y1> <x2> <y2>S <x1> <y1> <x2> <y2>C <x> <y> <r>四種格式中的一種)時,拋出該異常。

    該異常構造方式爲,傳入格式錯誤的圖形序號和該行字符串,可使用what()函數獲取錯誤信息,獲取到的錯誤信息爲序號、字符串以及相應修改建議。如:

單元測試舉例以下:

TEST_METHOD(TestMethod_ILLShape)
{
	string strin = "1\n\nL 1 2 3 F\n";
	IllegalFormatException s(1,"L 1 2 3 F");
	InputHandlerException std = s;
	istringstream in(strin);
	if (!in) {
		Assert::AreEqual(132, 0);
	}
	CIntersect ins;
	try {
		ins.inputShapes(in);
		Assert::AreEqual(132, 0);
	}
	catch (InputHandlerException e) {
		Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0);
	}
}
  • OutRangeException

    當讀入圖形信息時,圖形的參數超過規定的範圍,拋出該異常。

    該異常構造方式爲,傳入參數超過規定範圍的圖形序號和該行字符串,可使用what()函數獲取錯誤信息,獲取到的錯誤信息爲序號、字符串以及相應修改建議。如:

單元測試舉例以下:

TEST_METHOD(TestMethod_OutRange)
{
	string strin = "1\n\nC 1 1 0\n";
	OutRangeException s(1, "C 1 1 0");
	InputHandlerException std = s;
	istringstream in(strin);
	if (!in) {
		Assert::AreEqual(132, 0);
	}
	CIntersect ins;
	try {
		ins.inputShapes(in);
		Assert::AreEqual(132, 0);
	}
	catch (InputHandlerException e) {
		Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0);
	}
}
  • ShapeRepeatedException

    當讀入某一圖形,發現與已有圖形徹底相同時,拋出該異常。

    該異常構造方式爲,傳入出現重複的圖形序號和該行字符串,可使用what()函數獲取錯誤信息,獲取到的錯誤信息爲序號、字符串以及相應修改建議。如:

單元測試舉例以下:

TEST_METHOD(TestMethod_ShapeRe)
{
	string strin = "2\n\nC 1 1 1\nC 1 1 1\n";
	ShapeRepeatedException s(2, "C 1 1 1");
	InputHandlerException std = s;
	istringstream in(strin);
	if (!in) {
		Assert::AreEqual(132, 0);
	}
	CIntersect ins;
	try {
		ins.inputShapes(in);
		Assert::AreEqual(132, 0);
	}
	catch (InputHandlerException e) {
		Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0);
	}
}
  • IllegalLineException

    當讀入直線、線段、射線,且發現描述線類圖形的兩個點重合時,拋出該異常。

    該異常構造方式爲,傳入端點重合的圖形序號和該行字符串,可使用what()函數獲取錯誤信息,獲取到的錯誤信息爲序號、字符串以及相應修改建議。如:

單元測試舉例以下:

TEST_METHOD(TestMethod_IllLine)
{
	string strin = "2\n\nL 1 1 1 2\nL 0 1 0 1\n";
	IllegalLineException s(2, "L 0 1 0 1");
	InputHandlerException std = s;
	istringstream in(strin);
	if (!in) {
		Assert::AreEqual(132, 0);
	}
	CIntersect ins;
	try {
		ins.inputShapes(in);
		Assert::AreEqual(132, 0);
	}
	catch (InputHandlerException e) {
		Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0);
	}
}
  • ShapeCoverException

    當讀入直線、線段、射線等與已有線類圖形重合(即有無限個交點)時,拋出該異常。

    該異常構造方式爲,傳入該圖形(指新讀入的圖形)序號、該圖形字符串信息、與其重合的圖形字符串信息,可使用what()函數獲取錯誤信息,獲取到的錯誤信息爲序號、該圖形、與其重合的圖形信息及相應修改建議。如:

單元測試舉例以下:

TEST_METHOD(TestMethod_RS1)
{
	string strin = "3\nS 0 0 4 0\nR 0 -2 0 -1\nS 0 0 0 4";
	ShapeCoverException s(3, "S 0 0 0 4", "R 0 -2 0 -1");
	InputHandlerException std = s;
	istringstream in(strin);
	if (!in) {
		Assert::AreEqual(132, 0);
	}
	CIntersect ins;
	try {
		ins.inputShapes(in);
		Assert::AreEqual(132, 0);
	}
	catch (InputHandlerException e) {
		Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0);
	}
}

9、界面模塊

首先給出界面模塊的總體外觀

(一)界面模塊設計

  • 輸入面板模塊:提供添加、刪除、清空功能

    • 添加:提供文件導入和手動添加兩種方式,添加後在列表框中顯示,並繪製到繪圖模塊中
    • 刪除:在列表框中勾選,點擊刪除鍵便可刪除選中圖形,並自動在繪圖模塊中刪除
    • 清空:清空全部圖形,並清空繪圖面板
  • 計算核心接口:添加圖形後,點擊求解交點調用計算核心模塊,返回交點個數對話框,並在繪圖面板中繪製交點,後文將詳述接口函數的設計。

  • 繪圖面板模塊:在添加圖形時繪製圖形,在計算交點後繪製交點。

  • 錯誤反饋模塊:反饋計算核心模塊拋出的異常,例如

    • 直線兩點重合

  • 產生無數交點

  • 輸入數據範圍錯誤

(二)關鍵代碼說明

本項目的GUI採用c++的Qt庫實現,對比之前使用過的MFC,Qt有易上手,跨平臺,界面美觀等特色。對於不多寫圖形程序的我來講,採用Qt是一個很合適的選擇。下面分別介紹各個模塊的關鍵代碼實現。

  • 輸入面板模塊:充分利用Qt ui設計器提供的諸多功能強大的組件

    • 整個輸入面板置於可浮動拖出的Dock組建(窗體左側部分),對於屏幕分辨率低的用戶,可將輸入面板分離出主窗口,讓繪圖面板佔據所有主窗口

    • 輸入數據部分選用QComboBox和QSpinBox等組件

    • 按鈕利用Qt提供的QToolButton組件,方便定義樣式和在工具欄重用

    • 代碼主要定義添加,刪除,清空按鈕的槽函數,以文件添加爲例展現代碼

void IntersectionGUI::on_actAddFile_triggered() {
	if (lastPath == QString::fromLocal8Bit("")) { //記錄上次打開的路徑
		lastPath = QDir::currentPath();//獲取系統當前目錄
	}
	//獲取應用程序的路徑
	QString dlgTitle = QString::fromLocal8Bit("選擇一個文件"); //對話框標題
	QString filter = QString::fromLocal8Bit("文本文件(*.txt);;全部文件(*.*)"); //文件過濾器
	QString aFileName = QFileDialog::getOpenFileName(this, dlgTitle, lastPath, filter);
	if (!aFileName.isEmpty()) {
		lastPath = aFileName;
    // 讀入文件數據,文件格式與需求定義相同
		std::ifstream fin(aFileName.toLocal8Bit());
		int N;
		std::string line;
		fin >> N;
		std::getline(fin, line);
		while (N--) {
			std::getline(fin, line);
			QString qline = QString::fromStdString(line);
			if (!isShapeStrValid(qline)) { // 正則表達式簡單判斷輸入格式
				QString dlgTitle = QString::fromLocal8Bit("輸入格式錯誤");
				QMessageBox::critical(this, dlgTitle, qline);
				break;
			}
			draw_shape_from_str(qline); //在繪圖面板上繪圖
      // 圖形列表中添加一行
			QListWidgetItem * aItem = new QListWidgetItem(); //新建一個項
			aItem->setText(qline); //設置文字標籤
			aItem->setCheckState(Qt::Unchecked); //設置爲選中狀態
			aItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
			ui.listWidget->addItem(aItem); //增長一個項
		}
	}
}
  • 計算核心接口與異常處理模塊:用戶點擊求解交點按鈕後,調用槽函數on_actSolve_triggered
void IntersectionGUI::on_actSolve_triggered()
{
  // 從圖形列表中構造給核心接口的輸入字符串
	std::string input;
	input += std::to_string(ui.listWidget->count()) + "\n";
	for (int i = 0; i < ui.listWidget->count(); i++)
	{
		QListWidgetItem *aItem = ui.listWidget->item(i);//獲取一個項
		QString str = aItem->text();
		input += str.toStdString() + "\n";
	}
  // 調用核心接口
	std::vector<std::pair<double, double> > points;
	int cnt = 0;
	try {
		cnt = guiProcess(&points, input); // 調用核心接口dll函數
	}
	catch (std::exception e) { // 反饋計算核心拋出的異常
		QString dlgTitle = QString::fromLocal8Bit("計算出現錯誤");
		QMessageBox::critical(this, dlgTitle, e.what());
		return;
	}
	// 繪製交點  
	for (auto vit = points.begin(); vit != points.end(); ++vit) {
		int w, h;
		xy2wh((int)(vit->first), (int)(vit->second), w, h);
		draw_point(w, h, Qt::red);
	}
  // 反饋交點總數
	QString dlgTitle = QString::fromLocal8Bit("計算結果");
	QString strInfo = QString::fromLocal8Bit("交點總數爲:");
	strInfo += strInfo.asprintf("%d", cnt);
	QMessageBox::information(this, dlgTitle, strInfo,QMessageBox::Ok, QMessageBox::NoButton);
}
  • 繪圖面板模塊:採用QLabel和QPixmap組件進行繪圖,主要的繪圖函數有如下幾種

    • 繪製直線、射線
    • 繪製線段
    • 繪製圓
    • 繪製點
    • 座標軸初始化

    最難寫的函數就是繪製直線、射線,Qt自帶的繪製直線函數drawLine其實是給定兩點繪製線段,因此想要達到繪製直線和線段的效果就必須求出與畫板邊界的交點。先根據方向算左右方向的邊界,若是發現碰的不是左右邊界,再算上下邊界。

void IntersectionGUI::draw_ray(int x1, int y1, int x2, int y2, QColor const c, int const w) {
	QPainter Painter(&curPixmap);
	Painter.setRenderHint(QPainter::Antialiasing, true); //反走樣
	Painter.setPen(QPen(c, w));
	if (x2 == x1) { // 豎直
		if (y2 > y1) {
			y2 = REAL_SIZE;
		}
		else {
			y2 = -REAL_SIZE;
		}
	}
	else if (y1 == y2) { // 水平
		if (x2 > x1) {
			x2 = REAL_SIZE;
		}
		else {
			x2 = -REAL_SIZE;
		}
	}
	else { // 向右上傾斜 先算左右邊界交點,超範圍就算上下交點
		double k = (double)(y2 - y1) / (x2 - x1);
		double b = y1 - k * x1;
		if (x2 > x1) { 
			double y_ = REAL_SIZE * k + b;
			if (y_ > REAL_SIZE) { 
				y2 = REAL_SIZE;
				x2 = (y2 - b) / k;
			}
			else if (y_ < -REAL_SIZE) {
				y2 = -REAL_SIZE;
				x2 = (y2 - b) / k;
			}
			else {
				x2 = REAL_SIZE;
				y2 = y_;
			}
		}
		else { ... } // 相似略
	}
	QPoint p1 = xy2whPoint(x1, y1);
	QPoint p2 = xy2whPoint(x2, y2);
	Painter.drawLine(p1, p2);
	ui.canvas->setPixmap(curPixmap);
}

10、界面與核心的對接

(一)接口函數設計

爲了設計鬆耦合,咱們將核心部分設計了以下兩個函數做爲接口,命令行和GUI均可以經過這兩個接口訪問計算核心。

#ifdef IMPORT_DLL
#else
#define IMPORT_DLL extern "C" _declspec(dllimport) //指的是容許將其給外部調用
#endif

IMPORT_DLL int guiProcess(std::vector<std::pair<double,double>> *points, std::string msg);

IMPORT_DLL void cmdProcess(int argc, char *argv[]);
  • guiProcess函數傳入符合格式的字符串,交點集用指針返回,交點個數用返回值返回。
  • cmdProcess採用需求規定的命令行輸入格式進行輸入輸出

注:這兩個函數並非分別針對gui和cmd,只是輸入輸出格式不一樣,均可以任意調用,保證鬆耦合。

(二)導出dll

經過此接口將計算核心封裝成動態連接庫calcInterface.dll和庫calcInterface.lib

(三)模塊對接

  • 命令行接口對接
int main(int argc, char *argv[])
{
	typedef int (*pGui)(vector<pair<double,double>>* points, string a);
	typedef void (*pCmd)(int argc, char* argv[]);
	
	HMODULE hDLL = LoadLibrary(TEXT("calcInterface.dll"));

	vector<pair<double, double>>points;
	string tmp = "2\nL 1 1 0 1\nL 1 1 1 0\n";
	if (hDLL) {
		pGui guiProcess = (pGui)GetProcAddress(hDLL, "guiProcess");
		pCmd cmdProcess = (pCmd)GetProcAddress(hDLL, "cmdProcess");
		try {
			int ans1 = guiProcess(&points, tmp); // 測試接口函數1
			for (int i = 0; i < points.size(); i++) {
				cout << points[i].first << " " << points[i].second << endl;
			}
			cout << ans1 << endl;
		}
		catch (exception t) {
			cout << t.what() << endl;
		}
		cmdProcess(argc, argv); //測試接口函數2
	}
}

  • GUI接口對接
#pragma comment(lib,"calcInterface.lib")
_declspec(dllexport) extern "C" int guiProcess(std::vector<std::pair<double, double>> *points, std::string msg);
_declspec(dllexport) extern "C" void cmdProcess(int argc, char *argv[]);

// 調用核心接口
std::vector<std::pair<double, double> > points;
int cnt = 0;
try {
	cnt = guiProcess(&points, input); // 調用核心接口dll函數
}
catch (std::exception e) { // 反饋計算核心拋出的異常
	QString dlgTitle = QString::fromLocal8Bit("計算出現錯誤");
	QMessageBox::critical(this, dlgTitle, e.what());
	return;
}

(四)【附加題】跨組對接調用其餘組的計算核心

咱們與另外一個小組合做,互換了計算核心dll,測試結果以下,能夠正常運行,git倉庫連接

他們採用的gui接口函數格式與咱們的徹底相同,只需將對應的dll和lib替換便可直接運行。

IMPORT_DLL int guiProcess(std::vector<std::pair<double, double>>* points,std::string msg);
IMPORT_DLL void cmdProcess(int argc, char *argv[]);
  • 命令行對接結果

  • GUI接口對接結果

11、描述結對的過程

因爲單個軟件存在或多或少的問題,咱們綜合使用VS Live Share騰訊會議以及github來進行遠程結對編程。

  • VS Live Share:能夠更加真實的模擬兩我的共同面對同一文件編程的效果,「領航員」也能夠更方便的參與到代碼的編寫中,但VS Live Share沒法讓被邀請的人觀看到程序的運行結果以及整個解決方案的結構。
  • 騰訊會議: 咱們輔以騰訊會議共享屏幕,來觀看整個項目的架構和編譯、運行等信息。
  • github: 咱們根據兩我的擅長的不一樣部分,經過github進行代碼同步,分別在不一樣的模塊擔任「駕駛員」和「領航員」的角色。

結對過程當中,因爲兩人已經在許多課程做業中創建了深厚的合做基礎和友誼,互相信任,所以,在遇到一些猶豫不定的狀況時,爲了提升效率,在共同討論各類方法的優劣以後,多采用斷言和說服的方式肯定思路。

如下是咱們結對過程當中部分截圖:

12、說明結對編程的優勢和缺點。同時描述結對的每個人的優勢和缺點在哪裏

(一)結對編程的優勢:

  • 更快的攻破技術難點:對實現的細節技術難點能夠很方便地討論,並且兩人都熟悉代碼,交流無鴻溝。例如在處理double值的hash函數時,咱們通過討論商榷,發現了c++自帶的hash<double>不能直接在咱們的代碼中使用,故共同商量出用長整型的hash函數方式,解決了此技術難點。
  • 加快對新知識的熟悉和學習:例如以前咱們二人都沒用用vs封裝過dll,結對編程中就可讓領航者提早進行學習和蒐集資料。待編程者完成代碼編寫後能夠直接進行封裝。
  • 代碼質量更高:結對編程時,有搭檔盯着屏幕看,一些細小的粗心致使的錯誤,每每可以被當即發現,省得以後被粗心致使的bug卡住。
  • 愉快的編程氛圍:一我的編程一般比較枯燥無聊,特別是當發現bug的時候,而結對編程時,兩我的一塊兒工做的氛圍要更輕鬆,若是遇到bug的時候,也不像一我的的時候那麼絕望,兩人能夠利用知識互補,更容易定位bug,解決bug。

(二)結對編程的缺點:

  • 時間協調:結對編程須要兩人在同一時間作到電腦前,即便是遠程開展,也要經過共享屏幕等方式。可是。兩我的的習慣工做時間可能不一樣,好比我就喜歡晚上工做,隊友喜歡早起工做,此時就須要協商解決,好比採用隔一天一換的策略。
  • 遠程結對編程交流不便:有一些細節小問題,好比某一行、某一個字母、某一個數字,若是在線下的話能夠直接用手指出,或」奪下「鍵盤鼠標直接改掉。可是線上交流就不得不用用語言描述和定位,一些很簡單的操做,因爲着急或者其餘緣由一時間表達不清楚,就會浪費時間。若是是線下結對編程的話,應該不會存在該問題。
  • 效率平衡:原本是兩個生產力,硬性的只容許一我的寫代碼,雖然代碼質量會變高,可是效率畢竟多是減半的。如何維持效率平衡,須要在軟件工程實踐中不斷探索。

(三)結對編程的每個人的優缺點:

  • 我搭檔的優勢

    • acm大佬,寫代碼能力超強,可以梳理很複雜的程序結構,寫了大部分複雜的條件分支判斷代碼。例如處理輸入異常的函數。
    • debug和測試更有耐心,對繁雜的版本控制和測試任務都能遊刃有餘地處理
    • 對算法與代碼實現的細節關注的更多一些。好比浮點數的hash值問題,他進行了詳盡的測試,最終找到了解決方案。
  • 個人優勢

    • 充滿學習熱情,能很快的學習新的語言和開發工具,如本次項目中的QT開發工具。
    • 表達能力強。可以快速發現問題並描述出來,負責與其餘隊伍交流接口和對拍等事務。
    • 對C++語言更熟練
  • 個人缺點

    • 對一些繁瑣枯燥的工做不太認真
    • 版本控制混亂,各類版本容易把本身繞暈
  • 搭檔的缺點

    • OOP和c++基礎不牢固

總結:邏輯清晰、代碼規範、耐心細心、規劃合理,個人搭檔的是我見過最強的搭檔。

十3、警告信息消除

使用默認的規則。能夠看到已經沒有任何錯誤和警告。

十4、思考

(一)接口數據類型如何設計?

在設計接口數據類型時,咱們產生了一些異議,與其餘小組討論,意見也不是很一致。大致分爲如下兩種。

  • 鬆式設計

    所謂鬆式設計指採用萬能數據類型,如字符串、整形等做爲輸入輸出。

    • 優勢:任何人,不須要任何庫或包就能使用,易於跨平臺,移植性強。
    • 缺點:數據自檢錯能力差,調用者能夠傳任何字符串到接口,易形成:
      • 用戶須要學習數據格式,構造特定格式的字符串,增長學習成本
      • 易粗枝大葉形成格式錯誤或故意攻擊接口,讓核心程序獲得非法的輸入
      • 接口程序必須嚴格對輸入格式進行錯誤處理,防止核心程序崩潰
  • 緊式設計

    採用自定義數據類型做爲接口,提供特定的構造函數,提供檢錯判斷等。

    • 優勢:易學易用,防粗心,防攻擊,使不構造出合法的數據沒法經過編譯,或拋出異常。
    • 缺點:須要提供自定義數據類型的庫,不易跨平臺,移植性差,不方便對接,並且將class導出dll比函數麻煩。

討論以後,咱們最終採用了折中的辦法,用c++STL中的vector<pair<double,double>>這種數據類型返回點集,即不直接用純粹字符串,也不使用自定義數據類型。較方便地解決了本次需求。

可是,若是有些狀況下沒法採用STL來描述某些接口的數據類型,又該採用什麼樣的方式設計呢?

相關文章
相關標籤/搜索