項目 | 內容 |
---|---|
本做業屬於北航軟件工程課程 | 博客園班級連接 |
做業要求請點擊連接查看 | 做業要求 |
我在這門課程的目標是 | 成爲一個具備必定經驗的軟件開發人員 |
這個做業在哪一個具體方面幫助我實現目標 | 經過結對項目,鍛鍊極限編程的能力 |
Release版程序的項目倉庫地址見此git
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 | ??? |
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#程序調用的接口。這些接口使用簡單方便、意義一目瞭然,咱們認爲這是一個很優雅的接口設計。本次做業是實現一個最長單詞鏈計算程序,根據咱們的分析,這個程序本質上是一個算法問題,沒有必要把各類元素抽象成對象,元素之間也不須要維護本身的狀態,能夠說只是一個輸入=>輸出的純函數。所以,咱們沒有采用嚴格的面向對象編程方法(先把元素抽象成對象,再設計每一個對象的屬性、方法和接口,最後把類整合到一塊兒),而是隻寫了一個類,在類裏面設計一些純函數,用面向過程的方法編寫的程序。事實證實,面向過程的編程範式並無給咱們帶來麻煩,反而減小了面向對象方法所帶來的一些性能開銷,也讓總體的結構變得簡潔易懂。在實際的工程開發項目中,這樣的小程序只會做爲一個模塊出現,爲這樣簡單的需求制定一個龐大的面向對象設計,在如此緊張的工期之下,咱們認爲是不划算的。程序員
咱們選擇了更爲現代、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 | 用於將計算出來的最長單詞鏈輸出到文件 |
函數之間的關係大體以下所示:編程
get_chain_word
和get_chain_char
這兩個函數是供C/C++調用而提供的接口,因爲其功能高度類似,所以使用GenerateChain(int mode, char*[] words, int len, char*[] result, char head, char tail, bool enable_loop)
這個函數做爲其底層函數,功能上的區別由mode參數進行區分。GenerateChain
方法並沒有本質區別,只是傳入的參數不一樣。它們都先讀取用戶輸入的內容(這裏可能會用到ReadContentFromFile
函數把文件中的單詞列表加載到內存中),而後調用DivideWord
方法將單詞列表切分紅一個單詞List,隨後使用FindLongestChain
方法計算出最長單詞鏈。根據選項的不一樣,可能還會調用OutputChain
方法將單詞鏈輸出到指定文件之中。Core
構造函數有兩個重載,一個是經過命令行調用時使用,一個是做爲類庫時使用ParseCommandLineArguments
這個方法當且僅當核心模塊經過命令行使用時纔會被調用。它的做用是解析命令行選項,並將相關的參數存入類中。ExceptWithCause
負責程序拋出異常以後的處理。通常狀況下,它會將異常再次向外拋出。ConvertWordArrayToList
和ConvertWordListToArray
這兩個函數是工具函數,爲了適配C/C++風格的接口而誕生。它們的做用是List<string>和char*[]的互相轉換。算法的關鍵函數是FindLongestChain,在本次做業中咱們採用了BFS搜索算法,BFS已經在前序課程中屢次使用,所以在博客中我省略了流程圖和算法關鍵的說明。算法的獨到之處、創新點和計算關鍵將在第六節中詳細闡述。小程序
在設計與編寫計算模塊性能接口的時候,咱們總共花了約3小時的時間。c#
最開始,咱們採用了徹底的暴力搜索算法,不管是否打開了-r選項,都會用BFS搜索算法從頭至尾算出全部的單詞鏈,而後選取其中最長的一個返回。對於-r選項來講,這樣的算法無可厚非,由於隱含單詞環的最長單詞鏈問題等價於有向有環圖的最長路徑問題,是一個NPC問題,沒法在多項式內求解,只能採用暴力搜索的方法。可是,若是程序參數不含-r選項,再像有-r選項那樣,從頭至尾尋找出全部的單詞鏈,再判斷是否有單詞環,在性能上就損失太大了。數組
所以最後咱們在FindLongestChain
方法內部加入了對於-r選項的判斷,若是-r選項沒有打開,則一旦檢測到單詞環就馬上從函數中返回,避免沒必要要的、過大的計算開銷。數據結構
咱們本來計劃使用一些特殊的數據結構(如將一個單詞抽象成首字母、尾字母和長度的三元組),對性能作進一步的優化。但作項目的時間有限、工期緊張,於是沒能如計劃完成。若是採用數據結構優化,能夠將本來n*n!複雜度的算法優化至n!,但都屬於指數複雜度,在單詞量較大的時候顯現不出區別。框架
性能分析工具給出的性能結果以下圖所示
CPU資源消耗最大的函數無疑是FindLongestChain
這個用於計算最長單詞鏈的函數。
GenerateChain
作成了惟一對外暴露的接口,並進行了完善的單元測試,能夠知足事先規定的先驗條件、後驗條件和不變條件。以後,GUI模塊和計算模塊就能夠同步並行開發,這個接口的契約保證了最後GUI和計算模塊的順利對接。經過這樣的方式,咱們把契約式設計用在了此次結對項目之中。咱們的部分單元測試代碼以下所示:
[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_word
、gen_chain_char
、GenerateChain
和ParseCommandLineArguments
。這四個函數是計算模塊的核心函數,也是最容易出現Bug的函數。
咱們構造測試數據的思路是:
TestCorrectArgs
和TestWrongArgs
這兩個函數對正確和錯誤的參數進行測試。如上文中的單元測試代碼展現的那樣,咱們構造了各類各樣的輸入參數,對ParseCommandLineArgs
的每個語句都進行了測試。單元測試覆蓋率的截圖以下:
由上圖可見,WordChain模塊的單元測試的總體覆蓋率達到了93%,符合做業中的要求。須要注意的是,因爲ReSharper統計語句覆蓋率的計算方式有問題,會把拋出異常後的下一條空語句也加入統計範圍,而程序在拋出異常後便不可能在繼續執行,所以那幾條空語句本不應出如今覆蓋率統計之中。排除因統計軟件形成的影響以後,經過查看詳細的逐語句單元測試報告,能夠看到咱們的核心計算模塊WordChain的單元測試覆蓋率達到了接近100%的水平。
針對本次做業,咱們設計瞭如下幾種異常,它們的設計目標和對應的錯誤場景標註在異常名字的下方:
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(); } }
在本次結對編程做業中,咱們選取了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的設計仍是比較順利的。
前文提到,因爲咱們的界面模塊和GUI模塊都採用了C#語言進行開發,所以在GUI調用計算模塊接口時,就沒必要採起C/C++風格的帶有字符指針數組參數的接口,而是可使用更爲簡捷易用的List<string>類型進行對接。
在本次做業中,咱們爲計算模塊設計了GenerateChain
這個C#風格接口,專門用於和GUI對接。GUI在獲取到用戶選擇的參數後,將參數經過Core
構造函數傳遞給核心計算模塊,而後調用GenerateChain
方法便可計算出最長單詞鏈。
對接的過程十分簡單,僅需兩行代碼便可完成。這也展示了咱們這次做業設計的優越性。
最終,帶有GUI的程序總體實現的功能截圖以下:
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 |