天天看點

“并肩作戰,平面交點Pro”——記2020BUAA軟工結對程式設計作業

項目 内容
這個作業屬于哪個課程 2020春季計算機學院軟體工程(羅傑 任建)
這個作業的要求在哪裡 結對項目作業
我在這個課程的目标是

完成一次完整的軟體開發經曆

并以部落格的方式記錄開發過程的心得

掌握團隊協作的技巧

做出一個優秀的、持久的、具有實際意義的産品

這個作業在哪個具體方面幫助我實作目标 體驗結對程式設計,兩人互相配合所帶來的優缺點
教學班級 006
項目位址 Intersection Pro

PSP 規劃

在開始實作程式之前,在下述 PSP 表格記錄下你估計将在程式的各個子產品的開發上耗費的時間。(0.5')
PSP2.1 Personal Software Process Stages 預估耗時(分鐘) 實際耗時(分鐘)
Planning 計劃 15 10
· Estimate · 估計這個任務需要多少時間
Development 開發 780 965
· Analysis · 需求分析 (包括學習新技術) 240 160
· Design Spec · 生成設計文檔 30 20
· Design Review · 設計複審 (和同僚稽核設計文檔)
· Coding Standard · 代碼規範 (為目前的開發制定合适的規範)
· Design · 具體設計 60 80
· Coding · 具體編碼 360
· Code Review · 代碼複審 40
· Test · 測試(自我測試,修改代碼,送出修改) 120 180
Reporting 報告
· Test Report · 測試報告
· Size Measurement · 計算工作量
· Postmortem & Process Improvement Plan · 事後總結, 并提出過程改進計劃
合計 875 955

接口設計

看教科書和其它資料中關于 Information Hiding,Interface Design,Loose Coupling 的章節,說明你們在結對程式設計中是如何利用這些方法對接口進行設計的。(5')

這三個詞對于北航計算機學院的同學們來說一定不陌生,在大二下學期所接觸的 面向對象設計與構造(Java實作) 中,學習面向對象的三大特性的時候就已經屢次強調。在面向對象的作業中,我們使用的 checkstyle——代碼風格檢測 中也明确限制了同學們的設計。比如上面提到的:

Information Hiding

——資訊隐藏原則,就是面向對象的封裝思想,對對象内部的屬性不可見,但是提供對屬性進行修改和擷取的接口,我們的代碼風格檢測中就嚴格要求所有類内部屬性為

private

這樣做的好處一來是防止内部屬性在未知或者由于疏忽被更改,二是進行了保密,對外部是黑盒,隻提供服務。

Interface Design

——接口設計,在面向過程的代碼中我們很難體會到,但是在面向對象的學習中我們已經有所接觸。接口設計是符合現實生活的常理的,比如Java的單繼承多借口的機制,就是限定了一個類隻能是一個什麼,但是可以做很多工作。接口的實作(implements)使得類的功能多樣且不發生高内聚的情況,也就是常說的——對擴充開放,對修改封閉。

Loose Coupling

——解耦,在這次的作業中也明确讓我們體驗,實際上在學習計算機知識的過程中我們就已經接觸過很多樣例,比如作業系統中的Cache訪存,Java面對對象中的抽象類,計算機網絡中的層級結構,都是解耦的思想的實踐,這些都可以總結成一句非常經典的話——"計算機任何工程領域的問題,都可以通過增加一個中間層來解決"。一個中間層使得兩個對端不能“死”連結,降低了子產品之間的耦合性。

在本次的實驗中,我和我的隊友通過實作前溝通修改文檔的方式來實作通過接口連接配接核心成分和UI界面,達到的效果也令人滿意,寫好了内部的資料結構的操作之後隻需要保證接口的正确性就可以随意調用。

計算接口設計實作

計算子產品接口的設計與實作過程。設計包括代碼如何組織,比如會有幾個類,幾個函數,他們之間關系如何,關鍵函數是否需要畫出流程圖?說明你的算法的關鍵(不必列出源代碼),以及獨到之處。(7')

算法的關鍵和特性如下:

線段部分

本次作業的計算部分是對上一次的個人作業中的需求的延申,雖然上一次中我的項目沒有對附加題進行實作,但是已經完成的部分都是保證了嚴格的正确性且給這次作業的需求預留了空間。

在上一次作業中,隻需要求解直線之間的交點,在本次作業中,加入了線段和射線的需求,而我再存儲直線的表達式的時候,沒有采用上次作業中很多同學的 直線一般式 ,而是利用了類似于參數方程的方式,存儲一段起點和線的延申方向,在求解的過程中,距離的表示也是使用的 輸入的兩點的向量的倍數 的方式,是以在本次作業中,擴充不需要改動任何代碼,隻需要 對倍數進行限制即可,具體表現為:

  1. 對于直線而言,倍數沒有任何限制,因為可以兩端無限延長
  2. 對于射線而言,倍數是非負數,因為可以一端無限延長
  3. 對于直線而言,倍數是0到1之間的數值,因為不能做延長

是以線之間的相交隻需要5min即可完成需求。

圓部分

對于上一次作業中對圓的部分沒有實作,主要是在推導的時候沒有給自己留下充裕的時間,導緻出現了一些錯誤,在本次作業中我重新對圓圓相交和圓線相交進行了推到并求值,在這裡感受到了結對程式設計的好處——我負責實作功能的同時,他負責盯着我的代碼和構造測試資料。因為利用直線的參數方程在計算圓的相關交點需要很多中間變量,還有不可避免地做很多輔助線段,命名也不是很友善,是以在我們實作的時候,我将推導的過程講述一遍給我的同伴,并共用一套标記,比如在我們的具體代碼實作中的很多不知所雲的點——

P, M, S, Q

。當然我知道這種不知所雲的程式設計方式,如

CalculatePM(Circle c, Line l)

會對其他人了解我的代碼造成極大的困惑,特别是我的很多向量運算如内積,是以我在求解圓的相關内容的函數附近加上了詳細的注釋,保證了讀一遍注釋就能在草稿紙上畫出一個樣例,并清晰了解其中每個點對應的意義,如“垂足、距離相近的交點”等等。

新增和删除操作

對于新增的插入删除的需求,我和同伴達成一緻,采用了 邏輯删除 的實作。何為邏輯删除,其實在資料庫中經常用到。當删除資料庫中的某一條資料,如使用者資料,會對資料庫中的其他表造成連帶影響的時候,可以考慮将該資料的 存在屬性置為假,也就是雖然表中有他但是實際上他不會被使用。我們之是以也選擇這種方式是為了避免出現容器中的錯位,由于我們直接使用

std::vector

進行存儲,是以删除中間元素會給後續的元素的下标索引帶來變動,需要修改多處的對應關系,而将其邏輯删除則不會造成這種負面的影響,隻需要在通路元素的時候首先判斷該元素是否存在

isExists?

即可。至于我們為什麼使用

std::vector

而不使用如

std::unordered_map

之類的操作,後面會說到。

異常處理的操作

對異常的處理可以從時間上分為兩種方式:

  1. 運作前處理(輸入時處理)
  2. 運作時處理

兩者各有優劣,在這裡我們使用了運作時處理的方式,主要理由有下:

  1. 運作前處理存在 誤抛異常 的風險:

    這一點是我在面向對象的課程中體會到的。我們面向對象第一單元的作業中就有異常處理的部分,和很多人一樣,我當時也采用的是輸入時處理的方式,隻要在輸入中檢測到異常我就立刻抛出,結束程式。最後的程式結果也和不少同學一樣,對于異常情況反應過激,很多正确的樣例我們也抛出了異常。因為在輸入中對異常的邏輯判斷是要 保證甯殺一千不放走一個的,因為後面已經沒有可以處理異常的部分了,再有異常就會使得程式崩潰,是以在設計上過于嚴格導緻誤判。

  2. 運作前處理其實就是将過程中遇到的問題整合在了最前方而已:

    其實我們在輸入時的處理函數,就是後續的計算函數中的一部分,比如線和線之間重合的異常,在我們的計算子產品中,會線求解兩個線段的向量的内積,然後将内積當作分母求值。在求内積的時候我們實際上對于平行線段就已經能進行探測了,隻要兩線平行且兩線各取一點的連線還與兩線平行就是重合的情況,這種異常情況在我們判斷分母是否為0的時候就已經接觸,而且是一個必要的過程,是以放到前面實際上是代碼的備援了。

  3. 計算交點是一個 “一次性運作” 的計算過程:

    給定一份輸入,經過 “一次性運作” 給出一個輸出結果,是一個“不可疊代”的過程。什麼意思呢?這個帶上雙引号的 “不可疊代” 指的是對于新增線段的需求和删除線段的需求是不可疊代的,不是說減少一個圖形就是在目前結果的基礎上進行減少,而是将減少之後的整理重新進行一遍從頭的計算。因為對于交點而言處理删除比重新運作更加繁瑣,你不知道删除之後在删除圖形上的交點是否也是其他圖形之間的交點,實際上計算量是一樣的。是以對于這種 “一次性運作” 的計算過程,從在輸入時刻抛出異常而節省的時間我認為是不必要的,因為單次運作的時間是可接受的(測試中用了2000個圖形的資料在10s左右給出答案,和課程組預設的60s相比時間寬裕)

代碼組織架構

使用了非常正常的代碼組織,核心子產品在

Intersection

檔案夾中,指令行界面程式在

IntersectionCLI

中,圖形化界面程式在

IntersectionGUI

中:

  • Intersection
    • Intersection.h
    • Intersection.cpp 輸入輸出和計算交點的處理
    • Shape.cpp 圖形相關的計算
  • IntersectionCLI
    • main.cpp
  • IntersectionGUI
    • IntersectionGUI.h
    • ShowPic.h
    • IntersectionGUI.ui
    • IntersectionGUI.cpp 掌控布局和連結
    • ShowPic.cpp 繪制圖像
  • CoverageTest
    • coverageTest.cpp 覆寫測試檔案夾
  • Test
    • intersectTest.cpp 交點的單元測試回歸測試
    • errorHandleTest.cpp 錯誤處理的單元測試回歸測試
    • ShapeTest.cpp 圖形相關的單元測試回歸測試

UML設計

閱讀有關 UML 的内容。畫出 UML 圖顯示計算子產品部分各個實體之間的關系(畫一個圖即可)。(2’)
“并肩作戰,平面交點Pro”——記2020BUAA軟工結對程式設計作業

确實在耦合方面做的不夠好,将圖形都用struct儲存而不是class儲存是以直接放置在了Intersection.h中,其實為了後續的圖形的多樣性的可擴充性,應該單獨出來一個Shape.h類,裡面專門存放各種圖形的定義,但是考慮到其實增加圖形之後也要增加圖形之間的交點情況,而且求解的交點的種類個數是線性增長的,是以分離出來之後感覺定義上會友善一些,但是很多求解的操作還是要在Intersection.h中,是以幹脆就沒有分離了。

“并肩作戰,平面交點Pro”——記2020BUAA軟工結對程式設計作業

計算性能分析

計算子產品接口部分的性能改進。記錄在改進計算子產品性能上所花費的時間,描述你改進的思路,并展示一張性能分析圖(由VS 2015/2017的性能分析工具自動生成),并展示你程式中消耗最大的函數。(3')

采用的樣本是 1000個圓和2000條線:

“并肩作戰,平面交點Pro”——記2020BUAA軟工結對程式設計作業

可以看出還是dcmp最大占比,因為是最基礎的比較函數,在上次的個人作業結束後曾思考是否可以将兩兩比較的結果進行儲存,但是訪存的時間和直接相減計算相似,是以沒有進行dcmp的優化。

契約式程式設計

看 Design by Contract,Code Contract 的内容:

http://en.wikipedia.org/wiki/Design_by_contract

http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx

描述這些做法的優缺點,說明你是如何把它們融入結對作業中的。(5')

契約式程式設計(Design by Contract)其實在面向對象中又以及接觸過,在第三單元的JML(Java Modeling Language)的學習中,就是通過學習一種特殊的Java模型語言,對輸入和輸出和不變量進行限制,保證任意方法能夠得到想要的結果且不産生副作用,但是由于JML的配置極為麻煩,到目前為止我們隻嘗試過特别簡單的

a+b

類型的JML的實際運作,但是學習這種契約式程式設計從理論上讓我們體會到了契約的優越性和便利:隻要有方法的契約,不需要知道方法的作用是什麼,實際意義是什麼就可以寫出能夠正确運作的代碼。

當然這種類似于JML的嚴格的契約式程式設計也有自身的缺點,那就是消耗成本過高——方法需要先用契約寫一遍,再用進階程式設計語言翻譯一遍,對于團隊開發而言時間成本和人力成本是不可忽略的,這也是JML這種專門為Java設計的模型語言沒有流行起來的重要原因之一,還是在理論層面更加重要。

在本次的結對程式設計中,我和同伴沒有按照JML類似的死闆的契約式程式設計,因為一開始負責的子產品劃分比較清晰,我負責核心代碼,他負責GUI,是以隻交流了接口的調用的函數,還有增加删除圖形的功能的函數,因為項目比較小是以沒有牽扯到副作用,團隊規模也小,就沒有進行書面的整理。

單元測試

計算子產品部分單元測試展示。展示出項目部分單元測試代碼,并說明測試的函數,構造測試資料的思路。并将單元測試得到的測試覆寫率截圖,發表在部落格中。要求總體覆寫率到 90% 以上,否則單元測試部分視作無效。(6')

本次得測試分為覆寫測試和單元測試:

單元測試已經回歸測試

我們分了三個部分進行測試:

結果如下:

“并肩作戰,平面交點Pro”——記2020BUAA軟工結對程式設計作業

覆寫測試

雖然我們運作得直接輸出結果沒有達到90%,但是點入檔案會發現其實已經做到種類的全覆寫,沒有運作的部分是提供給GUI的接口,如擷取所有線段和交點,還有就是調試中開發者使用的輔助函數,如:

printAllPoints()

“并肩作戰,平面交點Pro”——記2020BUAA軟工結對程式設計作業

異常處理

計算子產品部分異常處理說明。在部落格中詳細介紹每種異常的設計目标。每種異常都要選擇一個單元測試樣例釋出在部落格中,并指明錯誤對應的場景。(5')

異常處理包括以下幾個部分:

  1. 線之間的重疊(這裡要特别考慮相交于一點不是重疊的情況)
    input.txt
    2
    L 1 1 2 2
    L -1 -1 0 0
               
    TEST_METHOD(LL_0)
     {
         try {
             ifstream in("../test/errortestcase/5.txt");
             Intersection* intersect = new Intersection();
             intersect->getAllPoints(in);
             intersect->solveIntersection();
             Assert::IsTrue(false);
         }
         catch (string msg) {
             Assert::AreEqual(string("直線與直線或線段重合"), msg);
         }
     }
               
  2. 線的兩端點重合
    input.txt
    1
    L 1 2 1 2
               
    TEST_METHOD(INPUT_1)
     {
         try {
             ifstream in("../test/errortestcase/1.txt");
             Intersection* intersect = new Intersection();
             intersect->getAllPoints(in);
             Assert::IsTrue(false);
         }
         catch (string msg) {
             Assert::AreEqual(string("在第1行,構成線段、射線或直線的兩點重合"), msg);
         }
     }
               
  3. 圓圓重合
    input.txt
     4
     C 0 0 5
     L 1 0 0 1
     S 2 4 6 8
     C 0 0 5
               
    try {
     	ifstream in49("../test/errortestcase/26.txt");
     	intersect->getAllPoints(in49);
     	ret = intersect->solveIntersection();
     	assertEqual(true, false);
     }
     catch (string msg) {
     	assertEqual(string("圓和圓重合"), msg);
     }
     intersect->clearGraph();
               
  4. 圓半徑非正數
    input.txt
     2
     L 0 1 1 2
     C 0 10 -1
               
    if (type == 'C') {
     	if (!(isInLimitation(x1) && isInLimitation(y1) && isInLimitation(x2))) {
     		throw "出現超範圍的資料";
     	}
     	if (dcmp(x2) <= 0) {
     		throw "圓的半徑應大于0";
     	}
     	circles.push_back(Circle(Heart(x1, y1, type), x2));
     }
               
  5. 輸入類型不正确
    input.txt: 
     2
     L 1 1 1 0
     P 1 2 3 4
               
    TEST_METHOD(INPUT_0)
     {
         try {
             ifstream in("../test/errortestcase/0.txt");
             Intersection* intersect = new Intersection();
             intersect->getAllPoints(in);
             Assert::IsTrue(false);
         }
         catch (string msg) {
             Assert::AreEqual(string("在第2行,出現未識别符号"), msg);
         }
     }
               
  6. 輸入非法字元
    input.txt
    2
    L 1 abc 1 1000
    C 1 1 2
               
    TEST_METHOD(INPUT_3)
     {
         try {
             ifstream in("../test/errortestcase/3.txt");
             Intersection* intersect = new Intersection();
             intersect->getAllPoints(in);
             Assert::IsTrue(false);
         }
         catch (string msg) {
             Assert::AreEqual(string("在第1行,出現值錯誤,無法讀入"), msg);
         }
     }
               
  7. 輸入參數超過給定定義域
    input.txt 
    1
    L 1 1 1 1000000
               
    TEST_METHOD(INPUT_2)
     {
         try {
             ifstream in("../test/errortestcase/2.txt");
             Intersection* intersect = new Intersection();
             intersect->getAllPoints(in);
             Assert::IsTrue(false);
         }
         catch (string msg) {
             Assert::AreEqual(string("在第1行,出現超範圍的資料"), msg);
         }
     }
               

UI設計思路

界面子產品的詳細設計過程。在部落格中詳細介紹界面子產品是如何設計的,并寫一些必要的代碼說明解釋實作過程。(5')

在UI的設計中,我們主要是以功能為導向設計,需要的功能有:

  1. 檢視圖形分布和交點——平闆顯示
  2. 檢視目前在面闆上的圖形——側邊欄展示
  3. 檢視目前的所有交點——側邊欄展示
  4. 增加新圖形的操作——按鈕
  5. 删減目前圖形的操作——按鈕
  6. 打開檔案的操作——按鈕

輔助功能(不是必要的但是存在友善使用者):

  1. 滑鼠滾輪實作縮放
  2. 上下左右移動畫布

于是一個UI就設計出來了:

“并肩作戰,平面交點Pro”——記2020BUAA軟工結對程式設計作業

當打開圖像檔案時:

“并肩作戰,平面交點Pro”——記2020BUAA軟工結對程式設計作業
“并肩作戰,平面交點Pro”——記2020BUAA軟工結對程式設計作業

本次的UI是基于Qt開發的,基本上是Cpp的文法,比較容易上手,隻是Qt的元件管理比較讓人頭暈,主要使用了如下幾個部件:

  1. QWidget —— 顯示圖像的畫布,重寫了 QPaintEvent方法,才使得可以在上面用 QPaint 進行圖像的繪制:
    1. 初始化畫筆
      “并肩作戰,平面交點Pro”——記2020BUAA軟工結對程式設計作業
    2. 繪制坐标軸
      “并肩作戰,平面交點Pro”——記2020BUAA軟工結對程式設計作業
    3. 繪制圖形和交點
      “并肩作戰,平面交點Pro”——記2020BUAA軟工結對程式設計作業
  2. 側邊的圖形限制欄和交點顯示欄使用的是 QListWidget,存儲字元串數組并顯示出來:
    1. 存儲圖形:
      “并肩作戰,平面交點Pro”——記2020BUAA軟工結對程式設計作業
    2. 存儲交點:
    void IntersectionGUI::getResult(void) {
         int res = intersection->solveIntersection();
         ui.pointNumResult->setText(QString("Answer:%1").arg(res));
         vector<Point> inters = intersection->getIntersects();
         ui.allIntersects->clear();
         for (auto p : inters) {
             ui.allIntersects->addItem(QString("(%1,%2)").arg(p.x).arg(p.y));
         }
    }
               
  3. 按鈕使用的是 QButton ,監控滑鼠在上面的點選:

    可以看到通過Qt自身的connect方法将UI上的Qbutton與核心函數中的槽函數連接配接了起來,一旦按鈕被監測到點選動作就會執行槽中的函數。

    connect(ui.openFileButton, SIGNAL(clicked()), this, SLOT(openFile()));
     connect(ui.getResult, SIGNAL(clicked()), this, SLOT(getResult()));
     connect(ui.deleteShape, SIGNAL(clicked()), this, SLOT(deleteItem()));
     connect(ui.zoom_in, SIGNAL(clicked()), this, SLOT(zoom_in()));
     connect(ui.zoom_out, SIGNAL(clicked()), this, SLOT(zoom_out()));
     connect(ui.addItem, SIGNAL(clicked()), this, SLOT(addItem()));
     connect(ui.zoom_reset, SIGNAL(clicked()), this, SLOT(zoom_reset()));
     connect(ui.left, SIGNAL(clicked()), this, SLOT(moveLeft()));
     connect(ui.right, SIGNAL(clicked()), this, SLOT(moveRight()));
     connect(ui.up, SIGNAL(clicked()), this, SLOT(moveUp()));
     connect(ui.down, SIGNAL(clicked()), this, SLOT(moveDown()));
               
  4. 畫布的輪滑放縮:

    實際上就是監控滑鼠的位置并且重寫滑鼠的滾輪滾動時的對應函數:

    void IntersectionGUI::wheelEvent(QWheelEvent *event)
     {
         QPoint pos;
         QPoint pos1;
         QPoint pos2;
         pos1 = mapToGlobal(QPoint(0,0));
         pos2 = event->globalPos();
         pos = pos2 - pos1;
    
         //判斷滑鼠位置是否在圖像顯示區域
         if (pos.x() > showPic->x() && pos.x() < showPic->x()+showPic->width()
                 && pos.y() > showPic->y() && pos.y() < showPic->y()+showPic->height())
         {
             // 當滾輪遠離使用者時進行放大,當滾輪向使用者方向旋轉時進行縮小
             if(event->delta() > 0)
             {
                 zoom_in();
             }
             else
             {
                 zoom_out();
             }
         }
     }
               

UI功能展示

界面子產品與計算子產品的對接。詳細地描述 UI 子產品的設計與兩個子產品的對接,并在部落格中截圖實作的功能。(4')
  1. 打開檔案:
    “并肩作戰,平面交點Pro”——記2020BUAA軟工結對程式設計作業
  2. 增加删除元素:
    “并肩作戰,平面交點Pro”——記2020BUAA軟工結對程式設計作業
  3. 滑鼠滾輪放大縮小與移動:
    “并肩作戰,平面交點Pro”——記2020BUAA軟工結對程式設計作業

結對現場展示

描述結對的過程,提供兩人在讨論的結對圖像資料(比如 Live Share 的截圖)。關于如何遠端進行結對參見作業最後的注意事項。(1')

由于疫情原因這學期的結對程式設計未能在同地程式設計,是以采用了遠端的方式,我們采取的具體方案是LiveShare + 微信語音通話的方式進行,下面是LiveShare的截圖:

“并肩作戰,平面交點Pro”——記2020BUAA軟工結對程式設計作業

結對程式設計優缺點

看教科書和其它參考書,網站中關于結對程式設計的章節,例如:現代軟體工程講義 3 結對程式設計和兩人合作 ,說明結對程式設計的優點和缺點。同時描述結對的每一個人的優點和缺點在哪裡(要列出至少三個優點和一個缺點)。(5')

體驗過這一次的結對程式設計之後,我算是親身體會到了鄒欣老師部落格中所說的結對程式設計中的角色。

這和大型交通工具都配有副駕駛的思想大抵是一緻的,當一個人長時間駕駛産生疲勞之後,需要副駕駛來交替;當駕駛員在駕駛的時候,副駕駛員不是無所事事的,是輔助駕駛員正确行駛的,在以往這種輔助駕駛主要展現在領航和提醒的作用上,現在由于電子地圖的興起,副駕駛更多的是提供臨時的幫助和關鍵時刻的決定。

我和我的同伴在結對程式設計的時候,一開始是我專門負責核心架構,他負責幫我測試和寫界面UI,經過第一輪實作所有需求之後,他構造出來的樣例确實能把我的很多細節的地方揪出來,這就是結對程式設計的第一第二個優點:

  1. 并行完成任務——高效。
  2. 看問題更加全面,彌補一個人容易疏忽的細節。

後來,開始到了UI的連結和異常處理的部分,我們兩人則嘗試着交換身份,我來着手UI,他來編寫異常處理,這是結對程式設計的第三個好處:

  1. 兩人都可以接觸到UI和核心子產品的開發,輪替開發不會有嚴重的疲勞感。

一直對着同一項工作特别容易産生疲勞,而這種疲勞所帶來的副作用還在于對項目的“自以為是”的了解,這裡的“自以為是”不是貶義詞,指的是重複進行同一項勞動而産生的焦躁的不安情緒。我開發完核心子產品和重構部分代碼之後真的對這個項目已經有點累了,因為實際上完成了所有的需求之後就不想再進行開發了——有一種好不容易修修補補之後“能跑就行”的僥幸心理,而我的同伴在UI的部分也恰好遇到了一些狀況,是以我們一拍即合互換身份。

最後,也是同樣重要的一點,結對程式設計的過程其實是共享知識的過程。我們一定可以從對方的身上學到優點——三人行必有我師焉,我從我的結對同伴處學習到了項目的代碼重構的方式,我之前是比較懼怕重構的,特别是多一點的檔案,還有Visual Studio這種重量級的項目管理器,我平時比較喜歡用linux下面的指令行,編譯運作什麼的都是用指令行。但是在這次的實踐中,我的同伴重構了兩次項目,讓我學習到了重構時候函數的提取,接口的抽象,如何生成DLL動态連結庫使用等等,也讓我沒有那麼懼怕Visual Studio了。

當然,結對程式設計也有令人不舒服的地方,每個人有自己的獨特的代碼風格,比如在異常處理的時候我就特别喜歡輸出調試,加很多的宏定義或者輔助函數去定位錯誤,包括利用-1的傳回值代表發生錯誤,而我的同伴特别喜歡抛出異常來定位,這種代碼風格的不同造成了我們的代碼有些不一緻,展現在單元測試中有幾個部分是-1,有幾個部分是

try{}catch{}

塊,當然後來進行了一次統一。這裡隻是舉了一個例子,在這次的項目中規模很小,而且UI的部分和core的部分分離的比較清楚,是以并沒有發現其他的太多的出入,當然以後工作中,一個良好的團隊一定會指定一個編碼規範的,比如Google的代碼風格和Ali的代碼風格等等,人總是要限制自己一點才能和團隊處得更加融洽,哈哈。

PSP 反填

在你實作完程式之後,在附錄提供的PSP表格記錄下你在程式的各個子產品上實際花費的時間。(0.5')

見開頭的PSP表格。

這兩周都花了大部分時間撲在了這個項目的實作上,其實真正編碼的時間并不多,更多的時間在于 Debug、學習UI和測試異常處理。有一些細節性的bug真的是結對程式設計能立刻發現,但是自己在做的時候就是成了盲點。我和我的同伴都有遇到自己感覺很奇怪的為什麼會出現bug的地方沉思許久,然後一拿給對方看就發現了某個變量的端倪,改了就跑通了的地方,也再次用 時間的教訓 感受到了結對程式設計和CodeReview的好處。

此外還有很多的時間花費在了捯饬Visual Studio上面,但是對于這個大家夥的情感,從一開始完全不懂導緻出現滿屏的紅色警告,點什麼都跑不動,到每天都在查Error,查Warning,似乎每天都在查同一個連結失敗的錯誤,找不到的錯誤,未定義的錯誤等等,用的愈發熟練,到結對項目結束的時候,以及能流暢的進行建構、清理、生成動态連結庫等等,強大的工具一定要規範地使用才能事半功倍。

還有就是血與淚的教訓:同伴之間的工具版本最好一樣!

因為Qt的版本不同和VS的版本不同的原因消耗了太多無用的時間,就比如說VS2017和VS2019的指令集分别是V141和V142,導緻一開始怎麼也無法編譯,而且改了正确的版本還是無法編譯的情況,既消磨時間又消磨精力,要是兩人的工具集從一開始就一緻必然節省很多時間。

完成結對項目的總結和反思

個人經曆所學習到的東西:

版本控制

在本次的結對程式設計中,中途出現了一點小插曲,由于和同伴同時開發沒有處理好兩人的commit關系,中途貪圖友善使用微信傳輸了一些檔案和壓縮包導緻commit出現了混亂,加上本次項目是基于自己的上一次項目上開發,在編寫

.gitignore

的時候不小心将

.vs

也加入到了

commit

中,出現了

.git

目錄龐大的局面,導緻後期兩人的代碼難以通過

git

進行協同工作,最終是感謝 LZB 同學提供的思路将送出曆史中的大檔案得以删除才回歸正常的大小,在此也貼出來分享給大家:

  • git不小心加入了大檔案如何删除
  • 直接獲得一個優秀的 .gitignote

"永遠"不要相信任何人

這個其實是我在做單元測試的時候遇到的一個問題,思考了一下。在單元測試中我建立了一個

intersection

類,去讀取檔案計算交點得到結果等等,然後每次讀完一個檔案之後清空裡面存儲的線和圓,再去讀下一個。因為我們設計的測試檔案比較多,大概40多個,然後測試的時候其中有一個地方一直過不去,那一個樣本本來應該交點是0個,但傳回的值裡面硬生生給了2個,百思不得其解,斷點之後才發現是上一個測試用例讀取之後沒有清空,然後這次讀取就累加在上面了。

本來每個測試樣例後面都應該有個

clearGraph()

的操作:

ifstream in3("../test/testcase/rsc_0.txt");
intersect->getAllPoints(in3);
ret = intersect->solveIntersection();
assertEqual(ret, 11);
intersect->clearGraph(); //******************
           

但是恰巧那一個地方就漏掉了,然後就導緻了單元測試不通過,于是我就在想,這個

clearGraph()

的操作,是不是放在讀取檔案前更好?如下面:

ifstream in3("../test/testcase/rsc_0.txt");
intersect->clearGraph(); //******************
intersect->getAllPoints(in3);
ret = intersect->solveIntersection();
assertEqual(ret, 11);
           

因為放在執行完求解後面,就是一個收尾的工作,收尾的動作通常是是 可有可無(不會造成嚴重後果),比如很多人

FILE*

打開之後沒有

fclose()

,這種收尾當然有的話更加規範,但是沒有的話對本單元的測試任務也不影響,但是對後面的任務可能會帶來潛在的危害,是以我們應該将清空畫闆這一步放在 讀入的前面,也就是真正開始該單元測試的最前方,我将其稱之為 “相信自己”,自己去檢查一遍、複核一遍畫闆是清空的,而不是 指望别人——在執行完單元測試之後将畫闆複原。

UI比想象中的要麻煩

一開始我們在糾結是C#還是Qt進行UI的開發,因為覺得Qt和Cpp是同類型的産品應該更好上手進而選擇了Qt,我的同伴沒有接觸過Qt,我隻運作過别人寫的Qt是以都比較陌生,但因為 聽說 Qt 和Cpp比較相似就沒有太放在心上,結果真正到了UI開發的時候發現并沒有想象的那麼簡單,還是花了我們不少時間去了解Qt中的元件,以及元件之間的關系,UI的元件是如何布局、是如何連結後面的函數的等等,一開始的時候就發現UI面闆上零件奪得吓人,到後來入門基礎知識了解概念,再到要實作什麼功能去查詢适合的工具,原本以為不難的UI讓我們産生了焦慮,因為即使簡單,你也不知道自己實作一個功能需要什麼,而且Qt上的一種功能,網上可以搜尋到好幾種實作,也無法比較優劣,萬一踩坑也隻能重頭開始。這對于我的啟發是在後續的團隊項目中,不能忽視UI的設計和開發。

參考連結

  1. stackoverflow | 如何删除git中加入的大檔案
  2. gitignore 模闆大全