軟件工程結對編程做業

軟件工程結對編程做業


項目 內容
這個做業屬於哪一個課程 2019春季計算機學院軟件工程(羅傑)(北京航空航天大學)
這個做業的要求在哪裏 結對項目-最長單詞鏈
我在這個課程的目標是 學習軟件工程方法與相關工具,提高本身的工程能力,鍛鍊本身與他人協同開發以及開發較大項目的能力
這個做業在哪一個具體方面幫助我實現目標 學習並嘗試實踐告終對編程,鍛鍊了代碼尤爲測試方面的基礎

Github地址: LongestWordChain

1. PSP表格

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

2. 關於Information Hiding, Interface Design和Loose Coulpling

  • Information Hiding

信息隱藏我認爲便是封裝的概念,在咱們實現過程當中,咱們將界面模塊(Base)和計算模塊(Core)中的操做均封裝爲接口,而且Core中的core和graph兩個類也分別進行封裝,從而外部僅能調用Core模塊的接口,而Base暴露的也僅僅是開始運行的方法。c++

  • Interface Design

接口設計咱們嚴格參照了做業的要求,即Core模塊只暴露兩個函數int gen_chain_word(char* words[], int len, char* result[], char head, char tail, bool enable_loop)int gen_chain_char(char* words[], int len, char* result[], char head, char tail, bool enable_loop)。其中須要注意的是二者的返回值均是單詞鏈的單詞個數(做業中的「單詞鏈長度」)。git

  • Loose Coulpling

咱們從開始編程時便有意地爲鬆耦合作準備,具體操做即是開始時將Base和Core分開,而且Base也僅僅調用以上約定的兩個函數。在將Core封裝爲dll後Base的代碼幾乎能夠直接應用,而且最終僅須要更換Core.dll文件即可切換計算模塊。github

3. 計算模塊Core的詳細設計

計算模塊主要由兩個類實現,分別是core和graph。其中core主要負責參數的讀入以及動態規劃算法的實現,而graph主要負責圖的創建以及圖論方面算法的實現,例如拓撲排序和DFS主要在graph中完成。算法

core類中,因爲暴露在外的兩個接口gen_chain_wordgen_chain_char僅僅是在初始化圖的時候有所不一樣(word函數將圖的邊權值所有初始化爲1,而char函數按單詞長度初始化權值),所以最終咱們選擇使用一個公共的函數做爲內部的實現,經過一個參數區分調用的是哪個接口。編程

算法流程圖:
未命名文件 (2).png-22.4kB
具體實現中,首先在單詞進入Core前就要對words數組進行檢查,若其中包含非法字符則報錯,不然將全部字母轉爲小寫再進入具體算法。以後進入delete_repeat_words將重複的單詞刪掉,由於單詞鏈中不容許出現重複單詞。windows

接下來進入main_func,這裏須要按照單詞列表和輸入的模式創建圖,並初始化好各個數據結構。以後對圖進行拓撲排序,如有環且沒有指定-r則報錯,不然按照圖中所示分別選擇兩中不一樣的算法進行計算。數組

在計算結束後須要檢查答案是不是一個詞,由於動態規劃僅能找到最長的邊鏈條,但並不能排除存在單個詞很是長的狀況,所以若是答案符合這種形式仍須要刪掉這個詞並從新計算。網絡

算法的獨到之處:數據結構

  • 使用鄰接表存儲圖結構,但並無使用指針,從實現過程上講不容易出現指針的問題
  • 使用動態規劃完成了拓撲排序和無環狀況下的鏈查找,而且利用動態規劃的初始化解決了開頭和結尾字母的問題,從而實現上更爲簡潔
  • 有環算法中使用數組模擬堆棧,減小單位訪存的時間

4. UML圖

image.png-129.4kB

5. 計算接口的性能改進

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

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

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

image.png-86.7kB

6. Design by Contract, Code Contract

契約式設計的主要思路是設計接口時提供接口的先驗條件、後驗條件和不變式,這讓我想起了OO課上學習的JSF。
契約式設計的優勢有:

  • 爲編程者提供的良好的思路,而且能在編程的過程當中儘早發現可能出現的問題
  • 契約式設計有助於後續的測試,使一些沒必要要的Bug不會出如今測試中
  • 契約式設計可以契合文檔,使得新的開發者在閱讀文檔時對代碼有更加深刻的理解

契約式設計的缺點是上手較難,一開始不容易嚴格按照要求實現,而且部分邏輯較爲複雜的函數使用契約時也比較麻煩。

在咱們實現的過程當中,咱們對函數運行先後的狀態事先進行了約定,從而能順利地將不一樣的函數串接起來造成完整的程序。但咱們並無在實際實現中添加斷言等來進行嚴格的約束。

7. 計算模塊的單元測試

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

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

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

8. 計算模塊異常處理設計

計算模塊因爲僅僅將兩個函數接口暴露給用戶,從而用戶在調用函數時能夠存在各類不合法的輸入。主要的不合法輸入包括如下幾點

  • words數組不合法,其中的單詞存在不合法字符(非字母字符)
  • head和tail不合法,輸入爲非字母字符
  • len不合法,輸入的len太小或過大(咱們規定了輸入單詞個數在10000個)
  • 輸入單詞存在環,卻沒有指定-r參數

針對以上的錯誤咱們分別構造了以下測試

