【北航软件工程】最长单词链

Part.1

github连接c++

Part.2

PSP2.1 Personal Software Process Stages 预计耗时(分钟) 实际耗时(分钟)
Planning 计划 15 10
PSP2.1 估计这个任务须要多少时间 15 10
Development 计划 1120 1360
· Analysis 需求分析 (包括学习新技术) 120 150
· Design Spec 生成设计文档 30 30
· Design Review 设计文档复审 10 10
· Coding Standard 代码规范 (为目前的开发制定合适的规范) 20 30
· Design 具体设计 40 60
· Coding 具体编码 500 480
· Code Review 代码复审 100 100
· Test 测试(自我测试,修改代码,提交修改 300 480
Reporting 报告 90 90
· Test Report · 测试报告 60 60
· Size Measurement · 计算工做量 10 10
· Postmortem & Process Improvement Plan · 计算工做量 20 20
合计 1225 1465

Part.3

Information Hiding的实践:计算核心代码为ChainSolver类,其只有一个对外public函数get_max_chain,内部计算细节都被封装成黑盒,完成了Information Hiding的要求。git

In computer science, information hiding is the principle of segregation of the design decisions in a computer program that are most likely to change, thus protecting other parts of the program from extensive modification if the design decision is changed. The protection involves providing a stable interface which protects the remainder of the program from the implementation (the details that are most likely to change).程序员

--Information Hiding定义github

Interface Design的实践:Core类做为一个纯C类型的模块调用核心ChainSolver,它做为核心计算类的对外接口体现了松散耦合的思想,好比能够调用交换核心模块。算法

Loose Coupling的实践:UIUtility是UI控制部分,咱们尽可能避免了该部分和UI形式上的关联,不使用其余类中的方法进行字符串处理等等。编程

In computing and systems design a loosely coupled system is one in which each of its components has, or makes use of, little or no knowledge of the definitions of other separate components. Subareas include the coupling of classes, interfaces, data, and services.[1] Loose coupling is the opposite of tight coupling.后端

--Loose Coupling定义数组

Part.4

a.算法概述

咱们认为此次结对编程的题目能够抽象为有向有环图求最长路径,目前咱们使用的算法仍是深度遍历穷举所用可能的路径,该图有26个点对应26个英文字母,每一个边表明输入的单词,例如输入"hello"能够化为从点'h'到'o'的边。数据结构

首先根据五项需求-w-r-h-t-c, 其中单独的-w是能够约化到-r的,而单独的-r是不能约化到-w,对于-h-t只是对路径首尾进行检测约束,对于-c只是将图的边加权。因此算法应该针对-r设计。深度遍历的过程将从一个点开始,例如'h',若是该点有指向点的边,则递归访问下一点,下一点的递归完成后才会访问'h'的其余边的指向。框架

b.算法数据结构

边和点的struct分别是:

struct Edge {  
    std::string word;  
    int code;  
    int weight;  
    int next;  
};

边Edge的word存储输入的原始字符串;code是该边的识别编码;weight是该边权重,weight默认为1,在-c时为字符串长度;next是字符串末尾字符ascii码-'a'的值,方便将点的编码都归为0-25。

struct WordMap {
    std::vector<Edge> toLast;
};

点WordMap只包含一个边的vector。

c.类和函数

核心计算包括两个类Core和ChainSolver,Core负责标准输入,全部外部类和函数只调用Core,Core再调用ChainSolver的get_max_chain函数。

ChainSolver类有三个函数公有函数get_max_chain、私有函数CreateMap、私有函数Recursion,get_max_chain调用CreateMap来建立图,再调用Recursion递归DFS建立过的图。

get_max_chain函数

接收如下参数:

char *input[], int num, char *result[], char head, char tail, bool isGetMaxChar, bool enable_loop

以上参数分别是输入的单词数组,输入的单词量,须要输出结果的result,head为-h的头部要求,tail为-t的尾部要求,isGetMaxChar是-c的边权重修改要求,enable_loop是-r的要求。

CreateMap函数

这个部分须要考虑重复输入的单词并将其抛弃,map<string,int>类型的inputWord存储的是已录入的单词。

if (inputWord.find(s) != inputWord.end()) {
        return 0;
    }
    inputWord.insert(std::pair<std::string, int>(s, code));

单词编码部分使用最普通的线性编码0-100,最开始想到过APHash编码可是对于做业里面的小规模的输入太过拖沓。

Recursion函数

使用的是递归法DFS,对传入节点进行路径穷举遍历,进入过的点和边都会被记载在isUsedPoint和isUsedEdge中,递归返回后再将记载记录释放。优化部分会在第六节描述。

Part.5

UML图,同时也展现了内部函数调用状况:

Part.6

咱们对DFS穷举法的主要优化是自环剪枝,很明显在求-r最长路径的过程当中自环是一定须要走的边,不须要再递归断定了,因此在Recursion进入节点的时候先将该节点全部未走过的自环边加入路径中。

//ensure the edge that wait to push is not used before.
    if (isUsedEdge[iter.code])
        continue;//'continue' will jump this edge.
    //push every self-circle edge.
    path.push_back(iter.word);
    if (iter.next == point) {
        len+=iter.weight;
        continue;
    }
    ...
        ...
        ...
    //pop every self-circle edge.
    for (auto iter : map[point].toLast) {
            if (iter.next == point) {
                isUsedEdge[iter.code] = false;
                path.pop_back();
            }
            else {
                break;
            }
    }

输入为55个彻底随机的单词的状况下,VS性能分析结果以下:

从图中能够看到ChainSolver::get_max_chain函数占用CPU资源总计约30%, 占整个后端main调用的四分之三,递归遍历毫无疑问是整个项目里面的性能瓶颈。

Part.8

Core模块的单元测试覆盖率:

对Core模块进行单元测试的过程是:构造测试样例=>传入进Core模块=>检查返回的单词链的长度与内容是否正确。

其中,咱们将后两步独立出来,封装进了testutil的两个函数中。

两个函数分别是:

void testRight(char* words[], int words_len, char* res[], int res_len, bool is_max_char, char head, char tail, bool enable_loop);
void testRightMulti(char* words[], int words_len, vector<char**> res, vector<int> res_len, bool is_max_char, char head, char tail, bool enable_loop);

两个函数的区别在于testRightMulti容许符合要求的最长单词链有多条,Core模块只要返回其中一条即为正解。

值得关注的是enable_loop参数。
当enable_loop为假时,会按索引序比较正解与Core返回的解;
当enable_loop为真时,只会进行无序比较。

做为展现的一个简单的测试样例:

TEST_METHOD(simple)
    {
        char* words[] = {
            "hello",
            "world",
            "ofk",
            "kw"
        };
        char* res[] = {
            "hello",
            "ofk",
            "kw",
            "world"
        };

        testRight(words, 4, res, 4);
    }

咱们构造测试样例的方式有如下四种:

a. 分析做业要求,找出边界条件,构造边界样例。

b. 根据算法的搜索方式的性质,构造针对性样例。

c. 使用简单算法自动生成大样例,构造压力样例。

d. 针对可能被抛出的异常,构造异常样例。

a. 边界样例

咱们首先细读做业文档,从中划出能够得出边界条件的语句,而后针对该语句构造边界样例。这一过程在demand_analyze.md的前半段被体现。摘取其中一段:

  • 单词链至少两个单词,不存在重复单词
    • 构造一个没有任何两个单词能连上的状况
    • 构造有多个重复单词的words
    • 构造一个首尾字母相同的单词

b. 针对性样例

在完成算法编写后,针对算法中可能存在的分支、迭代,构造针对性样例试图去覆盖,检验是否如预期那样执行。这一过程在demand_analyze.md的后半段被体现。摘取其中一段:

  • -c
    • 是惟一的单词数量最长也是字母数量最长
    • 是单词数量最长中的一个,可是是全部单词数量最长中的字母数量最长的
    • 不是单词数量最长的,可是是字母数量最长的。
    • 是字母数量最长中的一个。

c. 压力样例

咱们设计了一种简单的算法来自动生成大的测试样例。咱们称这种算法为PyramidGenerator。

算法的基本思路是构造一个最多只有26个非根节点的树,且该树具备最长深度的节点有且仅有一个。

如图构造树,每个节点表明一个字母,每条边表示以起点为首字母、以终点为尾字母的一族单词。由此,每条边上均可以生成无数单词。
而若是咱们控制root-t-e-o这条路径上的每条边都只生成一个单词,例如:tme-ego,那么该路径的单词连起来就是惟一最长单词链。

经过使用PyramidGenerator,咱们能够生成任意大小的无环测试样例。

d. 异常样例

咱们简单地针对Core执行中会抛出的异常构造对应的样例,当该样例被输入后,尝试捕获异常。

Part.9

计算部分一处异常处理,当没有启用-r功能却检查到有环图时将抛出异常w_c_h_t_ChainLoop,如下代码的断定条件是该边的终点已被走过,同时没有开启-r,同时不是自环。

if (isUsedPoint[iter.next] && !isEnableLoop && point != iter.next) {
        throw w_c_h_t_ChainLoop;
    }

另外也须要考虑对Core的单元测试异常抛出,其实如下异常在程序做为总体运行时不会抛出:
传入的尾部要求是否合理:

if (tail_input != 0 && (tail_input < 'a' || tail_input > 'z')) {
        throw para_tail_error;
    }

传入的头部要求是否合理:

if (head_input != 0 && (head_input <'a' || head_input > 'z')) {
        throw para_head_error;
    }

传入的enable_loop是否合法:

try {
        isEnableLoop = enable_loop;
    }
    catch(...){
        throw para_loop_error;
    }

传入的input数组里面是否是的确有num个值:

for (i = 0; i < num; i++) {
        try {
            CreateMap(input[i], isGetMaxChar);
        }
        catch (...) {
            throw para_input_error;
        }
    }

传入的res是否能接受结果:

try {
        for (auto iter : maxPath) {
            char *new_str = new char[iter.length() + 2];
            // std::cout << iter << " ";
            for (unsigned int j = 0; j < iter.length(); j++)
                new_str[j] = iter[j];
            new_str[iter.length()] = '\0';
            result[i] = new_str; // TODO release such memory
            i++;
        }
    }
    catch (...) {
        throw para_res_error;
    }

传入input输入字符串是否包括非法字符(在建立图时统一抛出异常):

int ChainSolver::CreateMap(char *c_s, bool isGetMaxChar) {
    try {
        ...
        ...
        ...
    }
    catch (...) {
        throw create_map_error;
    }
    return 0;

Part.10

界面分为三个组成部分:

a. UIUtility: UI共通的代码的集合

b. cmdUI: 基于命令行实现的UI

c. MFCUI: 基于MFC实现的GUI

a. UIUtility

由于不管是命令行实现仍是MFC实现,只要是UI界面就会有部分共通的逻辑须要处理。
因此咱们将这部分逻辑抽离出来,编译成UIUtility.dll。

实际被抽离出来的逻辑为对文本输入进行分词,获得英语单词数组的部分。咱们将其称为StreamReader,它接受一个输入流,输出对应的单词数组。
为了方便调用,咱们也提供了一个做为StreamReader的封装的FileReader,它接受一个文件名,并将该文件的输入流做为StreamReader的输入。

StreamReader的具体实现是从流中逐字符读取,自身维护一个临时单词与单词数组。

  • 当读到非英文字母时,检查临时单词的长度,若非空,则将临时单词加进单词数组中,而后清空临时单词,不然什么也不作。
  • 当读到英文字母时,简单地追加到临时单词的最后。

b. cmdUI

这部分实现了以命令行参数为用户输入的用户界面。

对命令行参数进行解析的工做被抽离出来在CoreSetting中被实现。CoreSetting会检验参数的合法性,并解析它的含义。
cmdUI的主程序就可使用CoreSetting解析出来的结果作进一步的操做。

以后主程序简单地调用Core模块,而后将结果输出至文件即结束程序。

c. MFCUI

这部分实现了基于MFC实现的GUI。

按照做业要求,GUI实现须要

  • 直接输入与文件输入两种输入模式
  • 选项选择的功能
  • 直接打印出结果并提供导出到指定位置的功能

咱们的设计中,整个界面分为4块:

  • 输入

    经过两个单选按钮来控制输入方式。对于直接输入就直接用一个文本框接收输入,对于文件输入就提供一个地址输入栏和文件打开按钮来让用户给出文件地址。
  • 选项

    选项分为三组。对于-w, -c这组,使用单选按钮保证用户只能够选取其中一个按钮。对于-h, -t,使用勾选框来控制是否添加,同时给出一个只接受一个英文字符的文本框来接受指定字母做为参数。对于-r,简单地使用一个勾选框来表示是否启用该参数。
  • 运行

    运行包括程序的执行与程序的关闭两部分。也就是Run和Cancel两个按钮。当用户点击Run时,程序将会读取输入和选项中的数据,并检查数据合法性,以后委托给Core执行,最后将返回的结果填充进输出部分。当Cancel被点击时,简单地结束程序的运行。
  • 输出

    输出分为两部分。第一部分是打印显示,当Run被点击,而且数据被顺利处理之后,返回的结果会直接打印到一个文字框中。第二部分是导出,相似于输入部分的地址选择,咱们也提供了接口供用户选择输出文件,并提供Export按钮来执行导出功能。

大部分代码都是简单显然的。值得一提的是选项中的-h, -t所使用的文本框的实现。咱们的作法是,每当文本框内容被改变,检查文本长度,若大于1,则截断前面的,只留下最后一个字符。而后检查该字符是否为英文字母(大小写无所谓,自动转化成小写字母),若非英文字母则忽略。

Part.11

UI模块的设计已在10中详细描述,此处不重复。

模块的对接方式

咱们将Core模块做为一个独立的dll抽离出了代码。咱们准备了一个导出类Core来做为Core模块总体对外的接口,UI模块能够经过简单地调用这个Core类的静态函数来调用Core模块。

在UI模块调用dll模块时,咱们采起的方案是隐式调用。为了支持这样的作法,咱们在UI模块的附加依赖项中加入了Core.lib,而且将Core.dll和UI模块的可执行文件放在了同一个文件夹中。

关于具体函数接口,咱们只是简单地按照做业的要求实现了Core的接口。接口函数内部会创建ChainSolver类来执行真正的计算过程。固然,这对UI模块而言是透明的。

实现的功能

cmdUI

对于cmdUI,它能解析命令行参数并输出结果至solution.txt中。

例如,命令行参数以下:

运行后的控制台显示:

同时,BIN目录下产生输出文件solution.txt:

而若是参数不正确,例以下图的非法参数-k:

则会提示错误,并终止程序:

MFCUI

对于MFCUI,它能提供一组控件来让用户输入,并提供一组控件来让用户获得输出。

例如,执行后用户界面以下:

用户能够经过这部分来选择输入模式,上面的是直接输入文本内容,下面的是选择文本文件做为输入:

能够经过这部分来控制选项:

能够经过这部分来查看输出并选择文件来导出:

例如,以下做为输入:

点击Run后就会获得以下输出:

点击Open选择导出文件:

再点击Export执行导出,若是成功就会有以下提示:

若是选项不正确,例以下图的明明勾选了-h,却没有给出对应的首字母:

就会给出报错提示:

Part.12

咱们两人的结对一直牢牢围绕邹欣老师《构建之法》一书的要求:

在结对编程模式下,一对程序员肩并肩、平等地、互补地进行开发工做。他们并排坐在一台电脑前,面对同一个显示器,使用同一个键盘、同一个鼠标一块儿工做。他们一块儿分析,一块儿设计,一块儿写测试用例,一块儿编码,一块儿作单元测试,一块儿作集成测试,一块儿写文档等。

咱们两人在这次结对编程以前就已熟识,此次结对编程的任务也必然是无缝衔接般地完成。在结对的过程当中咱们大部分地代码都是坐在一块儿完成的,一块儿分析一块儿设计。固然第一次尝试这样的编程方式也会带来不少疑惑,好比编程一方不免会出现一些没必要要的并且较为复杂的构想,好比在计算核心的部分是否须要将字符串进行哈希编码,此时须要另外一方加入思考博弈,这样的过程颇有趣不过也很花时间。

总的来讲,咱们以为结对编程是个高强度、注重思惟碰撞的编程方式,和传统那种“各自码各自的代码再push”的方式,结对编程的过程当中咱们感觉到了实时性的交流和code review,这是一种很是敏捷的软件开发方式。

Part.13

  • 结对编程的优势:
    • 更高效的代码时间,两人结对相互监督,开小差的状况会少不少。
    • 代码复审快捷方便,双方物理距离近,code review交流起来很敏捷。
    • 减小设计错误,两我的的思想会更泛化、普适化,会尽量考虑到更多的点。
  • 结对编程的缺点:
    • 有些我的错误其实我的解决起来更快,言语交流的速度比不上脑内思考速度。
    • 对应第一项更高效的工做时间,结对编程给程序开发者压力更大,每时每刻处在“监视”的状态下,连续工做对其精力的消耗也是巨大的。
  • 搭档的优势:
    • 代码能力优秀,整个项目的框架一天就理清了,有丰富的项目开发经验。
    • 学习能力强,VS的覆盖率测试问题也是他第一个在群里面提出来的,能很快地摸清盲区。
    • 超级nice的二次元宅,对我这个小菜鸡颇有耐心,核心计算模块的不少bug都是他考虑周全的测试测出来的QAQ
  • 搭档的缺点:
    • 那就长得帅却没有npy

交换模块

咱们和马振亚&马浩翔组交换了模块。

名字 学号
马振亚 16061109
马浩翔 16061097

咱们的界面模块、测试模块 + 他们的核心模块

由于咱们两组都将Core模块封装成了dll,因此交换过程很是简单便捷。他们将他们Core的dll, lib, h文件给咱们,而后咱们调整了一下编译选项、调用关系就能够方便地使用他们的代码。

衔接过程最大的不顺利是由于咱们仿照了做业示例中的那样将Core类的全部方法都声明为静态方法,而他们组选择的是将接口声明为Core类的成员函数。因此咱们不得不修改咱们的代码去符合他们组的接口要求。

另外一个不顺利是由于他们组是直接在Core类中实现了算法,因此他们的Core.h中包含了一些运行须要的库函数的头文件。而在咱们的机子上,他们所须要的stdc++.h头文件并不存在,因此咱们不得不删去这句,再包含vector和map来知足编译需求。

衔接完成后在测试过程当中也发现了不一样。主要是需求理解的不一样,对于他们的Core模块而言,空输入、空输出都是会抛出异常的、不合法的,但对于咱们的程序而言,这些都是合法的。

相关文章
相关标签/搜索