結對項目 - 最長單詞鏈

結對項目 - 最長單詞鏈


項目 內容
本做業屬於北航軟件工程課程 博客園班級連接
做業要求請點擊連接查看 做業要求
我在這門課程的目標是 成爲一個具備必定經驗的軟件開發人員
這個做業在哪一個具體方面幫助我實現目標 經過結對項目,鍛鍊極限編程的能力

1、GitHub項目地址

Release版程序的項目倉庫地址見此git

2、PSP表格與預估開發時間

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

3、教科書與設計

  • Information Hiding,即信息隱藏,與面向對象編程中的封裝性有殊途同歸之妙。具有封裝性得面向對象編程隱藏了某一方法的具體運行步驟,取而代之的是經過消息傳遞機制發送消息,在面向對象編程中就是調用一個類的方法。咱們在本次做業中將程序的核心計算模塊Core分離出來,爲它配置了幾個API,外界調用API傳入特定的參數得到指望的結果,但不會知道內部的運行細節,於是也避免了對程序的運行狀態形成破壞。經過這樣的手段,咱們實現了代碼的信息隱藏。
  • Interface Design,即接口設計,是任何程序設計過程的重中之重。接口設計這個詞顧名思義,是將一個程序抽象成具備一個或幾個特定功能的模塊,外界須要與程序進行信息交換時,只能經過這幾個接口進行。在本次做業中,咱們參照做業要求設計了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)這兩個接口,以及List<string> GenerateChain(bool outputToFile = false)這個專爲C#程序調用的接口。這些接口使用簡單方便、意義一目瞭然,咱們認爲這是一個很優雅的接口設計。
  • Loose Coupling,即鬆耦合,意味着程序的不一樣模塊之間耦合度很低,能夠單獨剝離出來使用。在本次做業中,咱們將核心計算模塊Core.dll單獨分離出來,供GUI調用,同時Core本身自己也能夠經過命令行調用,實現了總體的鬆耦合。

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

  • 本次做業是實現一個最長單詞鏈計算程序,根據咱們的分析,這個程序本質上是一個算法問題,沒有必要把各類元素抽象成對象,元素之間也不須要維護本身的狀態,能夠說只是一個輸入=>輸出的純函數。所以,咱們沒有采用嚴格的面向對象編程方法(先把元素抽象成對象,再設計每一個對象的屬性、方法和接口,最後把類整合到一塊兒),而是隻寫了一個類,在類裏面設計一些純函數,用面向過程的方法編寫的程序。事實證實,面向過程的編程範式並無給咱們帶來麻煩,反而減小了面向對象方法所帶來的一些性能開銷,也讓總體的結構變得簡潔易懂。在實際的工程開發項目中,這樣的小程序只會做爲一個模塊出現,爲這樣簡單的需求制定一個龐大的面向對象設計,在如此緊張的工期之下,咱們認爲是不划算的。程序員

  • 咱們選擇了更爲現代、IDE支持更友好、開發工具更爲豐富的C#語言進行編碼。github

  • 如下是一份全部函數的詳情列表算法

    函數名 返回值 參數列表 說明
    ConvertWordArrayToList List<string> char*[] words, int len 工具函數,爲了將C風格的char*[]轉換爲C#風格的List
    ConvertWordListToArray int IReadOnlyList<string> wordList, char*[] words 工具函數,爲了將C#風格的List轉換爲C風格的char*[]
    GenerateChain int int mode, char*[] words, int len, char*[] result, char head, char tail, bool enable_loop 將做業要求中的兩個接口合二爲一後的包裝函數
    gen_chain_word int char*[] words, int len, char*[] result, char head, char tail, bool enable_loop 做業要求中的接口之一
    gen_chain_char int char*[] words, int len, char*[] result, char head, char tail, bool enable_loop 做業要求中的接口之二
    CheckValid void char head, char tail 檢查-h和-t選項是否後接一個英文字母做爲參數
    GenerateChain List<string> bool outputToFile = false 爲C#程序提供的核心計算模塊調用接口
    Core None args 做爲獨立程序運行時能夠接收命令行參數的構造函數
    Core None string input = "", int mode = 0, char head = '\0', char tail = '\0', bool enableLoop = false, string outputFilePath = @"solution.txt", bool inputIsFile = true 做爲類庫時能夠接收程序參數的構造函數
    ParseCommandLineArguments void IReadOnlyList<string> args 解析命令行參數
    ExceptWithCause void ProgramException exception 用於拋出自定義異常
    ReadContentFromFile string None 用於將文本文件讀入到內存中
    ReadContentFromFile string string filePath 用於將文本文件讀入到內存中
    DivideWord List<string> string content 將字符串按做業要求切分爲單詞列表
    FindLongestChain List<string> List<string> words, int mode, char last = '\0', List<string> current = null 核心算法函數,用於尋找最長單詞鏈
    OutputChain void IEnumerable<string> chain 用於將計算出來的最長單詞鏈輸出到文件
  • 函數之間的關係大體以下所示:編程

    1. get_chain_wordget_chain_char這兩個函數是供C/C++調用而提供的接口,因爲其功能高度類似,所以使用GenerateChain(int mode, char*[] words, int len, char*[] result, char head, char tail, bool enable_loop)這個函數做爲其底層函數,功能上的區別由mode參數進行區分。
    2. 三個重載的GenerateChain方法並沒有本質區別,只是傳入的參數不一樣。它們都先讀取用戶輸入的內容(這裏可能會用到ReadContentFromFile函數把文件中的單詞列表加載到內存中),而後調用DivideWord方法將單詞列表切分紅一個單詞List,隨後使用FindLongestChain方法計算出最長單詞鏈。根據選項的不一樣,可能還會調用OutputChain方法將單詞鏈輸出到指定文件之中。
    3. Core構造函數有兩個重載,一個是經過命令行調用時使用,一個是做爲類庫時使用
    4. ParseCommandLineArguments這個方法當且僅當核心模塊經過命令行使用時纔會被調用。它的做用是解析命令行選項,並將相關的參數存入類中。
    5. ExceptWithCause負責程序拋出異常以後的處理。通常狀況下,它會將異常再次向外拋出。
    6. ConvertWordArrayToListConvertWordListToArray這兩個函數是工具函數,爲了適配C/C++風格的接口而誕生。它們的做用是List<string>和char*[]的互相轉換。
  • 算法的關鍵函數是FindLongestChain,在本次做業中咱們採用了BFS搜索算法,BFS已經在前序課程中屢次使用,所以在博客中我省略了流程圖和算法關鍵的說明。算法的獨到之處、創新點和計算關鍵將在第六節中詳細闡述。小程序