void invalid_char_test() //不合法字符
    {
        char* words[] =
        {
            "a12345",
            "avdc",
            "fewt"
        };
        char* results[100];
        int len = gen_chain_word(words, 3, results, 0, 0, true);
    }
    void empty_string() //空串
    {
        char* words[] =
        {
            "",
            "avdc",
            "fewt"
        };
        char* results[100];
        int len = gen_chain_word(words, 3, results, 0, 0, false);
    }
    void not_enough_words() //單詞不夠(一個詞不成鏈)
    {
        char* words[] =
        {
            "a"
        };
        char* results[100];
        int len = gen_chain_word(words, 1, results, 0, 0, true);
    }
    void has_loop() //(存在環)
    {
        char* words[] =
        {
            "aba",
            "aca",
            "fewt"
        };
        char* results[100];
        int len = gen_chain_word(words, 3, results, 0, 0, false);
    }
    void invalid_head() /(非法頭部)
    {
        char* words[] =
        {
            "aba",
            "avdc",
            "fewt"
        };
        char* results[100];
        int len = gen_chain_word(words, 3, results, 1, 0, false);
    }
    void invalid_tail() //(非法尾部)
    {
        char* words[] =
        {
            "aa",
            "avdc",
            "fewt"
        };
        char* results[100];
        int len = gen_chain_word(words, 3, results, 0, 1, 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());
    }
}

(在參閱了一些同窗的博客後我感受這種單元測試的寫法有點彆扭,實際上僅僅是爲了調用ExceptException接口而搞得如此複雜……)

9. 界面模塊(命令行)的設計與實現

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

在命令行參數解析部分,因爲目前已有不少的開源庫可供使用,本着不重複造輪子的原則咱們使用了一個較爲輕量的頭文件庫cxxopts來處理命令行參數。

Github地址:https://github.com/jarro2783/cxxopts/

簡單的使用介紹:

引入頭文件庫:
    #include <cxxopts.hpp>

建立一個cxxopts的Option實例,輸入參數是程序名和程序的描述

    cxxopts::Options options("MyProgram", "One line description of MyProgram");

使用add_options方法添加參數,其中括號內第一個參數爲短參數和長參數名,第二個參數是描述,可選的第三個參數是選項後輸入的實參。

    options.add_options()
      ("d,debug", "Enable debugging")
      ("f,file", "File name", cxxopts::value<std::string>())
      ;

使用parse方法對輸入的參數進行分析,其中這裏輸入的兩個參數爲main函數讀取的argc和argv。

    auto result = options.parse(argc, argv);

使用 `result.count("option")` 獲取名爲「option」的參數出現的次數,並使用:

    result["option"].as<type>()

來獲取其值。注意到若是option不存在會拋出異常。

(以上內容摘自項目Github主頁)

這個庫能夠自動將各個參數的值讀出並對不合法的狀況拋出異常。但因爲其涉及的不合法狀況較爲樸素,我對一些相對複雜的不合法輸入進行了處理,例如同時輸入-w和-c、或輸入了兩個-w的狀況。

test2.JPG-34.8kB

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

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

界面模塊的單元測試

界面模塊我也寫了一些單元測試,主要測試以上的兩個函數。其中大部分測試均針對命令行參數的解析。例如:
image.png-29kB

10. 界面模塊與計算模塊的對接

dll模塊對接方面我使用了Base模塊顯式調用Dll的方法(即僅藉助dll文件,不借助lib文件)。經過windows.h中的LoadLibrary和GetProcAddress實現。最終將調用的過程與開頭讀文件、解析參數等過程結合,造成完整的運行程序。
image.png-65.7kB
最終只要將Core.dll文件放置在可執行文件同一目錄下,即可以運行程序計算結果。

鬆耦合測試及遇到的問題

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

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

但對方的Core模塊並不能被咱們的base模塊調用。詢問得知對方的Core以C#實現,當我使用dumpbin查看其中導出的函數時並不能查看到任何信息。
image.png-62.2kB
在上網查閱了一些資料後我瞭解到C++調用C#須要將dll轉換爲C#的類組件,貌似並無相似於c++這樣直接調用的方法。

11. 結對過程

我和個人結對隊友因爲宿舍距離很遠,每次去咖啡廳又很貴,最終選擇了使用Teamviewer+視頻的方式進行結對合做。好在使用Teamviewer時網絡很流暢,而且對方也能夠在個人電腦上操做,甚至比兩我的在線下結對還要方便一些。這也讓我對結對編程有了更深的認識,結對不必定必須兩我的坐在一塊兒才能完成,只要溝通渠道方便、代碼均可以看到,就能方便地進行結對編程。固然因爲咱們開始的晚了一些,中間某些時候也不得不採用了並行的方式。

12. 結對編程的優缺點

結對編程相比於我的開發和雙人並行開發而言,好處是實時的複審能夠避免不少小的問題。我在和隊友結對編程的過程當中,發生的一些筆誤或者算法方面的疏忽均可以被及時地發現並改正。而且結對編程在討論中進行,對於編程過程當中模塊間的約束也更加清晰。結對編程的缺點是假如兩我的時間常常錯開(假若有12小時時差),或兩人的時間安排有差,則很難進行。

成員 優勢 缺點
周博聞 1.可以較快解決各類工程方面的問題 2.可以想到一些易被忽略的點 3. 快速上手新知識並加以應用 有些地方不夠細緻,致使浪費不少時間找小bug
庹東成 1.算法能力很強 2.思路清晰,遇到新的問題能找出不少辦法 3.結對中效率高,對隊友很友善 命名及代碼格式有些不太規範
相關文章
相關標籤/搜索