天天看點

[2017BUAA軟工]結對項目

軟工結對項目

## 一、 Github項目位址

## 二、 PSP表格

Psp personal software progress stages 預估耗時 實際耗時
planning 計劃 15 20
estimate 估計這個任務需要多少時間 10
development 開發 600 700
analysis 需求分析 25
design spec 生成設計文檔
design review 設計複審
coding standard 代碼規範
design 具體設計 30
coding 具體編碼 300 320
code review 代碼複審 60
test 測試 200 260
reporting 報告
test report 測試報告
size measuring 計算工作量 5
postmortem & process improvement plan 事後總結,提出過程改進計劃 40
合計 1335 1575

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

  • Information Hiding
    • 資訊隐蔽将可能變化的因素封裝起來,遇到變化時對該子產品進行操作即可,不會影響到其它子產品。對于這一點,我們将一些必要的部件封裝成類,将其變量成員設定為私有,通路隻通過方法來通路,之後再修改也隻要針對需要修改的子產品進行修改,不需要改動其它子產品。
  • Interface Design
    • 在考慮接口設計時,要厘清楚哪些子產品之間是有聯系的,設計時應該要遵循下面那個高内聚低耦合的原則。
  • Loose Coupling
    • 内聚是一個子產品内各個元素彼此結合的緊密程度,耦合是一個軟體結構内不同子產品之間互連程度的度量 。這個原則要求我們設計時明确每一個子產品的作用,子產品與子產品之間盡量無牽扯,就如生成數獨與解數獨,這兩個子產品盡可能的沒有什麼共用的東西,而生成數獨與挖空有聯系。

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

**求解數獨,對生成的數獨挖空得到題目,指令行部分(參數分析等)和異常處理**:

  涉及到Core子產品中的以下函數:

  _declspec(dllexport) bool __stdcall solve(int puzzle[81], int solution[81]);

       _declspec(dllexport) bool __stdcall blank(int puzzle[81], int mode);

       _declspec(dllexport) bool __stdcall blank(int puzzle[81], int lower, int upper, bool unique);

       Node* toMatrix(int puzzle[81]);

       void remove(Node* c);

       void resume(Node* c);

       int dance(Node* head, int solution[81]);

       int dance(Node* head, int &tag);

       bool DLX(int puzzle[81], int solution[81]);

       bool isunique(int puzzle[81]);

       void Delete(Node* n);

       void init(Node* n);

blank用于對生成好的數獨挖空得到題目,挖空時根據傳入的參數在範圍内随機挖空,會調用isunique判斷挖完的題目是否有唯一解,傳入參數不合法的話會抛出異常并傳回false

由于在實際上玩數獨的時候比起解的個數,挖空數對難度的影響要大得多,是以本項目難度的劃分不考慮解的個數,具體為:

難度 挖空數
1 20~35
2 36~45
3 46~55

求解使用了DLX算法,使用連結清單實作:

  • DLX用于求解數獨,會調用dance(Node* head, int solution[81])和toMatrix,傳回值表示是否有解
  • isunique用于判斷一個數獨題目是否有唯一解,會對數獨進行回溯求解,直到回溯完成或找到兩個解。是對DLX進行少量重寫得到的,傳回值表示是否有唯一解
  • isunique中調用dance(Node* head, int &tag),用tag标記解的個數,tag=2表示有兩個以上的解,=1表示唯一解,=0表示無解
  • init用于初始化連結清單的節點,Delete用于釋放連結清單的空間
  • 設計函數時将DLX算法分為2個部分:生成連結清單和周遊。其中周遊會多次執行删除元素和恢複元素的操作。是以将DLX的整個過程分為4個函數
  • toMatrix用于生成連結清單,傳回head節點
  • dance(Node* head, int solution[81])用于得出一個解并儲存到solution數組裡
  • resume和remove分别用于恢複和删除連結清單中元素