5、UML

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

  • 在設計與編寫計算模塊性能接口的時候,咱們總共花了約3小時的時間。c#

  • 最開始,咱們採用了徹底的暴力搜索算法,不管是否打開了-r選項,都會用BFS搜索算法從頭至尾算出全部的單詞鏈,而後選取其中最長的一個返回。對於-r選項來講,這樣的算法無可厚非,由於隱含單詞環的最長單詞鏈問題等價於有向有環圖的最長路徑問題,是一個NPC問題,沒法在多項式內求解,只能採用暴力搜索的方法。可是,若是程序參數不含-r選項,再像有-r選項那樣,從頭至尾尋找出全部的單詞鏈,再判斷是否有單詞環,在性能上就損失太大了。數組

  • 所以最後咱們在FindLongestChain方法內部加入了對於-r選項的判斷,若是-r選項沒有打開,則一旦檢測到單詞環就馬上從函數中返回,避免沒必要要的、過大的計算開銷。數據結構

  • 咱們本來計劃使用一些特殊的數據結構(如將一個單詞抽象成首字母、尾字母和長度的三元組),對性能作進一步的優化。但作項目的時間有限、工期緊張,於是沒能如計劃完成。若是採用數據結構優化,能夠將本來n*n!複雜度的算法優化至n!,但都屬於指數複雜度,在單詞量較大的時候顯現不出區別。框架

  • 性能分析工具給出的性能結果以下圖所示

