【軟工】結對編程做業

項目 內容
這個做業屬於哪一個課程 2019BUAA軟件工程
這個做業的要求在哪裏 做業要求
我在這個課程的目標是 完成本次做業,同時熟悉結對編程
這個做業的幫助 熟悉了vs2017的部分操做,同時對結對編程有了比較深入的理解

1、本次做業項目github地址


項目地址html

2、開發前PSP表格


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

3、接口設計


模塊之間經過他們的api通訊,一個模塊不須要知道另一個模塊的內部狀況,這就被稱爲信息隱藏或封裝。封裝提升了軟件的可重用性,由於模塊之間不緊密相連,最後封裝也下降了構建大型系統的風險,即便整個系統不可用,可是這些模塊多是有用的。c++

接口設計有六大原則:單一職責原則、里氏替換原則、依賴倒置原則、接口隔離原則、迪米特法則以及開閉原則。這些原則對接口設計有了很大的約束,本次咱們實現的做業沒有使用繼承,因此里氏替換原則天然不會違背,同時因爲設計比較簡單,其餘的原則也都一一知足了。git

藕合度是度量一個代碼單元在使用時與其餘單元的關係。最理想,最鬆散的耦合,是一個單元無需其餘代碼單元特別的配合而可使用。這裏的Loose Coupling實際上和單一職責原則有些相似,主要是爲了確保每個類或方法的做用盡可能單一,避免耦合。github

在咱們的做業中,最核心的模塊是core,內部有一個實例化的儲存圖主要信息的private對象graph,因此外部沒法調用,這很好的實現了封裝。同時,計算模塊(core)和圖模塊(graph)是分開的,因此彼此不會干擾,各司其職,耦合度天然知足條件。算法

4、計算模塊接口的設計與實現


咱們的計算模塊是Core,同時儲存圖信息以及和圖相關的運算與運算結果都存儲在graph中,graph是在實例化Core對象時自動實例化的一個對象,其存儲在Core 中。因爲get_chain_char和get_chain_word的內部實現其實很類似,僅僅是邊的權值不一樣,因此咱們也在Core內部抽象出一個公共接口,這樣外部在調用get_chain_word和get_chain_char的時候只須要調用這個公共接口就行了。具體的流程圖以下:

算法大體思路:首先對問題進行建模:該問題能夠抽象爲在一個有向圖中求最長路徑的問題:每一個單詞的首尾字母分別爲有向圖中的節點,而後一個單詞就抽象爲一條邊,最長鏈即爲最長路徑。編程

  • 第一步是對輸入的異常進行檢測,若是輸入的單詞不合法或者指定的頭尾不合法則報錯。
  • 第二步是對輸入的單詞進行建圖。這裏有兩種模式,若是是調用get_chain_word則權值是1,不然權值爲單詞的長度。同時,對於自環的狀況咱們單獨處理的,至關於後面拓撲排序的時候直接跳過了自環。
  • 第三步是進行拓撲排序。此步驟的目的是判斷是否有環以及爲後面的算法作準備
  • 第四步是根據上一步的排序結果選擇不一樣的算法。若是有環則進行暴力搜索,若是沒環則根據排序結果從尾到頭作一次dp便可。

獨到之處windows

  • 節省空間。每條邊只存了一個權值,即1或其單詞長度。在一開始建圖的時候就會加判斷,節省了空間。
  • 代碼複用。無論有環仍是無環都會先調用拓撲排序的算法,該算法即進行了排序也能夠判斷是否有環。同時,咱們的內部接口實現也作到了代碼複用與減小代碼之間的耦合。
  • 實現簡潔。拓撲排序時咱們採用的是經典的dfs的寫法,沒有采用入度和出度的寫法,實現方式更簡潔,效率也不錯,同時代碼看上去也很簡單。

5、UML圖


因爲vs2017能夠自動導出類圖,咱們最終生成的類圖以下:api

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


【引自小夥伴的博客數組

這一部分中咱們花費了約3-4小時。咱們主要針對有環狀況的算法進行改進。
因爲在一個有向有環圖中尋找最長的路徑是一個NP問題,從算法自己的角度來看不管如何都逃不開NP這個坑,所以咱們使用了普通的DFS來進行。咱們優化的地方在於將算法中訪存的消耗盡量下降。起初咱們使用了vector做爲路徑存儲的數據結構,在通過一次性能分析後咱們發現遞歸部分中退棧操做不少,所以咱們將vector改成一維數組,將原先的pop_back變成棧頂指針的--操做,從而下降了時間消耗。數據結構

