项目 | 内容 |
---|---|
本做业属于北航软件工程课程 | 博客园班级连接 |
做业要求请点击连接查看 | 做业要求 |
我在这门课程的目标是 | 成为一个具备必定经验的软件开发人员 |
这个做业在哪一个具体方面帮助我实现目标 | 经过结对项目,锻炼极限编程的能力 |
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 |