结对编程实况录像-2022北航软工

项目内容
这个作业属于哪个课程2022春季软件工程(罗杰 任健)
这个作业的要求在哪里结对编程项目-最长英语单词链
我在这个课程的目标是学习软工的项目合作管理知识,提升软件开发技术
这个作业在哪个具体方面帮助我实现目标学习敏捷开发中的PSP与结对编程的思想并付诸实践

Part0 准备

1.必要信息

教学班级:周二班

项目地址:https://github.com/BUAADreamer/Longest-English-word-chain

成员:18373466 战晨曦,19373573 冯张驰

2.PSP开发时间估计

PSP2.1Personal Software Process Stages预估耗时(分钟)
Planning计划90
· Estimate· 估计这个任务需要多少时间90
Development开发790
· Analysis· 需求分析 (包括学习新技术)60
· Design Spec· 生成设计文档30
· Design Review· 设计复审 (和同事审核设计文档)30
· Coding Standard· 代码规范 (为目前的开发制定合适的规范)10
· Design· 具体设计60
· Coding· 具体编码360
· Code Review· 代码复审60
· Test· 测试(自我测试,修改代码,提交修改)180
Reporting报告100
· Test Report· 测试报告60
· Size Measurement· 计算工作量10
· Postmortem & Process Improvement Plan· 事后总结, 并提出过程改进计划30
合计980

Part1 设计

3.看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的

Information Hiding

多层设计中的层与层之间加入接口层

本次项目的接口层主要是顶层模块和各个模块的接口,使用了一个 List<string> CmdTestInterface.Solve(string[] args) 接口,只需要传递一个 参数字符串列表 args,即可返回最终得到的结果字符串列表

所有类与类之间都通过接口类访问

此特点主要是通过 CommandParserCore 之间的传递访问接口类 ResultRes 类进行传递

其中 ParseRes 类包含了允许成环,首尾字母,文件绝对路径字符串,最长的模式,命令字符串这些计算模块需要的数据。ParseRes 的实例既可以通过直接传给 Core 进行处理,也可以通过获得 ParseRes 类的数据进行传递来达到相同目的。

public class ParseRes
{
    public bool enableLoop = false;
    public char start;
    public char end;
    public string absolutePathOfWordList = "";
    public int mode = 0; //最长的模式 0表示单词数量 1表示字母个数
    public HashSet<char> cmdChars;
    public ParseRes(int mode, bool enableLoop, char start, char end, string absolutePathOfWordList, HashSet<char> cmdChars)
    {
        this.enableLoop = enableLoop;
        this.mode = mode;
        this.start = start;
        this.end = end;
        this.absolutePathOfWordList = absolutePathOfWordList;
        this.cmdChars = cmdChars;
    }
}
类的所有数据成员都是private,所有访问都是通过访问函数实现的

在计算核心模块中,所有的数据都是不可见的,对于外部其他模块来说,这些数据都不能直接访问,只能通过一些接口函数间接修改。

public class CalcuCore
{
    //数据变量
    private List<string> words;
    private Hashtable graph;
    private Hashtable inDegree;
    private Hashtable word2len;
    private char start;
    private char end;
    private bool enableLoop;
    private int graphMode;
    private int totalCharCount;
    private int MAXLEN = 20000;
    private int chainCount;
    
    //方法
    ...
}

Interface Design

本次项目我们和交换的小组定义了统一的四个 c# 函数接口,如下所示

public static int gen_chain_word(List<string> words, List<string> result, char head, char tail, bool enable_loop)

public static int gen_chains_all(List<string> words, List<string> result)

public static int gen_chain_word_unique(List<string> words, List<string> result)

public static int gen_chain_char(List<string> words, List<string> result, char head, char tail, bool enable_loop)

这四个接口函数都符合单一职责原则和接口隔离原则,即四个接口的设计都首先是功能单一的,而且几乎不可再分,每一个接口函数如果拼凑就冗余,如果细分则显得太琐碎。

Loose Coupling

本次 CLI 主程序和 GUI 主程序,LibraryCore 四个模块都是松耦合的,即每两个模块之间没有重复的部分。

可以用下图进行示意

CLIGUI 都调用了 LibraryCore 中的接口,而每个模块都只处理了自己那部分的事务,其他的功能通过接口调用实现。

4.计算模块接口的设计与实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处

计算模块接口设计

由于阅读了后面阶段的要求,并且预先找到了互换模块的小组,所以我们设计了适用于 C# 的类似于官方接口的接口,接口中每个方法的功能,和官方接口同名方法的功能相同。

接口设计如下

public static int gen_chain_word(List<string> words, List<string> result, char head, char tail, bool enable_loop);

public static int gen_chains_all(List<string> words, List<string> result);

public static int gen_chain_word_unique(List<string> words, List<string> result);

public static int gen_chain_char(List<string> words, List<string> result, char head, char tail, bool enable_loop);