下圖爲有環狀況下消耗CPU最多的函數,即DFS的主函數
image.png-109.6kB

咱們使用一個9000餘詞的文件對於無環狀況進行了測試,結果發現算法在無環狀況下表現良好,get_chain_word/char中消耗最多的部分其實是接口中進行malloc的部分。

image.png-86.7kB

7、Design by Contract, Code Contract


Design by Contract, 即契約編程:咱們在聲明一個函數/方法的時候,對函數的輸入和輸出所具有的性質是有所指望和規定的。有時候這種性質會被咱們明確的寫出來,有時候會被咱們忽略掉。這些指望和規定就是Contract。

其好處是責任的細化。每一個程序猿都只須要處理本身的契約範圍負責。同時,有責任的時候也能很快的定位到我的。

在咱們的做業中,因爲咱們是兩我的共同開發和debug的,因此有些地方並無很嚴格的按照這個要求來作。

8、計算模塊部分單元測試展現


【引自小夥伴的博客

單元測試範例
image.png-29.2kB

單元測試部分咱們分別對無環和有環以及各類異常狀況進行了測試,共構造了19個單元測試。
構造測試的思路大致上是儘量覆蓋各個分支。因爲char和word兩個接口在具體運行時僅僅是初始化權值不一樣,所以測試中咱們更注重於各類特殊狀況的測試,如開頭和結尾的自環等。

單元測試的分支覆蓋率爲97%,其中包含了各類異常測試的覆蓋。
testr.JPG-30.9kB

9、計算模塊部分異常處理說明


異常一 輸入的頭或尾不是字母

在輸入的頭或尾不是字母的狀況下會拋出異常,如下爲測試樣例:

void invalid_head()
    {
        char* words[] =
        {
            "aba",
            "avdc",
            "fewt"
        };
        char* results[100];
        int len = gen_chain_word(words, 3, results, 1, 0, false);
    }
        TEST_METHOD(WordsException9)
    {
        Assert::ExpectException<std::invalid_argument>([&] {exception_test::invalid_head(); });
        try
        {
            exception_test::invalid_head();
        }
        catch (const std::exception& e)
        {
            Assert::AreEqual("Core: invalid head or tail", e.what());
        }
    }

異常二 有環但沒有指定-r參數

在檢測到圖中有環可是輸入的參數中沒有-r參數,會拋出異常:

void has_loop()
    {
        char* words[] =
        {
            "aba",
            "aca",
            "fewt"
        };
        char* results[100];
        int len = gen_chain_word(words, 3, results, 0, 0, false);
    }
    TEST_METHOD(WordsException8)
    {
        Assert::ExpectException<std::invalid_argument>([&] {exception_test::has_loop(); });
        try
        {
            exception_test::has_loop();
        }
        catch (const std::exception& e)
        {
            Assert::AreEqual("Core: loop deteced in words, you need to use -r to parse.", e.what());
        }
    }

異常三 輸入的words數組中有空串

若是輸入的words數組中某個字符串爲空的話也會拋出異常,測試樣例以下:

void empty_string()
    {
        char* words[] =
        {
            "",
            "avdc",
            "fewt"
        };
        char* results[100];
        int len = gen_chain_word(words, 3, results, 0, 0, false);
    }
        TEST_METHOD(WordsException6)
    {
        Assert::ExpectException<std::invalid_argument>([&] {exception_test::empty_string(); });
        try
        {
            exception_test::empty_string();
        }
        catch (const std::exception& e)
        {
            Assert::AreEqual("Core: empty string in words", e.what());
        }
    }

異常四 輸入的串中有無效字符

若是輸入的字符串中有無效字符,則會拋出該異常:

void invalid_char_test()
    {
        char* words[] =
        {
            "a12345",
            "avdc",
            "fewt"
        };
        char* results[100];
        int len = gen_chain_word(words, 3, results, 0, 0, true);
    }
        TEST_METHOD(WordsException5)
    {
        Assert::ExpectException<std::invalid_argument>([&] {exception_test::invalid_char_test(); });
        try
        {
            exception_test::invalid_char_test();
        }
        catch (const std::exception& e)
        {
            Assert::AreEqual("Core: invalid char in words", e.what());
        }
    }

異常五 沒有足夠單詞

