结对项目 - 最长单词链

结对项目 - 最长单词链


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

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
相关文章
相关标签/搜索