2020軟工結對項目做業-簡單幾何形狀間交點統計

2020軟工結對項目做業-簡單幾何形狀間交點統計

項目 內容
課程連接 2020春季計算機學院軟件工程(羅傑 任健)
做業要求 結對項目做業
課程目標 系統學習軟件開發理論和流程,經過實踐積累軟件開發經驗
本博客的收穫 開發了一個簡單帶有UI的項目,總結結對編程過程的經歷和優缺點
教學班級 005
項目地址 https://github.com/zwx980624/IntersectProject
UI地址 https://github.com/zwx980624/IntersectionGUI
隊友地址 xgnb

1、估計將在程序的各個模塊的開發上耗費的時間

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

2、結對編程中是如何利用Information Hiding,Interface Design,Loose Coupling方法設計接口

  • information Hiding 是指信息的隱藏和封裝python

    In computer science, information hiding is the principle of segregation of the design decisions in a computer program that are most likely to change, thus protecting other parts of the program from extensive modification if the design decision is changed. The protection involves providing a stable interface(computer_science) which protects the remainder of the program from the implementation (the details that are most likely to change).c++

    從維基百科中,咱們能夠看出information Hiding是指外部人員在能夠調用接口而沒必要關心內部的實現,即便內部實現發生變更,對外部人員也不會產生任何影響。在咱們設計一些接口時,就採用了這種思想。好比處理輸入的接口能夠接受文件、字符串、標準輸入等任意輸入流,調用接口者不須要知道咱們內部是怎麼處理的,就能夠完成對輸入圖形的處理和保存。還有GUI使用的guiProcess接口,只須要傳入字符串和保存交點的vector,不須要額外構建該接口內部使用的各類類,便可完成交點計算。git

  • interface Design 指接口設計,包含單一職責原則、里氏替換原則、依賴倒置原則、接口隔離原則、最少知識原則、開閉原則等。github

    咱們根據以上原則的一部分,設計了以下兩個計算核心的接口:正則表達式

    • guiProcess算法

      函數傳入符合格式的字符串,交點集用指針返回,交點個數用返回值返回。編程

      • 由數據格式的設計原則,只使用c++標準庫中的容器,而不用自定義數據類型。如CPoint要轉成std::pair<double,double>表示,保證調用者清晰理解。
      • 輸入採用特定格式的字符串,符合方便易用原則,若是格式錯誤,由核心處理並拋出異常
    • cmdProcess爲了命令行調用方便,採用需求規定的命令行輸入格式進行輸入輸出canvas

  • loose Coupling是鬆耦合設計模式

    In computing and systems design a loosely coupled system is one in which each of its components has, or makes use of, little or no knowledge of the definitions of other separate components. Subareas include the coupling of classes, interfaces, data, and services.數據結構

    咱們的dll在拋出異常時會將錯誤信息存儲爲標準異常,別人在調用時,不須要專門使用咱們設計的異常類,就能夠捕捉異常,能夠減少耦合度。

3、4、計算模塊設計與實現過程及UML。

(一)PipeLine

PreProcess

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

CalcIntersect

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

(二)類間關係圖

  • 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裏。

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

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

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

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

(四)代碼說明

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

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值將不一樣,因此應該截取某精度,轉換成整形進行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、看 Design by Contract,Code Contract 的內容,描述這些作法的優缺點,說明你是如何把它們融入結對做業中的

Design by Contract,也稱契約式設計:

It prescribes that software designers should define formal, precise and verifiable interface specifications for software components, which extend the ordinary definition of abstract data types with preconditions, postconditions and invariants. These specifications are referred to as "contracts", in accordance with a conceptual metaphor with the conditions and obligations of business contracts.

簡單來講,就是爲每一個接口寫好各類使用的規範,包括調用前的規範和調用後的結果規範。在曾經的OO課上,咱們接觸並學習過寫函數的約束和規範,我的感受優勢和缺點以下:

  • 優勢:經過書寫前置條件、後置條件和不變式這種邏輯化的語言,能夠避免開發人員對模塊的實現產生歧義,其次經過合理的設計約束,可讓模塊的耦合度更低,更加符合上述提到的三個設計原則。

  • 缺點:須要花費大量時間,而且對開發人員的素質要求高。好比本次做業過程當中,咱們就沒有嚴格使用標準的前置、後置語句,而是使用通俗化的語言描述,不然又是一筆巨大的時間開銷。