我们将这些接口方法整合到了 PairTestInterface 中。在本地测试时,我们通过 CmdTestInterface 接口去驱动测试,调用 PairTestInterface 提供的接口,而 PairTestInterface 提供的接口调用 CalcuCore 中的具体计算方法。

计算部分的 UML 如下(CalcuCore 中的私有方法因为过多且无需外界关心,故没有列出):

UML

接口方法的实现,实际上是首先通过参数去实例化 CalcuCore(这是我们真正的核心计算类),然后调用 CalcuCore 类中对应的真正的计算方法 ,之后对返回的结果做异常处理,体现了层次化的思想。例如:

  • 对于gen_chain_word ,我们用给定的参数 wordsheadtailenable_loop 以及一些 default 参数(辅助建图)去实例化一个 CalcuCore,然后调用 CalcuCore 中的 getMaxWordCountChain 方法进行具体的计算。
  • 对于 CalcuCore
    • 其构造方法会使用 gen_chain_word 给定的参数,进行参数化建图。
    • getMaxWordCountChain 首先会调用数据检查方法 dataCheck,去检查数据中是否有隐含环以及是否允许在有环的情况下求解;如果数据中没有环,则调用重构图方法 refactorGraph 对图进行预处理,接着调用快速算法 fastGetMaxWordCountChainDAG 上跑 dp 求解;如果数据中有环,且要求在有环的情况下求解,则调用暴力算法 trivialGetMaxWordCountChain 求解最长链。
  • 对于异常:
    • 主要是处理结果过长以及数据有环且不能求解这两种情况。

具体算法设计

首先,如果单词 A 的尾字母和单词 B 的首字母相同,则以 AB 为结点,连接一条从 AB 的有向边,在 O ( n 2 ) O(n^2) O(n2) 的时间内建立一张有向图。其中 n n n 是不同单词的种类数。有向图的边权我们并不关心,但是点权的设置要根据情况来:如果我们求解的最长链是以个数为指标,则点权为 1;如果是以字母数为指标,那么点权为单词的长度。

然后就是每个具体接口的求解算法:

gen_chain_word 的求解算法主要是 getMaxWordCountChain ,而 getMaxWordCountChain 的算法分为两部分。如果图是一个有向无环图,那么可以使用动态规划求解:

  • dp[word] 表示 wordword 结尾的单词链的最长长度,lastWord[word] 表示以 word 结尾的最长链的前驱结点。
  • 初始化 dp[word]word 的点权,lastWord[word] 为空(即没有前驱)。
  • 转移时,采用在拓扑排序的基础上进行状态转移的方式,假设 AB 的前驱,那么 dp[B] = max(dp[B], dp[A] + weight[B]),其中 weight[B]B 的点权。假如有 dp[A] + weight[B] > dp[B],那么还需要更新前驱,即 lastWord[B] = A
  • 最终统计答案时,需要看所有点的 dp 值,把最大的那个给记录下来,这就是最长链的长度。由于要求解出一条具体的链,所以还需要通过这条最长链的尾结点,根据 lastWord 去找到所有的前驱,重构出整条链。
  • 上面的算法过程是假设了没有设置头结点的开头字母。倘若要求指定开头字母的最长链,那么需要先进行重构图操作:做一遍拓扑排序,每次把入度为 0 且开头字母不是指定字母的单词加入队列中,等到从队列中弹出时则将其彻底删除掉。这样做完之后,使用剩下的单词重构图,可以保证拓扑序并列第一大的结点都是以给定字母开头,这样可以在重构的图上执行上面几步的操作,即可求出结果。
  • 时间复杂度为 O ( n 2 + m ) O(n^2 + m) O(n2+m) n n n 为重构前图的点数, m m m 为重构后图的边数。
  • 快速算法的流程图如图所示:fastGetMaxWordCountChain

而如果图是一个有环图,那么则使用暴力求解:

  • 选择一个以给定字母开头的单词,调用 getOneMaxWordCountChain 方法,找到以该单词开头的最长链。在搜索的每一步判断结尾是否符合字母要求,如果符合要求就看能否更新当前的最长链以及长度。在实现 getOneMaxWordCountChain的时候为了加速计算,需要传递一些冗余的参数,比如当前最长链列表。
  • 暴力算法的流程图如图所示:trivialGetMaxWordCountChain

gen_chain_chargen_chain_word 没有本质区别,二者调用的快速算法是同一个方法,暴力算法仅在更新长度时略有不同,故不再赘述。

gen_chain_all 的求解算法是纯暴力搜索:

  • 枚举所有单词作为起点,然后以这个点开始 dfs,求出来以这个点为开头的所有链。

  • 由于最终结果不允许超过 20000 字符,所以可以实时统计答案的字符数,超过 20000 就只搜索统计链的个数而不保存具体链的结果,防止内存消耗过大导致程序崩溃。要想在每个链上避免一些重复计算,则需要参数传递时好好考虑,比如传递当前链的时候应该传递最终的字符串而不是单词列表。

  • 暴力算法过程大同小异,所以不再重复给出流程图。