若是輸入的words數組的長度len <= 1則一樣會拋出異常,由於鏈的長度要求必須大於1,因此若是單詞個數爲負數或01的狀況也會拋出異常,測試樣例以下:

void not_enough_words()
    {
        char* words[] =
        {
            "a"
        };
        char* results[100];
        int len = gen_chain_word(words, 1, results, 0, 0, true);
    }
        TEST_METHOD(WordsException7)
    {
        Assert::ExpectException<std::invalid_argument>([&] {exception_test::not_enough_words(); });
        try
        {
            exception_test::not_enough_words();
        }
        catch (const std::exception& e)
        {
            Assert::AreEqual("Core: not enough words", e.what());
        }
    }

10、命令行模塊


【引自小夥伴的博客

界面主要分爲兩大部分:命令行參數讀取解析及文件讀取。

在命令行參數解析部分,因爲目前已有不少的開源庫可供使用,本着不重複造輪子的原則咱們使用了一個較爲輕量的頭文件庫cxxopts來處理命令行參數。這個庫能夠自動將各個參數的值讀出並對不合法的狀況拋出異常。但因爲其涉及的不合法狀況較爲樸素,我對一些相對複雜的不合法輸入進行了處理,例如同時輸入-w和-c、或輸入了兩個-w的狀況。

test2.JPG-34.8kB

最終我將命令行參數讀取和分析封裝在base類的parse_arguments函數中,函數經過參數將讀取到的值返回。

在文件讀取部分,我設計的思路是按字符從文件頭開始向後掃描,並在出現特殊字符的位置斷開,從而將文件中的合法單詞分隔出來。
test3.JPG-38.2kB
注意到在讀文件過程當中我對單詞的長度也作了約束,如讀到長度過長的單詞則會拋出異常。最終將以上單詞保存在base類成員中的數組內便可。

11、命令行與計算模塊的對接


【引自小夥伴的博客

dll模塊對接方面我使用了Base模塊顯式調用Dll的方法(即僅藉助dll文件,不借助lib文件)。經過windows.h中的LoadLibrary和GetProcAddress實現。最終將調用的過程與開頭讀文件、解析參數等過程結合,造成完整的運行程序。

image.png-65.7kB

12、結對過程


因爲咱們兩人大多數時間都是在線上用teamviewer交流,加上前兩次線下討論模塊設計的時候忘了拍照片,因此咱們提供咱們線上交流的截圖。

咱們結對的過程其實仍是蠻順利的。一開始咱們在大概設計好模塊以後便各司其職,我負責處理計算部分,另外一個小夥伴寫完了輸入和異常。最後的dll生成和單元測試因爲我不是很熟悉,因此咱們是連上teamviewer,而後小夥伴來操做,我提供一些幫助。因此整個過程其實蠻愉快的。就是咱們開始的時間有點晚了以及中間有些事情耽擱致使進度有點偏慢。不過此次結對編程確實是一次很棒的經歷,收穫頗豐。

十3、結對編程的優缺點


結對編程最大的優勢在於,同一份代碼通過兩我的的複審以後代碼的質量會有很大提高。同時,兩人在寫代碼的過程當中不斷交流思路,代碼的架構也會更加豐富。

缺點在於若是兩個配合很差的話可能形成1 + 1 < 2的後果,主要緣由是問題過於簡單致使浪費時間以及問題過於難致使聚在一塊兒討論也是浪費時間。

成員 優勢 缺點
庹東成 (1)對計算模塊比較熟。 (2)對問題可以提出本身的見解並實踐 (3)對待小夥伴很友善 對vs2017不太熟,致使後面生成dll和測試的時候進展緩慢
周博聞 (1)對vs2017很熟,以及操做熟練 (2)認真仔細,可以想到不少易忽略的點 (3)對待小夥伴很友善 對於題目要求有些地方不是很瞭解

十4、PSP表格回填


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

十5、模塊鬆耦合


【引自小夥伴的博客

咱們與1610106一、16061118組互換了Core.dll進行測試。

咱們的dll文件能夠在對方的界面模塊下運行:


但對方的Core模塊並不能被咱們的base模塊調用。詢問得知對方的Core以C#實現,當我使用dumpbin查看其中導出的函數時並不能查看到任何信息。

image.png-62.2kB 在上網查閱了一些資料後我瞭解到C++調用C#須要將dll轉換爲C#的類組件,貌似並無相似於c++這樣直接調用的方法。

相關文章
相關標籤/搜索