CPU資源消耗最大的函數無疑是FindLongestChain這個用於計算最長單詞鏈的函數。

7、契約式設計

  • Design by Contract,即契約式設計,是一種設計計算機軟件的方法。這種方法要求軟件設計這爲軟件組建定義正式的、精確的而且可驗證的接口,這樣,爲傳統的抽象數據類型又增長了先驗條件、後驗條件和不變式。
  • 若是在面向對象程序設計中一個類的函數提供了某種功能,那麼它要:
    • 指望全部調用它的客戶每跑一都保證必定的進入條件,這就是函數的先驗條件:客戶的義務和供應商的權利,這樣它就不用去處理不知足先驗條件的狀況。
    • 保證退出時給出特定的屬性,這就是函數的後驗條件:供應商的義務,顯然也是客戶的權利。
    • 在進入時假定、並在退出時保持一些特定的屬性:不變條件。
  • 契約式設計既有優勢也有缺點:
    • 在多人合做的大型項目中,任何人都不會有能力完整掌控全局的全部代碼。所以,就須要將整個程序分紅許多個子模塊,每一個模塊由不一樣的開發人員負責。所以,在軟件項目的一開始,就須要爲每個模塊定義其與其它模塊的接口,以及這些接口應當具備的性質,即先驗條件、後驗條件和不變條件。不這樣的話,模塊之間就無從配合,整個軟件也會成爲一團沒法成型的稀泥。只有當各個模塊的接口都定義良好,軟件才能以分模塊開發的方式繼續構建。
    • 可是,在小型的、只有兩三我的的項目中,契約式設計就變成了一種累贅。小團隊追求的是小步快跑的敏捷開發模式,一般一個項目只有一兩個星期的時間,這個時候若是先花上幾天時間去設計接口,顯然是不划算的作法。此外,因爲小團隊一般使用更爲便捷的腳本語言進行開發,接口每每難以設計獲得,當編碼進行到必定時間以後若是忽然發現接口沒法知足需求,再作改動的話就會耗費大量的時間。
    • 在真實的軟件工程項目中,大型項目每每傾向於契約式設計,小型項目每每選擇避開契約式設計。
  • 在本次做業中,咱們將核心計算模塊的接口GenerateChain作成了惟一對外暴露的接口,並進行了完善的單元測試,能夠知足事先規定的先驗條件、後驗條件和不變條件。以後,GUI模塊和計算模塊就能夠同步並行開發,這個接口的契約保證了最後GUI和計算模塊的順利對接。經過這樣的方式,咱們把契約式設計用在了此次結對項目之中。

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

  • 咱們的部分單元測試代碼以下所示:

    [TestMethod()]
        public unsafe void gen_chain_wordTest()
        {
            TestGenChain(_wordList1, _wordChain1WithR, enableLoop: true);
            TestGenChain(_wordList2, _wordChain2);
            TestGenChain(_wordList2, _wordChain2WithHe, head: 'e');
            TestGenChain(_wordList2, _wordChain2WithTt, tail: 't');
        }
        [TestMethod()]
        public void gen_chain_charTest()
        {
            TestGenChain(_wordList2, _wordChain2WithC, mode: 1);
        }
        private unsafe void TestGenChain(List<string> wordList, List<string> expectedChain, int mode = 0, char head = '\0', char tail = '\0', bool enableLoop = false)
        {
            var resultArray = CreateStringArray(wordList.Count, 100);
            var wordListArray = ConvertToArray(wordList);
            var len = 0;
            switch (mode)
            {
                case 0:
                    len = Core.gen_chain_word(wordListArray, wordListArray.Length, resultArray, head, tail, enableLoop);
                    break;
                case 1:
                    len = Core.gen_chain_char(wordListArray, wordListArray.Length, resultArray, head, tail, enableLoop);
                    break;
                default:
                    Assert.Fail();
                    break;
            }
            var result = ConvertToList(resultArray, len);
            CollectionAssert.AreEqual(expectedChain, result);
        }
        private static unsafe char*[] CreateStringArray(int length = 100, int wordLength = 100)
        {
            var array = new char*[length];
            for (var i = 0; i < length; i++)
            {
                var word = new char[wordLength];
                fixed (char* wordPointer = &word[0])
                {
                    array[i] = wordPointer;
                }
            }
            return array;
        }
        private static unsafe List<string> ConvertToList(char*[] words, int len)
        {
            var wordList = new List<string>();
            for (var i = 0; i < len; i++)
            {
                wordList.Add(new string(words[i]));
            }
            return wordList;
        }
        private static unsafe char*[] ConvertToArray(IReadOnlyList<string> words)
        {
            var wordList = new char*[words.Count];
            for (var i = 0; i < words.Count; i++)
            {
                var word = new char[100];
                fixed (char* wordPointer = &word[0])
                {
                    int j;
                    for (j = 0; j < words[i].Length; j++)
                    {
                        word[j] = words[i][j];
                    }
                    word[j] = '\0';
                    wordList[i] = wordPointer;
                }
            }
            return wordList;
        }
    
        [TestMethod()]
        public void ParseCommandLineArgumentsTest()
        {
            TestWrongArgs("-w");
            TestWrongArgs("-c");
            TestWrongArgs("-h");
            TestWrongArgs("-t");
            TestWrongArgs("-");
            TestWrongArgs("-h 1");
            TestWrongArgs("-t 233");
            TestCorrectArgs("-w input.txt");
            TestCorrectArgs("-c input.txt");
            TestCorrectArgs("-w input.txt -h a");
            TestCorrectArgs("-w input.txt -t b");
            TestWrongArgs("abcdefg");
            TestWrongArgs("-w input.txt -h 9");
            TestWrongArgs("-w input.txt -t 0");
            TestWrongArgs("-c input.txt -h 7 -t 1");
            TestWrongArgs("-w input.txt -h 123");
            TestWrongArgs("-c input.txt -t 321");
            TestWrongArgs("-h a");
            TestWrongArgs("-w input.txt -w input.txt");
            TestWrongArgs("-w input.txt -c input.txt");
            TestWrongArgs("-c input.txt -c input.txt");
            TestWrongArgs("-c input.txt -w input.txt");
            TestWrongArgs("-w input.txt -h a -h c");
            TestWrongArgs("-w input.txt -t a -t c");
            TestWrongArgs("-w input.txt -r -r");
            TestCorrectArgs("-w input.txt -r");
        }
        private static void TestWrongArgs(string arguments)
        {
            var args = System.Text.RegularExpressions.Regex.Split(arguments, @"\s+");
            try
            {
                var core = new Core(args);
                Assert.Fail();
            }
            catch (ProgramException)
            {
    
            }
        }
        private static void TestCorrectArgs(string arguments)
        {
            var args = System.Text.RegularExpressions.Regex.Split(arguments, @"\s+");
            try
            {
                var core = new Core(args);
            }
            catch (Exception)
            {
                Assert.Fail();
            }
        }
  • 咱們的單元測試主要對四個函數進行測試,分別是gen_chain_wordgen_chain_charGenerateChainParseCommandLineArguments。這四個函數是計算模塊的核心函數,也是最容易出現Bug的函數。

  • 咱們構造測試數據的思路是:

    • 對於三個計算單詞鏈的函數,構造兩個類型爲```List<string>的字符串列表,第一個列表是輸入的單詞,第二個列表是預期生成的單詞鏈,而後將第一個列表傳入待測函數,將返回的列表與預期單詞鏈進行比對。
    • 對於解析命令行參數的函數,咱們經過TestCorrectArgsTestWrongArgs這兩個函數對正確和錯誤的參數進行測試。如上文中的單元測試代碼展現的那樣,咱們構造了各類各樣的輸入參數,對ParseCommandLineArgs的每個語句都進行了測試。
  • 單元測試覆蓋率的截圖以下:

由上圖可見,WordChain模塊的單元測試的總體覆蓋率達到了93%,符合做業中的要求。須要注意的是,因爲ReSharper統計語句覆蓋率的計算方式有問題,會把拋出異常後的下一條空語句也加入統計範圍,而程序在拋出異常後便不可能在繼續執行,所以那幾條空語句本不應出如今覆蓋率統計之中。排除因統計軟件形成的影響以後,經過查看詳細的逐語句單元測試報告,能夠看到咱們的核心計算模塊WordChain的單元測試覆蓋率達到了接近100%的水平。

9、異常處理

  • 針對本次做業,咱們設計瞭如下幾種異常,它們的設計目標和對應的錯誤場景標註在異常名字的下方:

    • public class ProgramException : ApplicationException

      最長單詞鏈程序的異常基類,由程序自己邏輯引起的異常所有繼承自ProgramException

    • public class ModeNotProvidedException : ProgramException

      沒有指定-w或-c時引起的異常

    • public class ArgumentErrorException : ProgramException

      參數錯誤時引起的異常,如-h或-t後面不是英文字母、-w和-c同時出現等等

    • public class FileException : ProgramException

      因爲文件讀寫錯誤形成的異常基類

    • public class InputFileException : FileException

      找不到輸入文件引起的異常

    • public class FileNotReadableException : FileException

      沒有權限讀文件引起的異常

    • public class FileNotWritableException : FileException

      沒有權限寫文件引起的異常

    • public class WordRingException : ProgramException

      單詞環存在而沒有提供-r參數引起的異常

  • 可以覆蓋這些異常的單元測試樣例前文中已給出,這裏再次複製粘貼一遍:

    [TestMethod()]
        public void ParseCommandLineArgumentsTest()
        {
            TestWrongArgs("-w");
            TestWrongArgs("-c");
            TestWrongArgs("-h");
            TestWrongArgs("-t");
            TestWrongArgs("-");
            TestWrongArgs("-h 1");
            TestWrongArgs("-t 233");
            TestCorrectArgs("-w input.txt");
            TestCorrectArgs("-c input.txt");
            TestCorrectArgs("-w input.txt -h a");
            TestCorrectArgs("-w input.txt -t b");
            TestWrongArgs("abcdefg");
            TestWrongArgs("-w input.txt -h 9");
            TestWrongArgs("-w input.txt -t 0");
            TestWrongArgs("-c input.txt -h 7 -t 1");
            TestWrongArgs("-w input.txt -h 123");
            TestWrongArgs("-c input.txt -t 321");
            TestWrongArgs("-h a");
            TestWrongArgs("-w input.txt -w input.txt");
            TestWrongArgs("-w input.txt -c input.txt");
            TestWrongArgs("-c input.txt -c input.txt");
            TestWrongArgs("-c input.txt -w input.txt");
            TestWrongArgs("-w input.txt -h a -h c");
            TestWrongArgs("-w input.txt -t a -t c");
            TestWrongArgs("-w input.txt -r -r");
            TestCorrectArgs("-w input.txt -r");
        }
        private static void TestWrongArgs(string arguments)
        {
            var args = System.Text.RegularExpressions.Regex.Split(arguments, @"\s+");
            try
            {
                var core = new Core(args);
                Assert.Fail();
            }
            catch (ProgramException)
            {
    
            }
        }
        private static void TestCorrectArgs(string arguments)
        {
            var args = System.Text.RegularExpressions.Regex.Split(arguments, @"\s+");
            try
            {
                var core = new Core(args);
            }
            catch (Exception)
            {
                Assert.Fail();
            }
        }

10、界面模塊的設計過程

  • 在本次結對編程做業中,咱們選取了C#做爲開發語言,而C#受到微軟的支持,對GUI有自然的適應性。咱們選取了WinForm做爲GUI的圖形界面框架,用少許的代碼便完成了GUI的功能。

  • 咱們的設計目標是:

    • 支持兩種導入單詞文本的方式:導入單詞文本文件,或直接在界面上輸入單詞並提交;
    • 提供可供用戶交互的按鈕;
    • 實現-w -c -h -t -r這五個參數的功能;
    • 對於異常狀況,彈窗給用戶提示;
    • 將結果直接輸出到界面上,或者導出到指定文件。
  • WinForm在Visual Studio開發環境下,能夠直接使用拖動的方式進行界面的設計與構建。咱們將選項作成CheckBox的形式,導入文件作成OpenFileDialog的形式,將異常狀況提示作成彈窗的形式,將-h和-t參數作成下拉列表選擇形式。

  • GUI的實現過程因爲採用了Visual Studio做爲開發環境,所以界面的搭建過程都是可視化的,下面的截圖清晰地展現了這一點。這也是咱們選用C#做爲開發語言的重要緣由。

  • 不過,有時也會須要寫一些代碼,以完成按鈕觸發動做的功能。例如,在實現」點擊按鈕選擇單詞文件「的過程當中,咱們便用到了下面這段代碼:

    private void select_file_Click(object sender, EventArgs e)
        {
            OpenFileDialog dlg = new OpenFileDialog();
            if (dlg.ShowDialog() == DialogResult.OK)
                inputText.Text = dlg.FileName;
        }
  • 整體而言,因爲C#對GUI的自然優秀支持,GUI的設計仍是比較順利的。

11、界面模塊與計算模塊的對接

  • 前文提到,因爲咱們的界面模塊和GUI模塊都採用了C#語言進行開發,所以在GUI調用計算模塊接口時,就沒必要採起C/C++風格的帶有字符指針數組參數的接口,而是可使用更爲簡捷易用的List<string>類型進行對接。

  • 在本次做業中,咱們爲計算模塊設計了GenerateChain這個C#風格接口,專門用於和GUI對接。GUI在獲取到用戶選擇的參數後,將參數經過Core構造函數傳遞給核心計算模塊,而後調用GenerateChain方法便可計算出最長單詞鏈。

  • 對接的過程十分簡單,僅需兩行代碼便可完成。這也展示了咱們這次做業設計的優越性。

  • 最終,帶有GUI的程序總體實現的功能截圖以下:

12、結對過程與照片

  • 結對照片以下所示:

十3、結對編程與結對組員的優勢與缺點

  • 結對編程的優勢與缺點
    • 優勢
      1. 結對編程讓兩我的所寫的代碼不斷地處於」複審「的過程,避免牛仔式的編程
      2. 結對編程的過程是一個互相督促的過程,因爲督促的壓力,程序員得以更認真地工做
      3. 結對編程避免了「個人代碼」仍是「他的代碼」的問題,使得代碼的責任再也不屬於某我的,而是屬於兩我的,進而屬於整個團隊,這樣可以幫助創建集體擁有代碼的意識
    • 缺點
      1. 處於探索階段的項目若是採用結對編程的方式,就會致使研究沒法深刻、鑽研沒法繼續
  • 結對組員的優勢與缺點
    • 我(16061125 周雨飛)
      • 優勢
        1. 開發效率高
        2. 擅長快速學習和使用新的技術
        3. 代碼風格優秀、工程意識較強
      • 缺點
        1. 寫代碼時不夠仔細,有時候會出現一些小Bug
    • 他(16061145 周國傑)
      • 優勢
        1. 技術能力強、算法功底好
        2. 擅長深刻鑽研程序的性能部分
        3. 思想睿智,適合擔任團隊領導者
      • 缺點
        1. 喜歡裝弱

十4、PSP表格與實際開發時間

PSP2.1 Personal Software Process Stages 預估耗時(分鐘) 實際耗時(分鐘)
Planning 計劃 --- ---
· Estimate · 估計這個任務須要多少時間 1350 1440
Development 開發 --- ---
· Analysis · 需求分析 30 30
· Design Spec · 生成設計文檔 60 30
· Design Review · 設計複審(和同事審覈設計文檔) 30 30
· Coding Standard · 代碼規範(爲目前的開發制定合適的規範) 30 30
· Design · 具體設計 120 150
· Coding · 具體編碼 480 600
· Code Review · 代碼複審 120 180
· Test · 測試(自我測試,修改代碼,提交修改) 240 300
Reporting 報告 --- ---
· Test Report · 測試報告 60 15
· Size Measurement · 計算工做量 60 15
· Postmortem & Process Improvement Plan · 過後總結,並提出過程改進計劃 120 60
Total 合計 1350 1440
相關文章
相關標籤/搜索