gen_chain_word_unique 的求解我们组没有按照是否为有向无环图进行分类,因为我们在讨论的时候不确定线性算法的正确性,且在和其他组互测的过程中发现其他组的线性算法似乎存在一定的问题,所以最后统一使用了暴力搜索:

  • 枚举所有单词作为起点,然后以这个点开始 dfs,需要维护当前的首字母集合,保证在转移的时候不能转移到重复的首字母的单词。
  • 由于要随时更新已经求得的最长链,所以要把已经求得的最长链作为参数传递下去。
  • 暴力算法大同小异,所以不再重复给出流程图。

CalcuCore 中方法之间的调用关系示意图如图所示:

function_relation

其中,A -> B 意味着方法 A 调用方法 B

在性能方面:

  • 我们最开始的打算便是所有的求解都复用核心计算函数 getAllWordChain,使用该方法找到所有单词链,最后把合法链挑出来,统计想要计算的结果即可。这样实现的后果是随手复制英语文章中的两句话都未必能在程序崩溃之前跑出结果
  • 在测好了暴力求解之后,我们发现了有向无环图具有优良的性质,可以按照拓扑序从前往后递推计算很多信息,所以我们针对 -w-c 进行了建图后拓扑排序 + 动态规划求解不带环的部分。但对于指定 -h 参数,最开始我们没有想到很好的方案。
  • 后来,我们发现了 -h 的部分可以额外用一遍拓扑排序删去没用但是拓扑序比较靠前的单词,这样得到的新的图就又可以动态规划求解了,所以我们编写了重新建图方法来扩大上面想到的算法的适用范围。至此,-w-c 参数搭配 -h-t 参数,且数据无环时都可以快速求解。
  • 最后,我们对每种问题的暴力方法根据题目的不同去单独编写:对于 -w-c-m,其暴力算法无需保所有的链,所以只需要维护当前链以及已经找到的最长链即可;对于 -n,由于题目要求求出所有的链,但是又要求输出长度不能超过 20000,所以我们在保存链的同时维护了当前结果的长度,如果超过 20000 字符则只统计链的个数,而不保存链,这样就能较好地避免因为链太多导致内存空间消耗过多。
  • 最后的最后,说下没有优化的 -m。我们注意到有别的组的同学使用动态规划了来解决,但是和他们对拍时发现了他们似乎存在 bug ,且我们组内两人也不确定这是不是假算法,于是就没优化这个部分,所以我们 -m 的求解性能可能相比其他同学要差一些。

5.阅读有关 UML 的内容,画出 UML 图显示计算模块部分各个实体之间的关系(画一个图即可)

之前第四部分已经展示过一个UML图大致表现从顶层模块到底层模块的调用关系,这里再仔细展示一下 Core 模块内部的4个类的调用关系,由于本次项目需求并不复杂,因此只使用了一个 CalcuCore 类进行主要的计算逻辑书写,而在计算过程中遇到的异常则进行抛出。内部有几个公共方法供互测接口调用。

UML

Part2 编码与测试

6.计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2019的性能分析工具自动生成),并展示你程序中消耗最大的函数。

整个性能调优和回归测试上总计花了约 5 小时,调优前:

before_optimize

优化后:

after_optimize

可见优化后只有过于暴力(实际上也只能暴力)的 gen_chain_all 占用 CPU 仍然很高。

优化后几百个随机单词的有向无环图可以秒出 -w-c,几十个随机单词的 -r 在对拍时也可以在三五秒之内算出结果。

7.看 Design by Contract,Code Contract 的内容,并描述这些做法的优缺点,说明你是如何把它们融入结对作业中的。

Design by Contract,即契约式设计,在此方面的杰出代表是Eiffel语言,Java 中的 JML 也是契约式设计的具体实例。其中用到的涉及到规则约束和编译检查相关的部分被称为Code Contract

简单来说就是每个类或者实体方法都需要满足某种前置条件,后置条件和不变式以及其他的状态约束条件,这对于代码的状态有了一个很好的描述。

本次作业虽然我们没有明确的用这种思想编程,即严格规定每个方法和类的三种状态约束条件,但是在设计 CalcuCore 类时还是确立了在实例化类之前并没有图的建立,实例化之后建立了图结构,而之后在各个函数内部默认前置条件是已经建立了图,不变式为处理的过程不能破坏已有结构图,且始终需要保证是否符合数据约束,后置条件为返回值需要确保是正确结果。Library 中也有类似的函数设计

8.计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 90% 以上,否则单元测试部分视作无效。

本次项目我们用两种方式进行测试,一种是 visual studio 内部进行单元测试,另一种是使用 WordList.exe 接口在本地进行大量数据对拍测试。

单元测试

本次项目我们建立了四个单元测试类分别对四个主要模块进行测试

总的来说,单元测试采用局部和整体两部分测试逻辑。

局部测试