咱們的pipeline函數就採用了契約式設計模式,爲了提升效率簡化過程,咱們採用了比較書面化的語言來描述規範,而沒有嚴格遵循標準得contract格式。如如下幾個函數:

// 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插件測試獲得代碼覆蓋率爲:

其中部分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++標準異常:

  • 輸入異常:
    • 有關圖形數量的異常:如沒法讀入N、N不符合規範、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;
}

11、描述結對的過程

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

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

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

如下是咱們結對過程當中同時使用LiveShare和騰訊會議時的截圖:

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

結對編程的優勢:

  • 更快的攻破技術難點。因爲VS用的很少、QT也是第一次使用,在咱們結對編程時常常會碰到一些操做、語言上的問題和障礙。這個時候,咱們能夠分頭查找相關資料,解決問題的速度大大提高。
  • 保持愉快的編程氛圍,平衡心態。本身一我的編程的時候,常常會由於一些莫名其妙的bug卡幾個小時,心情煩躁到想要拍桌子,並且最後無處訴說。但在結對編程的時候,兩我的一塊兒開發代碼,一塊兒享受debug的」樂趣「,看到有人和本身一塊兒由於共同的代碼受苦,心態也好了許多,debug也更加積極,編程氛圍也很輕鬆愉快。
  • 加快對新知識的熟悉和學習,下降開發代價。個人搭檔是一名大佬,曾啃過《C++ Primer》,所以對於C++比我熟悉的多,在我編程過程當中,能夠給我提供許多指導,避免了我花費在查找資料上的時間,也預防了不少細節上的錯誤。
  • 結對期間產出的代碼質量更高一些。結對編程時,有搭檔盯着屏幕看,一些細小的粗心致使的錯誤,每每可以被當即發現,省得以後被粗心致使的bug卡住。

結對編程的缺點:

  • 時間協調問題。因爲咱們還都是學生,沒辦法把精力全用在開發項目之中。特別到了大三,每一個人選的課幾乎都不相同,也都有科研、助教、馮如杯等等一些不一樣的私事,並且如今仍是在家遠程結對編程,家事也不可能無論不顧,所以時間的協調比較麻煩,有一些比較簡單的、已經肯定好設計的部分也就各自抽空完成了。
  • 遠程結對編程還存在交流不便的問題。有一些細節小問題,好比某一行、某一個字母、某一個數字,若是在線下的話能夠直接用手指出,或」奪下「鍵盤鼠標直接改掉。可是線上交流就不得不用用語言描述和定位,一些很簡單的操做,因爲着急或者其餘緣由一時間表達不清楚,就會浪費時間。若是是線下結對編程的話,應該不會存在該問題。

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

  • 個人搭檔的優勢
    • 學習能力強。充滿學習熱情,能很快的學習新的語言和開發工具,如本次項目中的QT開發工具。
    • 表達能力強。可以快速發現問題並描述出來,也負責與其餘隊伍交流接口和對拍等事務。
    • OOP思想、C++語言細節等基礎知識很熟悉。給了我不少幫助
  • 個人優勢
    • debug和測試更有耐心一點。
    • 對算法與代碼實現的細節關注的更多一些。
    • 對做業要求更上心一些。(分奴嘴臉)
  • 個人搭檔的缺點
    • 不喜歡一些繁瑣枯燥的工做
  • 個人缺點
    • OOP和c++基礎不牢固,代碼一股面向過程的風格
    • 有的時候過於死板和猶豫,須要隊友」斷言「以提升效率

總結:學識淵博、代碼規範、能說會道、規劃合理,個人搭檔的是我見過最強的搭檔。

十3、警告信息消除

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

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

合做小組中的兩位成員:17373124 閆苗、17373299 劉紫涵

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

這是咱們簡單測試他們的dll的結果:

這是他們測試咱們dll的結果:

因爲在最初設計時就已經進行過大體的商量和規劃,他們採用的gui接口函數格式與咱們的不一樣之處不多,只需修改接口部分少許代碼便可正常對接。

IMPORT_DLL int guiProcess(std::vector<std::pair<double, double>>* points,std::vector<string> msg);
  • 這是咱們測試他們dll的GUI接口對接結果

  • 這是他們測試咱們dll的GUI接口對接結果

相關文章
相關標籤/搜索