生成部分:

  涉及到的函數有:

  generate(int number, int result[][81])

  void produce(int total, int nums[], int block_num, int & count_total, int count_nums, int sudo[9][9], int result[][81]);

  bool isinraw(int num, int raw_num, int sudo[9][9]);

  bool isincolumn(int num, int c_num, int sudo[9][9]);

  int insert(int num, int blocknum, int marked[], int sudo[9][9]);

  void out(int sudo[9][9], int result[][81], int count_total);

生成函數采用的是回溯的方法,該方法雖然不能確定生成的所有數獨都不是等價的,但是是盡可能少的産生等價的數獨。

## 五、 畫出UML圖顯示計算子產品部分各個實體之間的關系

![](http://images2017.cnblogs.com/blog/1225050/201710/1225050-20171014224411824-1696566443.png)

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

基本隻對求解進行了改進,但是花費了大量時間(7-8h),第一版完成時在x86下進行了簡單的測試,當時還沒有發現問題,在第一版完成以後我們組進行了第四階段的交換測試,發現在x64環境下DLX求解變得十分慢而且記憶體消耗極大,在長時間調試後找到了原因。

一開始通過中斷調試找到了死循環,發現多次挖空時沒有還原挖過的數獨,修正了挖空的邏輯,但是問題沒有得到解決,然後通過記憶體分析發現連結清單的Node節點占用了大量的記憶體,可能是釋放連結清單空間出了問題。

經過長時間的分析後發現在求解完成時連結清單是不完整的,一部分節點被删除,但是變成了孤立節點,沒有被釋放(仔細想想求解完了的時候連結清單已經幾乎被删空了……)。

主要問題出現在dance函數上:

```

int Core::dance(Node* head, int solution[81])

{

if (head->right == head)

{

return 1; //得到一個解,傳回

}

Node *i = head->right->right;

Node *temp = head->right;

while (i != head)

if (i->countcount)

temp = i;

i = i->right;

if (temp->down == temp)return 0;

remove(temp);

i = temp->down;

while (i != temp)

Node *j = i->right;

solution[i->num[0] * 9 + i->num[1]] = i->num[2];

while (j != i)

{

remove(j->col);

j = j->right;

}

if (dance(head, solution))

return 1; //依次傳回1直到退出遞歸

j = i->left;

resume(j->col);

j = j->left;

solution[i->num[0] * 9 + i->num[1]] = 0;

i = i->down;

resume(temp); //用于恢複連結清單,但是退出遞歸的時候不會執行

return 0;

}

dance函數遞歸執行,在一開始連結清單是完整的,在遞歸過程中會逐漸删除連結清單中的元素,當執行到:
           

if (head->right == head)

return 1;

這段語句的時候,遞歸的dance函數會逐層傳回1并退出。但是這時,連結清單裡面隻剩一個head了。以原本的連結清單索引為基礎釋放隻能釋放head,其他元素已經變成了孤立節點。
可以通過在退出遞歸前還原連結清單或者建立額外的索引解決, 最後經過測試選擇了效率較高的方法,在連結清單中加入了一個額外的指針,使整個連結清單形成一個一維連結清單,以此為索引進行釋放就不會漏掉節點,最終解決了問題。
**性能分析**:
![](http://images2017.cnblogs.com/blog/1225050/201710/1225050-20171015121942262-503926097.png)

![](http://images2017.cnblogs.com/blog/1225050/201710/1225050-20171015121951355-302497886.png)



兩張分别對應生成數獨題和求解,生成參數為 -n 100 -u,求解為多次求解一個較難的17個數的數獨,由于generate調用了blank,blank調用了isunique,isunique調用了toMatrix和dance,時間主要消耗在轉換成矩陣上,求解也花了一定量的時間。求解的時候選用了難度較大的數獨,這時dance函數消耗的時間明顯上升,在使用-u參數的時候,由于需要多次求解和重新挖空,效率很慢,生成100個就要5s,目前還沒有找到什麼比較好的解決辦法。

<br>
##<font color=gray > 七、 描述這些做法的優缺點, 說明你是如何把它們融入結對作業中的</font>
- 優點
    - 有利于設計層面上保證程式的正确性
    - 獲得更優秀的設計
    - 編寫契約可以幫助開發者更好地了解代碼
    - 契約有助于測試(契約可以随時關閉或者啟用)
    - 簡化調試
    - 支援複用
- 缺點
    - 撰寫契約需要時間
    - 開發者需要時間學習撰寫良好契約的思想和技術
    - 需要大量的實踐
    - 目前的主流語言中,隻有Eiffel支援契約,而且僅僅支援順序式程式(sequential program)的契約
- 事先約定好各接口的參數定義以及能夠輸入的合法值,約定好各種條件下的傳回值。

<br>
##<font color=gray > 八、 計算子產品部分單元測試展示。展示出項目部分單元測試代碼,并說明測試的函數,構造測試資料的思路。并将單元測試得到的測試覆寫率截圖,發表在部落格中。要求總體覆寫率到90%以上,否則單元測試部分視作無效</font>
測試生成:
           

TEST_METHOD(TestGenerate)

        {

            // TODO: 在此輸入測試代碼

            int sudo[9][9];

            Core test;

            int result[3][81];

            test.generate(3, result);

            for (int i = 0; i < 3; i++)

            {

                for (int j = 0; j < 81; j++)

                {

                    sudo[j / 9][j % 9] = result[i][j];

                }

                for (int j = 0; j < 9; j++)

                    for (int k = 0; k < 9; k++)

                    {

                        for (int n = 0; n < 9; n++)

                        {

                            if (n != j)

                            {

                                Assert::AreEqual(false, sudo[i][j] == sudo[i][n]);

                            }

                            if (n != i)

                                Assert::AreEqual(false, sudo[i][j] == sudo[n][j]);

                        }

                    }

            }

            int blankNum = 0;

            int puzzle[81] = { 8,1,2,7,5,3,6,4,9,9,4,3,6,8

                ,2,1,7,5,6,7,5,4,9,1,2,8,3,1,5,4,2,3,7,8,9

                ,6,3,6,9,8,4,5,7,2,1,2,8,7,1,6,9,5,3,4,5,2

                ,1,9,7,4,3,6,8,4,3,8,5,2,6,9,1,7,7,9,6,3,1

                ,8,4,5,2 };

            int backup[81] = { 8,1,2,7,5,3,6,4,9,9,4,3,6,8

            test.generate(1, 1, result);

            for (int i = 0; i < 81; i++)

                if (result[0][i] == 0)

                    blankNum++;

                    result[0][i] = backup[i];

            Assert::AreEqual((20 <= blankNum&&blankNum <= 35), true);

            blankNum = 0;

            test.generate(1, 2, result);

            Assert::AreEqual((36 <= blankNum&&blankNum <= 45), true);

            test.generate(1, 3, result);

            Assert::AreEqual((46 <= blankNum&&blankNum <= 55), true);

            test.generate(1, 20, 55, true, result);

            Assert::AreEqual((20 <= blankNum&&blankNum <= 55), true);

            test.generate(1, 40, 40, true, result);

            Assert::AreEqual((40 == blankNum), true);

        }

分别針對生成的三種接口進行測試,檢測生成的數獨終局是否合法/題目是否符合要求
測試求解:
           

TEST_METHOD(TestSolve)

// TODO: 在此輸入測試代碼

int puzzle[81] = {

8,0,0,0,0,0,0,0,0

,0,0,3,6,0,0,0,0,0

,0,7,0,0,9,0,2,0,0

,0,5,0,0,0,7,0,0,0

,0,0,0,0,4,5,7,0,0

,0,0,0,1,0,0,0,3,0

,0,0,1,0,0,0,0,6,8

,0,0,8,5,0,0,0,1,0

,0,9,0,0,0,0,4,0,0,};

int result[81] = { 0 };

int answer[81] = { 8,1,2,7,5,3,6,4,9,9,4,3,6,8

,2,1,7,5,6,7,5,4,9,1,2,8,3,1,5,4,2,3,7,8,9

,6,3,6,9,8,4,5,7,2,1,2,8,7,1,6,9,5,3,4,5,2

,1,9,7,4,3,6,8,4,3,8,5,2,6,9,1,7,7,9,6,3,1

,8,4,5,2 };

int wrong[81] = { 8,1,2,7,5,3,6,4,9,9,4,3,6,8

,2,1,7,5,6,7,5,4,9,1,8,2,3,1,5,4,2,3,7,8,9

Core test;

bool isvalid = true;

test.solve(puzzle, result);

for (int i = 0; i < 81; i++)

puzzle[i] = answer[i];

Assert::AreEqual(result[i], answer[i]);

test.blank(puzzle, 20, 55, true);

isvalid = test.solve(wrong, result);

Assert::AreEqual(isvalid, false);

isvalid = test.solve(answer, result);

Assert::AreEqual(isvalid, true);

依次求解一個較難的數獨,随機挖空的唯一解數獨,不合法的數獨和完整的合法數獨,檢測求解的結果和傳回值正不正确。
同時檢測了判斷唯一解算法的正确性,若傳回的數獨不是唯一解的話求解出的result和answer可能不相同,不能通過測試。
單元測試覆寫率:
![](http://images2017.cnblogs.com/blog/1225050/201710/1225050-20171015122401809-1660118922.png)


<br>
##<font color=gray > 九、 計算子產品部分異常處理說明。在部落格中詳細介紹每種異常的設計目标。每種異常都要選擇一個單元測試樣例釋出在部落格中,并指明錯誤對應的場景</font>
項目的異常分為三種:

|異常|描述|
|:--|:--|
|numberException|輸入的數獨生成數量異常|
|boundException|輸入的挖空邊界異常|
|modeException|輸入的難度異常|

           

TEST_METHOD(TestException)

int result[3][81];

try {

test.generate(2000000, result);

catch (numberException &e)

Assert::AreEqual(0, 0);

test.generate(20000, 2, result);

test.generate(20000, 20, 40, false, result);

test.generate(3, 5, result);

catch (modeException &e)

test.generate(3, 41, 40, false, result);

catch (boundException &e)

test.generate(3, 12, 40, false, result);

test.generate(3, 32, 70, false, result);

單元測試分别針對使用-c和-n時的參數,難度不為1-3,邊界超出範圍和lower>upper等情況進行了測試:

|測試函數|具體描述|
|:--|:--|
|test.generate(2000000, result);|使用參數-c,超出了最大範圍1000000|
|test.generate(20000, 2, result)|使用參數-n,超出了最大範圍10000|
|test.generate(20000, 20, 40, false, result)|使用參數-n,超出了最大範圍10000|
|test.generate(3, 5, result)|使用參數-m,輸入的參數不在1~3範圍内|
|test.generate(3, 41, 40, false, result)|使用參數-r,lower>upper|
|test.generate(3, 12, 40, false, result)|使用參數-r,lower超出範圍|
|test.generate(3, 32, 70, false, result)|使用參數-r,upper超出範圍|



<br>
##<font color=gray > 十、界面子產品的詳細設計過程。在部落格中詳細介紹界面子產品是如何設計的,并寫一些必要的代碼說明解釋實作過程</font>
界面主要分為兩部分:菜單欄和主界面。菜單欄實作的功能有難度的選擇,help文檔,主界面實作的功能有重新開始、清空、提示、送出、計時以及最佳記錄。
**數獨盤**:通過81個單行文本框實作,繼承了QLineEdit類實作MyLineEdit類,重寫了contextMenuEvent方法,新增了hint信号以及槽函數為了實作提示功能,新增setRead方法,使得題目中的數字背景變色以及hint失效。
**模式選擇**:在菜單中實作,通過點選執行相對應的槽函數,實作難度的改變。
**提示**:通過點選相應空格的右鍵進行提示,該動作的槽函數在自己寫的MyLineEdit類裡,該函數是發送信号,在主界面接受到信号後調用相應的函數求解并提示。
**計時計最佳記錄**:通過實作定時器和QTimer實作,讓定時器每隔一秒觸發一次,更新時間并輸入到文本框當中。最佳紀錄是在送出成功解正确後比對目前時間與最佳紀錄,若目前時間短則更新,通過一個字元串儲存在類裡。

具體布局代碼如下:
           

//布局

QGridLayout layout = new QGridLayout;

layout->addWidget(restartButton, 0, 0, 2, 1, 0); // 重新開始按鈕

layout->addWidget(clearButton, 0, 1, 2, 1, 0); // 清空按鈕

layout->addWidget(bestRecord, 0, 8, 1, 1, Qt::AlignRight); //最佳紀錄時間

layout->addWidget(nowRecord, 1, 8, 1, 1, Qt::AlignRight); //已經用時

layout->addLayout(layoutSudo, 2, 0, 1, 9, Qt::AlignCenter); // 99的方正

layout->addWidget(submitButton, 3, 0, 1, 9, Qt::AlignCenter); // 送出按鈕

計時部分代碼如下:
設定1秒觸發一次,調用updateTime函數,加一秒并更新文本框。
           
QLineEdit *bestRecord;  // 顯示最好記錄的時間
QLineEdit *nowRecord;  // 顯示現在的時間
QString record;

    connect(timer, SIGNAL(timeout()), this, SLOT(updateTime()));
timer->start(1000);
           

void QtGuiApplication2::updateTime()

*TimeRecord = TimeRecord->addSecs(1);

nowRecord->setText(TimeRecord->toString("hh:mm:ss"));

提示部分代碼如下:
點選hint後,發出信号,在主界面接收到信号調用hintClicked()函數。

           

class MyLineEdit :public QLineEdit

protected:

QMenu * hintMenu = new QMenu();

QAction * action = hintMenu->addAction("hint");

void contextMenuEvent(QContextMenuEvent *event);

signals:

void hint();

public slots :

void hintCliked();

connect(blocks[nowNum], SIGNAL(hint()), this, SLOT(hintCliked())); // 提示綁定事件

<br>
##<font color=gray > 十一、界面子產品與計算子產品的對接。詳細地描述UI子產品的設計與兩個子產品的對接,并在部落格中截圖實作的功能</font>
- 要使用計算子產品的功能首先要配置相應的dll,我參考了這篇[部落格](http://www.cnblogs.com/houkai/archive/2013/06/05/3119513.html)。
- 接下來是具體的調用部分
    - 首先建立了一個core對象,供調用函數。
    - 初始化的時候調用generate(1, mode, sudo),生成一個簡單的數獨局。
    - 重新開始按鈕點選後,需要生成新的數獨局,同樣調用generate函數。
    - 在送出按鈕點選後,需要先判斷填寫是否正确,錯誤的話應該顯示正确答案,此時先調用solve函數判斷解的正确性,若錯誤,再次調用solve函數。
    - 模式選擇中,每次選擇一個模式,都需要生成相應模式的數獨,調用generate函數并傳入相應的模式難度參數。
    - 提示被點選之後,要在該空顯示出數字,調用solve并取出對應位置的數填寫即可。
- 實作的功能有:模式選擇,幫助菜單,重新開始,清空目前所有,計時功能,最佳紀錄,提示功能
![](http://images2017.cnblogs.com/blog/1225050/201710/1225050-20171015011522012-384442684.png)
![](http://images2017.cnblogs.com/blog/1225050/201710/1225050-20171015011532105-1832188551.png)



![](http://images2017.cnblogs.com/blog/1225050/201710/1225050-20171015011253730-187854503.png)
![](http://images2017.cnblogs.com/blog/1225050/201710/1225050-20171015011740887-991600821.jpg)
![](http://images2017.cnblogs.com/blog/1225050/201710/1225050-20171015011749918-1944386366.jpg)









<br>
##<font color=gray > 十二、 描述結對的過程,提供非擺拍的兩人在讨論的結對照片</font>
主要的讨論都在網上進行,在确定結對後線上讨論得出分工,之後制訂計劃并開始編碼
讨論的結果是我負責除GUI和生成完整數獨算法以外的任務
在下課的時候會當面讨論一些線上無法解決的問題
![](http://images2017.cnblogs.com/blog/1225050/201710/1225050-20171015122556449-2080805473.jpg)



<br>
##<font color=gray > 十三、 結對程式設計的優點和缺點</font>
- 優點
    - 在開發層次,結對程式設計能提供更好的設計品質和代碼品質,兩人合作能有更強的解決問題的能力
    - 對開發人員自身來說,結對工作能帶來更多的信心,高品質的産出能帶來更高的滿足感
    - 在心理上,  當有另一個人在你身邊和你緊密配合, 做同樣一件事情的時候,  你不好意思開小差, 也不好意思糊弄
    - 在企業管理層次上,結對能更有效地交流,互相學習和傳遞經驗,能更好地處理人員流動。因為一個人的知識已經被其他人共享
    - 降低學習成本。一邊程式設計,一邊共享知識和經驗,有效地在實踐中進行學習
    - 可以讓程式設計環境有效地貫徹Design
- 缺點
    - 有時候,程式員們會對一個問題各執己見(代碼風格可能會是引發技術人員口水戰的地方),争吵不休,反而産生重大内耗
    - 面對新手,有經驗的老手可能會覺得非常的煩躁。不合适的溝通會導到團隊的不和諧
    - 有經驗的人更喜歡單兵作戰,找個人來站在他背後看着他可能會讓他感到非常的不爽,最終導緻程式設計時受到情緒影響,反而出現反作用
    - 兩個人在一起工作可能會出現工作精力不能集中的情況,程式員可能會交談一些與工作無關的事情
    - 對于有不同習慣的程式設計人員,可以在起工作會産生麻煩,甚至沖突

<br>

##<font color=gray>十四、 結對的每一個人的優點和缺點在哪裡 (要列出至少三個優點和一個缺點)</font>
- 個人
    - 優點
        - 代碼子產品分的比較清楚
        - 比較細緻
        - 遇到問題願意去溝通
    - 缺點
        - 不太主動去彙報進度
- 同伴
    - 優點
        - 積極溝通
        - 有規劃,知道應該做什麼
        - 願意嘗試
    - 缺點
        - 代碼不怎麼寫注釋,了解起來較困難

<br>


##<font color=gray>附加題: 界面子產品,測試子產品和核心子產品的松耦合</font>
合作小組:15061122 15061144
問題主要出現在兩方的測試子產品和Core對接,由于我們在測試的時候都用了非标準接口的函數,在測試時出現了問題。

我們對測試進行了針對性修改後可以使用他們的Core。

他們在測試時發現generate(100,40,55,true,result)會導緻程式異常,這是導緻發現第六部分提到的問題的原因(在此感謝他們)

由于我們測試的時候是在x86上,他們是在x64上,是以出現了完全不一樣的結果,對問題的處理和優化在第六部分有詳細說明。

gui與核心子產品對接沒什麼問題,隻是在設計上有所不同,他們把無解的數獨認為是異常,而我們能夠判斷,而我們的gui需要能判斷該數獨是否有解,也就是說,接上他們的核心子產品有可能填錯數獨而發生異常。