局部的测试主要针对于特定的类的方法进行测试,对于四个接口函数进行单独的样例测试,示例代码如下。数据主要是制造量少但是可以直接看出正确结果的典型数据。力求对输入输出和计算类,异常类中各个函数都有基本的正确性测试检验

public void CoreTest5()
{
    List<string> words = new List<string>() { "gbps", "generate", "google", 
                                             "growing", "handle", "handling", "hardware", "has"};
    List<string> res = new List<string>();
    PairTestInterface.gen_chains_all(words, res);
    Assert.AreEqual(res.Count, 10);

    res = new List<string>();
    PairTestInterface.gen_chain_word(words, res, 'g', 'e', true);
    Assert.AreEqual(res.Count, 2);

    res = new List<string>();
    PairTestInterface.gen_chain_word_unique(words, res);
    Assert.AreEqual(res.Count, 2);

    res = new List<string>();
    PairTestInterface.gen_chain_char(words, res, 'i', 'e', true);
    Assert.AreEqual(res.Count, 0);

    res = new List<string>();
    PairTestInterface.gen_chain_char(words, res, 'h', 't', false);
    Assert.AreEqual(res.Count, 0);
}

public void InputTest1()
{
    string[] args = { "-w" ,"-t","a"};
    CommandParser commandParser = new CommandParser(args);
    ParseRes parseRes = commandParser.getParseRes();
    Assert.AreEqual(parseRes.mode,1);
    Assert.AreEqual(parseRes.end, 'a');
}
整体测试

整体测试主要是强调将整个预处理到计算的流程完整性,采用大量数据进行测试。由于这样的情况下并无法手动确定出正确结果,因此采用了对拍这一思路,使用和我们组在第四阶段交换的小组的 Core.dll 引入进行测试。具体在测试时调用了如下方法 TestOneSample 方法。

public void TestOneSample(String[] args)
{
    List<string> res;
    List<string> res1;
    int a = 0;
    try 
    { 
        res = CmdTestInterface.testOneSample(args);
    }
    catch(Exception e)
    {
        Console.WriteLine(e.Message);
        a = 1;
        try
        {
            res1 = TestOneSampleByOther(args);
        }
        catch (Exception e1)
        {
            a = 2;
            Console.WriteLine(e1.Message);
        }
        Assert.AreEqual(2, a);
        return;
    }
    try
    {
        res1 = TestOneSampleByOther(args);
        Assert.AreEqual(res.Count, res1.Count);
    }
    catch (Exception e)
    {
        a = 1;
        printArgs(args);
        Assert.AreEqual(2, a);
    }
}

其中 CmdTestInterface.testOneSample 方法是我们定义的一个顶层接口,输入参数列表,返回字符串列表形式的结果。TestOneSampleByOther 则是调用交换的小组的 core 进行计算逻辑,而其他的输入输出使用我们自己的代码进行处理。这两个函数都运行之后,将得到的两个结果进行比较即可判定整体的正确性。即类似计算机网络中的 ping 通,一旦可以跑完就说明整个流程都是正确的。

数据生成上主要包含回归测试和指令组合覆盖性测试。对于每一个文件都使用四层循环遍历所有可能的两两指令组合以及首尾字母组合,覆盖性测试代码如下。

public void CoreTest2()
{
    int testNum = 5;
    String baseFile = "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile{0}.txt";
    String testFile = String.Format(baseFile,testNum);
    for (int j = 0; j < 26; j++)
    {
        for(int k = 0; k < 26; k++)
        {
            String head = ('a' + j).ToString();
            String tail = ('a' + k).ToString();
            foreach (char c1 in validCmdChars)
            {
                ArrayList argList = new ArrayList() { "-" + c1 };
                if (c1 == 't') argList.Add(tail);
                if (c1 == 'h') argList.Add(head);
                argList.Add("");
                for (int i = 1; i <= testNum; i++)
                {
                    testFile = String.Format(baseFile, i);
                    argList[argList.Count - 1] = testFile;
                    TestOneSample(getArgs(argList));
                }
                foreach (char c2 in validCmdChars)
                {
                    argList = new ArrayList() { "-" + c1 };
                    if (c1 == 't') argList.Add(tail);
                    if (c1 == 'h') argList.Add(head);
                    argList.Add("-" + c2);
                    if (c2 == 't') argList.Add(tail);
                    if (c2 == 'h') argList.Add(head);
                    argList.Add("");
                    for (int i = 1; i <= testNum; i++)
                    {
                        testFile = String.Format(baseFile, i);
                        argList[argList.Count - 1] = testFile;
                        TestOneSample(getArgs(argList));
                    }
                }
            }
        }
    }
    Console.WriteLine(correctNum);
}
单元测试结果展示

可以看到每个模块的单元测试覆盖率都达到了90%以上,总体覆盖率为96%,这个过程也确实发现了自己和交换小组的很多bug,效果还是较好的。

本地CLI对拍

本地 CLI 对拍主要是使用我们的 me.exe和小组的 other.exe 对正确性部分进行随机数据测试。这部分并不属于单元测试,但因为我们做了,所以这里也介绍一下。

对拍主要编写了 test.cppcreate.cpp 两个文件。

test.cpp 负责驱动对拍以及检查答案:

  • 编译一遍数据生成脚本 create.cpp
  • 执行 create.exe,并把输出的数据重定向到 in.txt
  • 随机从参数池中生成一组合法命令行参数
    • 首先是从 -n-m-w-c 中随机选择一个作为主参数
    • 然后根据参数的自身限制,随机生成其他可以组合的辅参数
  • 执行 me.exeother.exe,将输出的内容写到各自的输出文件中。
  • 比较结果的一致性,我使用的判定方法都比较简单:
    • 对于 -n,只看第一行的个数是否一样。
    • 对于 -m-w,只看输出的单词行数以及首尾字母。
    • 对于 -c,看首尾字母,并计算字符个数,比对是否一致。

create.cpp 的负责数据生成,由于时间紧张,所以只生成了两类特殊的随机数据:

  • 纯随机单词数据的生成策略:每次随机从可见字符里面输出一串字符。根据被测程序的性能,可以控制输出字符串的长度。
  • 有向无环图生成策略:考虑按顺序枚举首字母从 az,假设当前枚举到首字母为 ch,则可以生成随机个数的以 ch 为开头的单词,且这些单词的尾字母必须字典序比 ch,这样保证生成出来的数据不能往之前生成的单词连有向边,所以生成的数据就是有向无环图。

对拍时,我们组发现的自己的 bug 有:

  • 最长单词链算法中单个单词也误当作链了。
  • -n 时没有输出到 stdout
  • 性能调优过程中出现的算法错误。

我们组发现的别的组的 bug 有:

  • 最长单词链算法中单个单词也误当作链了。
  • -m 算法错误。
  • -c 最长链算出来不是最长的。

OO 互测既视感

9.计算模块部分异常处理说明。在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。

本次项目主要确定了5种异常并进行测试。描述和测试样例如下:

错误的参数 CommandInvalidException

描述
  • 没有指定主参数,即 n/w/m/c 至少出现一个
  • 文件参数没有放在最后的位置
  • 出现非法字符(出现了- n这样的横线没有和字母连起来的情况或者其他非法字符)
测试
public void CommandInvalidExceptionTest()
{
    string[] args = { "-n", "t", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile1.txt" };
    int ans = 0;
    try
    {
        CmdTestInterface.testOneSample(args);
    }
    catch (CommandInvalidException e)
    {
        ans = 1;
        Console.WriteLine(e.Message);
    }
    Assert.AreEqual(1, ans);

    args = new string[] { "-n", "-!", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile2.txt" };
    ans = 0;
    try
    {
        CmdTestInterface.testOneSample(args);
    }
    catch (CommandInvalidException e)
    {
        ans = 1;
        Console.WriteLine(e.Message);
    }
    Assert.AreEqual(1, ans);

    args = new string[] { "-r", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile2.txt" };
    ans = 0;
    try
    {
        CmdTestInterface.testOneSample(args);
    }
    catch (CommandInvalidException e)
    {
        ans = 1;
        Console.WriteLine(e.Message);
    }
    Assert.AreEqual(1, ans);
}

错误的参数组合 CommandComplexException

描述

两两参数冲突如下

-n-w-m-c-h-t-r
-n×××××××
-w××××
-m×××××××
-c××××
-h×××
-t×××
-r×××

上表中打 × 的位置说明两者是冲突的

测试
public void CommandComplexExceptionTest()
{
    string[] args = { "-n","-n", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile1.txt" };
    int ans = 0;
    try
    {
        CmdTestInterface.testOneSample(args);
    }
    catch (CommandComplexException e)
    {
        ans = 1;
        Console.WriteLine(e.Message);
    }
    Assert.AreEqual(1, ans);

    args = new string[] { "-n", "-m", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile2.txt" };
    ans = 0;
    try
    {
        CmdTestInterface.testOneSample(args);
    }
    catch (CommandComplexException e)
    {
        ans = 1;
        Console.WriteLine(e.Message);
    }
    Assert.AreEqual(1, ans);
}

错误的文件格式 FileInvalidException

描述
  • 没有以 .txt 结尾
  • 文件不存在
测试
public void FileInvalidExceptionTest()
{
    string[] args = { "-n", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain" };
    int ans = 0;
    try
    {
        CmdTestInterface.testOneSample(args);
    }
    catch(FileInvalidException e)
    {
        ans = 1;
        Console.WriteLine(e.Message);
    }
    Assert.AreEqual(1, ans);

    args = new string[] { "-n", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/1.txt" };
    ans = 0;
    try
    {
        CmdTestInterface.testOneSample(args);
    }
    catch (FileInvalidException e)
    {
        ans = 1;
        Console.WriteLine(e.Message);
    }
    Assert.AreEqual(1, ans);
}

结果过长 ResultTooLongException

描述
  • 计算得到的结果列表过长
测试

样例数据:aa ab bb bc cc cd dd de ee ef ff fg gg gh hh hi ii ij jj jk kk kl ll lm mm mn nn no oo op pp pq qq

public void ResultTooLongExceptionTest()
{
    string[] args = { "-n", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile7.txt" };
    int ans = 0;
    try
    {
        CmdTestInterface.testOneSample(args);
    }
    catch (ResultTooLongException e)
    {
        ans = 1;
        Console.WriteLine(e.Message);
    }
    Assert.AreEqual(ans, 1);
    ans = 0;
    args = new string[] { "-n", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile7.txt" };
    try
    {
        CmdTestInterface.testOneSample(args);
    }
    catch (ResultTooLongException e)
    {
        ans = 1;
        Console.WriteLine(e.Message);
    }
    Assert.AreEqual(ans, 1);
}

出现单词环 HasImplicitLoopException

描述
  • 输入的数据中含有隐含环
  • 比如:ab ba bc 这样的文本
测试

样例数据:abc ca

public void HasImplicitLoopExceptionTest()
{
    string[] args = { "-n", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile6.txt" };
    int ans = 0;
    try
    {
        CmdTestInterface.testOneSample(args);
    }
    catch (HasImplicitLoopException e)
    {
        ans = 1;
        Console.WriteLine(e.Message);
    }
    Assert.AreEqual(ans, 1);
    ans = 0;
    args = new string[] { "-w", "C:/Users/fzc/source/repos/Longest-English-word-chain/Longest-English-word-chain/TestFile6.txt" };
    try
    {
        CmdTestInterface.testOneSample(args);
    }
    catch (HasImplicitLoopException e)
    {
        ans = 1;
        Console.WriteLine(e.Message);
    }
    Assert.AreEqual(ans, 1);
}

10.界面模块的详细设计过程。在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。

首先是原型设计,由于时间有限,只是在脑海中大致想了一下布局,即左半部分是输入部分,右半部分是输出部分。两边都是有一个文本框和若干操作按钮组成。

之后是技术调研,最开始选择 c# 就是为了体验一下 gui 设计,于是主要学习了 WinForm 的使用

之后就开始正式开发了,先新建了一个 C# windows 应用程序项目,之后利用工具箱中的组件拖动进主界面进行组件化开发。

主要使用了 Button/TextBox/Label/ToolTip/MessageBox/ShowDialog 这6个组件,拖动到设计界面后,调整位置和文字参数,之后在相应的 .cs 文件的绑定的控制函数中进行逻辑编写。这里以一个导入文件的按钮举例。当按下按钮时,就会触发这个事件函数,就会先弹出一个文件选择框,选择之后就会将文本导入到输入框内。其他的按钮的逻辑也类似,都是通过相应的按钮事件函数进行绑定。给每个按钮和文本都写好相应的事件后,就差不多写好了GUI界面的基本逻辑。

private void button1_Click(object sender, EventArgs e)
{
    if (openFileDialog1.ShowDialog(this) == DialogResult.OK)
    {
        //保存路径
        string filePath = Path.GetFullPath(openFileDialog1.FileName);
        Console.WriteLine(filePath);
        //读取数据
        StreamReader str = new StreamReader(filePath);
        //获取每行字符
        string line;
        string inputStr = "";
        while ((line = str.ReadLine()) != null)
        {
            //通过','将行分裂为字符串组
            inputStr += line+"\r\n";
        }
        str.Close();
        textBox1.Text = inputStr;
    }
}

整体逻辑为,导入文件或者直接输入到文本框内,设置相应的命令,之后对输入文本框内的字符串进行处理和分析,得到输出结果,输出结果实时打印到右侧的输出框。之后可以采用将结果导出到文件等操作。异常处理和完整处理计算流程主要放在导出分析结果文件这个按钮的事件函数中,按下之后就会触发整个的流程得到计算结果。

具体设计时的界面如下图所示

11.界面模块与计算模块的对接。详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能。

对接计算模块

先在 visual studio 2019 中点击 项目-添加项目引用-浏览-导入Library.dll Core.dll

主要在 开始分析 按钮处进行预处理和计算流程运行,大致代码架构如下,可以很轻松的修改使用的 Core

主要在核心计算逻辑部分使用 Core 内定义的四个接口进行计算,其余部分都使用 Library.dll 中的类进行操作

private void button4_Click(object sender, EventArgs e)
{
    //CoreType:0 使用我们的Core
    //CoreType:1 使用交换的Core
    int CoreType = 0;
    if (CoreType == 0)
    {
        try
        {
            //预处理
            string[] args = System.Text.RegularExpressions.Regex.Split(textBox3.Text, @"\s+");
            CommandParser parser = new CommandParser(args);
            ParseRes parseRes = parser.getParseRes();
            WordListMaker maker = new WordListMaker();
            string article = textBox1.Text;
            List<string> wordList = maker.makeWordList(article);
            List<string> result = new List<string>();
            //核心计算逻辑
            int outputMode = 1;
            if (parseRes.cmdChars.Contains('n'))
            {
                outputMode = 0;
                PairTestInterface.gen_chains_all(wordList, result);
            }
            else if (parseRes.cmdChars.Contains('w'))
            {
                PairTestInterface.gen_chain_word(wordList, result, 
                                                 parseRes.start, parseRes.end, parseRes.enableLoop);
            }
            else if (parseRes.cmdChars.Contains('m'))
            {
                PairTestInterface.gen_chain_word_unique(wordList, result);
            }
            else
            {
                PairTestInterface.gen_chain_char(wordList, result, 
                                                 parseRes.start, parseRes.end, parseRes.enableLoop);
            }
            //输出逻辑
            Output output = new Output();
            string outputRes = output.printWordChains(result, outputMode);
            outputRes = outputRes.Replace("\n", "\r\n");
            textBox2.Text = outputRes;
        }
        catch (Exception error)
        {
            MessageBox.Show(error.Message);
        }
    } else
    {
        try
        {
            //预处理
            string[] args = System.Text.RegularExpressions.Regex.Split(textBox3.Text, @"\s+");
            CommandParser parser = new CommandParser(args);
            ParseRes parseRes = parser.getParseRes();
            WordListMaker maker = new WordListMaker();
            string article = textBox1.Text;
            List<string> wordList = maker.makeWordList(article);
            List<string> result = new List<string>();
            //核心计算逻辑
            if (parseRes.cmdChars.Contains('n'))
            {
                Chain.gen_chains_all(wordList, 0, result);
            }
            else if (parseRes.cmdChars.Contains('w'))
            {
                Chain.gen_chain_word(wordList, 0, result, 
                                     parseRes.start, parseRes.end, parseRes.enableLoop);
            }
            else if (parseRes.cmdChars.Contains('m'))
            {
                Chain.gen_chain_word_unique(wordList, 0, result);
            }
            else
            {
                Chain.gen_chain_char(wordList, 0, result, 
                                     parseRes.start, parseRes.end, parseRes.enableLoop);
            }
            //输出逻辑
            Output output = new Output();
            string outputRes = output.printWordChains(result, 1);
            outputRes = outputRes.Replace("\n", "\r\n");
            textBox2.Text = outputRes;
        }
        catch (Exception error)
        {
            MessageBox.Show(error.Message);
        }
    }
}

功能介绍

导入文件
输入命令进行分析
选择命令参数按钮输入命令
必要的输入提示

输入 -h/-t 后会提示再输入一个字母

清空参数

分析结果并给出必要异常提示
正常的输出
遇到异常
导出分析结果到文件

附加任务-互换核心模块

信息

与我们互换的组为:

  • 杨濡冰 19373263
  • 黄炜 19373174

交换过程总结

命令行和GUI,以及测试程序都对杨濡冰和黄炜组的 Core 进行了使用。

基本的使用方式是先添加项目引用,导入他们提供的 HYCore.dll,之后调用他们提供的接口 Chain.接口函数 进行计算即可,我们将之前使用 PairTestInterface.接口函数 的部分替换成他们的函数即可。值得注意的是版本比较重要,.net 官方文档中明确提出了只保证大版本号的兼容,即 4.x 是可以互相兼容的,但是 3.x4.x 的支持就很不顺畅,所以我们双方都单独生成了对方能够使用的版本。

我们小组单独生成了 .net 4.7.2 版本的 Core.dll,具体来说就是新建一个 .net framework 用于创建c#类库 的项目,之后添加现有项。把 Core 的相关文件导入,并直接运行一下就会在 bin 路径下找到 dll 文件。

正常导入之后,我们发现了他们的一些小bug,下图中发现他们有环数据遇到了下标越界的错误

同时在单元测试还发现了在遇到找不到某个结尾的链时会抛异常,这些错误我们都及时反馈并帮助他们完善了设计。

无独有偶,他们也发现了我们的类似bug

在完成bug修复之后,我们都使用对方的 Core.dll 得到了正确的运行结果

正确运行效果

Part3 总结

12.描述结对的过程,提供两人在讨论的结对图像资料(比如 Live Share 的截图)。关于如何远程进行结对参见作业最后的注意事项。

3.22 宿舍 初步讨论

进行第一次大致读题和进度安排,用时10min,主要是大概沟通一下和理解基本要求。同时明确了当天晚上需要好好学习 c# 相关的知识以及自己动手写一些demo,做好开发前必要准备工作。

3.23 主M楼 计划与初步开发

深入读题,明确了代码规范和开发计划,完成了设计文档,同时进行了第一次结对实践,完成了输入类和一个简陋的dfs算法。

计划好之后两天自己独自开发的内容

3.25 新主楼 核心逻辑开发1

3_25

zcx重构了算法,使用拓扑排序等方法进行优化,完成了一个输出函数。

fzc写好了GUI以及实现了参数解析,定义了输入输出的一些异常

3.27 新主楼 核心逻辑开发2

(此次忘记拍照了)

zcx继续完善算法逻辑

fzc写完善输入输出,设计单元测试

3.29 主M楼 核心逻辑开发3

3_29

修复了解析参数的一些bug,讨论接口实现

zcx发现了之前算法的一些问题,进行重新构建

4.2 主M楼 GUI、CLI与计算模块对接,完善异常设计

4_2

基本完成了所有模块的编写,进行测试,以及命令行主程序,GUI界面和计算模块的对接

fzc进行整体文件架构的调整,zcx继续完成接口

4.3-4.5 测试各个接口,模块松耦合

这两天由于各种原因,线上一起进行。

和交换的小组进行互换模块,交流并互测。

编写文档,测试以及做最后的签入

完结撒花合影

4_2

感谢战哥的包容和忍耐以及算法的carry!这两周的合作简直丝滑至极😀

13.看教科书和其它参考书,网站中关于结对编程的章节,例如:http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html ,说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)

结对编程评价

优点
  • 代码质量高,写出来的代码往往很规范而且不容易有bug,因为有问题往往也第一时间解决了。比如本次项目我们的算法设计部分就在一开始就解决了很多问题
  • 对于需求理解更到位,对一些边界问题会有更清晰的把握
  • 专注度更高,效率高,出于不浪费他人时间的考虑,且有人监督,一般在一起写的时候没人会开小差做别的事。
  • 适合于一些中间的阶段或者一些需要对需求有一个全面清晰认知的模块编写,比如本次项目的参数解析模块。
缺点
  • 对于一些简单的模块和函数,两个人一起写的效率可能物极必反,因为这部分代码本身没有什么需要一起复审的,这个时候领航员并行写一些简单的逻辑会更好
  • 出于怕麻烦的心理,在被看写代码的时候压力较大,会使用更保险,更安全的写法,有时候会带来代码冗余和代码效率较低,无法写出特别高质量的代码。

个人评价

优点
  • 对于新技术有热情,学习能力较强
  • 善于合作,沟通能力强
  • 测试能力较强
缺点
  • 代码风格需要IDE的自动调整,有时候自己写风格上会出问题
  • 算法能力较弱

队友评价

优点
  • 对于算法设计与优化具有很高热情,算法设计与实现能力强(这次结对尝试实现过很多图算法,以及进行了较多的优化)
  • 代码设计风格相当规范甚至可以说是精美
  • 学习能力强,善于查阅资料文档
  • 善于合作,接口设计对接与交流很顺畅
缺点
  • 也许是别的事太多了,有时候懒得配环境

14.在你实现完程序之后,在附录提供的PSP表格记录下你在程序的各个模块上实际花费的时间。

PSP2.1Personal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划9090
· Estimate· 估计这个任务需要多少时间9090
Development开发790915
· Analysis· 需求分析 (包括学习新技术)6030
· Design Spec· 生成设计文档3030
· Design Review· 设计复审 (和同事审核设计文档)3010
· Coding Standard· 代码规范 (为目前的开发制定合适的规范)105
· Design· 具体设计6060
· Coding· 具体编码360480
· Code Review· 代码复审60120
· Test· 测试(自我测试,修改代码,提交修改)180180
Reporting报告100100
· Test Report· 测试报告6060
· Size Measurement· 计算工作量1010
· Postmortem & Process Improvement Plan· 事后总结, 并提出过程改进计划3030
合计9801105

事后总结

本次结对编程是一次非常令人难忘的经历,收获了以下内容:

  • 从几乎陌生到大概了解 c# 面向对象设计,c# WinForm GUI 开发
  • 掌握了基本的 visual studio 使用方法,包括单元测试,代码性能分析,打包方法,编译运行等等
  • 基本了解了结对编程的流程,大致掌握了一种高效的两人项目开发方法
  • 复习了OO课上的JML和UML部分的一些内容,同时对于接口设计,高内聚低耦合等设计原则有了更深的理解
  • 同时很重要的是更加了解战哥这位巨佬,从他身上也学到了不少东西,包括设计思想,代码规范等等

同时对于结对编程的流程也有了一些改进的想法,之后结对编程和团队开发中可以吸取教训和经验

  • 项目的松耦合从一开始就要大致规划好,除了代码层面的松耦合,还有项目之间的松耦合,比如需要分出几个子项目,以及同一个项目之内需要分成几个模块。以及版本也需要从一开始进行统一
  • 编码时就要考虑 Information Hiding,Interface Design,Loose Coupling 这些原则,保证接口,实体类,工具类,方法的可扩展性。同时一定程度上可以对于算法设计,以及有明确输入输出的函数进行契约化设计实践
  • 结对编程不能一味死板的执行领航+驾驶这样的原则,相当的一段时间还是需要独立思考开发的。但是对于边界较混乱和比较复杂的部分需要合作和讨论完成,确保不会出错
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值