2023软工第三次作业-最长英语单词链

结对项目-最长英语单词链

项目内容
这个作业属于哪个课程2023北航软件工程
这个作业的要求在哪里结对项目-最长英语单词链
我在这个课程的目标是帮助我初步建立软件工程敏捷开发的整体流程和概念,初步认识软件工程
这个作业在哪个具体方面帮助我实现目标学习结对编程方法,并加以实践

1.项目信息

在文章开头给出教学班级和可克隆的 Github 项目地址

  • 教学班级:周四下午班
  • 项目地址:https://github.com/Wpy12346946/Word-Chain.git

2.PSP

在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的 各个模块的开发上耗费的时间。

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

3.接口设计原则

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

  • Information Hiding:

在我们的设计中,我们将计算单词链的功能封装为独立模块core.dll,该模块通过向外暴露了三个接口与接口的调用方(如命令行端CLI,图形化界面GUI)进行交互。

core.dll中所有公开的接口函数都在core.cpp中,这些接口函数可以被其他程序直接调用。模块中欧冠还有许多私有功能,包括图的构建和计算过程中涉及到的各种图算法。其中,图的设计与相关算法和边的设计分别用Graph和Edge类封装,不对外界暴露,只能在接口函数被调用时按照接口的逻辑固定调用执行。

  • Interface Design:

为了使接口具有良好的可读性、可理解性和可维护性,我们参考指导书实现了以下三个接口。

int gen_chain_word(char *words[], int len, char *result[], char head, char tail, char reject, bool enable_loop);

int gen_chain_char(char *words[], int len, char *result[], char head, char tail, char reject, bool enable_loop);

int gen_chains_all(char *words[], int len, char *result[]);

其中words数组及数组中每一个指针指向的内存由调用者申请,result数组由调用者申请内存,但数组每一项指向的内存由模块申请。这样做的好处是可以节省内存,不必事先声明result中每一个字符串的大小。

Loose Coupling:

该原则指的是,在设计软件系统时,应该尽量避免模块之间的紧密耦合。我们设计的接口函数只要传参满足对应的数据类型与范围(如head的值为字母或’\0’)即可正常运行。

4.接口设计和实现

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

CORE接口设计如下:

const int MAX_LENGTH = 20000;
const int WORD_CYCLE_EXCEPTION = 0x80000001;//有环异常
const int TOO_LONG_EXCEPTION = 0x80000002;//链过长异常

int gen_chain_word(char *words[], int len, char *result[], char head, char tail, char reject, bool enable_loop);

int gen_chain_char(char *words[], int len, char *result[], char head, char tail, char reject, bool enable_loop);

int gen_chains_all(char *words[], int len, char *result[]);
  • MAX_LENGTH:表示result最长单词链长度,如果超过则报错
  • TOO_LONG_EXCEPTION:若模块在处理过程中发现最长单词链长度超过MAX_LENGTH,则报错,直接返回其异常值
  • WORD_CYCLE_EXCEPTION:若模型在处理gen_chains_all或者enable_loop=false的过程中发现单词环,则返回此异常值

gen_chain_word/gen_chain_char/gen_chains_all的各个参数含义如下

  • 输入参数

    • words输入单词串,由全小写英文单词组成

    • len输入单词串长度

    • result空的单词链,用于存储返回单词链

    • head:头字母限制,表示单词链的首字母必须为head

    • tail:尾字母限制,表示单词链的尾字母必须为tail

    • reject:首字母限制,表示单词链中不允许出现首字母为reject的单词

  • 返回值

    • result:返回一个单词链
    • int返回值
      1. 未发生异常:gen_chains_all返回单词文本中单词链数量,gen_chain_word/gen_chain_char返回单词链中单词的个数
      2. 发生异常:返回异常码TOO_LONG_EXCEPTION或者WORD_CYCLE_EXCEPTION

CORE模块构成为Core.cpp/Edge.cpp/Graph.cpp

  • Core:接口的实现部分,通过调用Graph来进行对图的操作
  • Edge:边节点,用于保存边的信息
  • Graph:图结构,存储了图信息和实际计算函数

具体步骤如下:

  1. 调用Graph.MakeGraph
    1. 将边的信息存储到Edge
    2. 根据单词之间的关系,生成图
  2. 调用 Graph 中的图论算法,计算结果
    1. 三个接口的图论算法均为dfs
    2. 利用拓扑序检测单词环。若检测出单词环或者不允许单词环,则直接返回异常码WORD_CYCLE_EXCEPTION
    3. 未指定-h参数时,利用拓扑序快速筛选出单词链可能的起点。
  3. 在图论算法中不断检测最长链,若检测出单词链长度大于MAX_LENGTH,则直接返回TOO_LONG_EXCEPTION异常码
  4. 返回单词链的长度与单词链

5.无警告截图

展示在所在开发环境下编译器编译通过无警告的截图

构建core.dll过程的无警告截图

请添加图片描述

构建Wordlist.exe过程的无警告截图

请添加图片描述

构建test单元测试的无警告截图

请添加图片描述

6.UML图

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

请添加图片描述

7.性能改进

**计算模块接口部分的性能改进。**记录在改进计算模块性能上所花费的时间,并展示你程序中消耗最大的函数,陈述你的性能改进策略。

对于无隐含环的情况,将指向相同的边合并后只需要常数次的遍历(图的结点数被限制在了26)即可完成,因此基本不需要优化。而有隐含环时,该问题为NP问题。因此只能通过一些方法来降低存在部分特征的图的复杂度。我们采用了如下几种优化来改进性能:

  • 使用指针避免拷贝构造

由于C++的stl容器在添加元素时无法使用引用,只能使用拷贝构造。因此,为避免string与自定义的Edge在拷贝构造上的开销,我们在初始化时会创建所有本次调用中会用到的这些对象,后续使用时,容器中用指针保存这些对象的地址,省略了多余的拷贝。

  • 分离自环边和非自环边

根据贪心策略,当首次遍历到某个节点时要首先添加所有自环边,然后再dfs非自环边。因此将边细分为自环边集合和非自环边集合后,dfs时可以省略掉对自环边的遍历。

  • 强连通分量+拓扑排序+dp

使用tarjan算法计算图中的强连通分量,将结点划分到不同集合中。不同强连通分量可通过拓扑排序得到单词链的可能顺序。对于有-h-r的运行,可以剪枝提前剔除一定不在最终单词链中的强连通分量。强连通分量内的任意两个节点通过暴力dfs计算两点间的最长距离。最后通过拓扑序和dp计算单词链总长。

我们随机生成了单词数为100的测试样例对上述的优化前后效果进行对比。可以看到,在优化前后递归过程中产生的函数调用次数有很显著的减少。

优化前cpu运行时间图:

请添加图片描述

优化后cpu运行时间图:

请添加图片描述

8.Design by Contract,Code Contract

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

  • Design by Contract:

契约式编程在OO的时候就有所涉及,但还是学的是对于JML方法的形式化描述。简单来说就是构成的契约是一组前置条件、后置条件和不变式的集合,用来描述组件之间的协议。

而在本次作业之中,我们认为本次作业内容相对较为简单,接口的参数和其他的函数光看名字就可以知道是什么意思,因此很难利用契约式编程。因此我们只是利用了这种思想简单的对于错误处理和内存分配进行了前置条件、后置条件和不变式的讨论。例如,在读取文件后,会通过is_open()方法判断文件是否被成功打开,如果没有则进行异常提示并结束运行。

  • Code Contract:

微软为 .NET 提供的一个契约式编程插件。我们本次作业没有使用.NET,因此不需要考虑。

9.单元测试展示

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

我们计算模块单元测试的代码覆盖率和分支覆盖率如下图所示。

请添加图片描述

CORE模块单元测试(不涉及异常值):

我们手工构造了22个单元测试点:

TEST(test, TestMethod1) {
    for (int i = 1; i <= 22; i++) {
        coreTest(i);
    }
};

同时我们设计了五种针对CORE接口的逻辑测评:

void retCheck(int res, int truth)

  • 检查返回单词链个数是否相等

void chainCheck(char *results[], int len):此

  • 检查返回单词链个数是否越界
  • 检查返回单词链中相邻单词是否满足基本约束

void headCheck(char *results[], int len, char head)

  • 检查单词链是否为指定首字母

void tailCheck(char *results[], int len, char tail)

  • 检查单词链是否为指定尾字母

void rejectCheck(char *results[], int len, char rejectCheck)

  • 检查单词链是否出现首字母为rejectCheck的单词

void circleCheck(char *results[], int len)

  • 检查单词链中是否有单词环

coreTest具体方法如下:

void coreTest(int index) {
    std::string dataFile = "../testfile/core/data" + std::to_string(index) + ".txt";
    std::string paramFile = "../testfile/core/param" + std::to_string(index) + ".txt";
    std::vector<std::string> wordList;
    char **words = loadData(dataFile, wordList);
    Config config;
    loadConfig(paramFile, config);
    char *result[20000];
    int res;
    switch (config.api) {
        case 'n':
            res = gen_chains_all(words, wordList.size(), result);
            retCheck(res, config.ret);
            for (int i = 0; i < res; i++) {
                std::string tmp[20000];
                int cnt = 0;
                for (int j = 0; result[i][j] != '\0'; j++) {
                    if (result[i][j] == ' ') {
                        cnt++;
                    } else {
                        tmp[cnt] += result[i][j];
                    }
                }
                cnt++;
                char *tmpRes[20000];
                for (int j = 0; j < cnt; j++) {
                    tmpRes[j] = (char *) tmp[j].c_str();
                }
                if (cnt >= 0) {
                    chainCheck(tmpRes, cnt);
                    circleCheck(tmpRes, cnt);
                }
            }
            break;
        case 'w':
            res = gen_chain_word(words, wordList.size(), result, config.h, config.t, config.j, config.r);
            retCheck(res, config.ret);
            if (res >= 0) {
                chainCheck(result, res);
                headCheck(result, res, config.h);
                tailCheck(result, res, config.t);
                rejectCheck(result, res, config.j);
                if (!config.r) {
                    circleCheck(result, res);
                }
            }
            break;
        case 'c':
            res = gen_chain_char(words, wordList.size(), result, config.h, config.t, config.j, config.r);
            retCheck(res, config.ret);
            if (res >= 0) {
                chainCheck(result, res);
                headCheck(result, res, config.h);
                tailCheck(result, res, config.t);
                rejectCheck(result, res, config.j);
                if (!config.r) {
                    circleCheck(result, res);
                }
            }
            break;
    }
    delete[]words;
}

10.异常处理说明

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

core部分的测试代码沿用了上述**coreTest()**函数。我们在core中定义了两种异常:

const int WORD_CYCLE_EXCEPTION = 0x80000001;//有环异常
const int TOO_LONG_EXCEPTION = 0x80000002;//链过长异常

当没有指定-r参数,但输入的单词中存在隐含单词环时,触发有环异常WORD_CYCLE_EXCEPTION。测试样例如下:

Element
Heaven
Table
Teach
Talk

此时存在隐含环Element-Table。

对于链过长异常TOO_LONG_EXCEPTION,我们使用a*b、b*c、c*d、d*e、e*f构造样例(*保证每组有10组以上互不相同的单词),参数使用-n,此时返回的数量为100000以上,触发异常TOO_LONG_EXCEPTION

由于接口函数从设计上已经避免了大部分由参数导致的异常,因此其余可能的异常全部在命令行中进行检查。涉及的异常如下:

const int DUPLICATE_FILE_EXCEPTION = 0x80010000;//输入多个文件
const int UNDEFINED_PARAM_EXCEPTION = 0x80010001;//未定义参数
const int READ_FILE_EXCEPTION = 0x80010002;//读文件失败(文件不存在)
const int LACK_PARAM_EXCEPTION = 0x80010003;//缺少必要参数(-n -w -c)
const int CONFLICT_PARAM_EXCEPTION = 0x80010004;//参数冲突
const int PARAM_FORMAT_EXCEPTION = 0x80010005;//参数值格式错误(-h -t -j的参数值)
const int DUPLICATE_PARAM_EXCEPTION = 0x80010006;//参数重复出现
const int LACK_FILE_EXCEPTION = 0x80010007;//缺少输入文件
const int WRITE_FILE_EXCEPTION = 0x80010008;//写入文件失败

coreTest()类似,我们定义了cliTest()和对应的data*.txt、param*.txt来进行测试。测试文件包含了coreTest()所有的样例。针对命令行中的异常,还包含如下样例:

  • DUPLICATE_FILE_EXCEPTION

-w ../testfile/cli/data_cli_exception.txt ../testfile/cli/data_cli_exception.txt

  • UNDEFINED_PARAM_EXCEPTION

-w -ht ../testfile/cli/data_cli_exception.txt

  • READ_FILE_EXCEPTION

-n ../testfile/cli/data_cli_exception_not_exist.txt

  • LACK_PARAM_EXCEPTION

-h a ../testfile/cli/data_cli_exception.txt

  • CONFLICT_PARAM_EXCEPTION

-n -w ../testfile/cli/data_cli_exception.txt

  • PARAM_FORMAT_EXCEPTION

-w -h ? ../testfile/cli/data_cli_exception.txt

  • DUPLICATE_PARAM_EXCEPTION

-w -w ../testfile/cli/data_cli_exception.txt

  • LACK_FILE_EXCEPTION

-w -h a

11.界面模块说明

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

运行展示

我们的图形界面设计如下。根据功能不同,分为输入部分、输出部分、参数控制部分三个部分。

请添加图片描述

  • 输入部分

第一部分为输入部分。用户可直接输入要处理的单词,也可以通过文件资源管理器选择并读取文件中的单词,读取后的单词会显示在输入框中。

  • 输出部分

第二部分为输出部分。经过core.dll处理得到的正确结果会显示在输出框中,并可以以.txt格式导出到自定义路径下。

  • 参数控制部分

第三部分为参数控制部分。用户可以勾选并设置-h、-t、-j的参数值,也可以选择是否允许隐含单词环。完成参数设置后,点击下方三个按钮,分别计算所有单词链、最长单词数的单词链、最多字母数的单词链。

实现细节

图形化界面通过交互逻辑的约束,极大的简化了命令行中可能产生的异常。

  • 参数冲突

为避免使用者因为错误的搭配参数造成参数冲突,我们令上方的四个勾选框和“所有单词链”按钮彼此冲突,当有选项被勾选时,禁用该按钮。部分代码如下:

# 更新allButton(查询所有链)按钮状态
def paramClick(self, state, button, textEdit=None):
    if textEdit:
        if state == Qt.Checked:
            textEdit.setDisabled(False)
            textEdit.setText('a')
            textEdit.setFocus()
        else:
            textEdit.setDisabled(True)
            textEdit.setText('')

    if self.allowHeadButton.isChecked() \
            or self.allowTailButton.isChecked() \
            or self.allowRejectButton.isChecked() \
            or self.allowCircleButton.isChecked():
        self.allButton.setDisabled(True)
    else:
        self.allButton.setDisabled(False)

示例如下:

请添加图片描述

  • 参数格式错误反馈:

对于-h、-t、-j的参数值,我们约束了参数长度最大为1。当参数值为空或非字母字符时,由界面直接反馈,而不用交给core.dll判断。

请添加图片描述

12.界面模块对接

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

UI模块使用pyqt5设计,计算模块使用C++实现,因此我们使用ctypes包进行两个模块的对接。我们使用包内的WinDLL()实现dll文件的载入,然后将python中参数的数据类型通过ctypes将python中的数据类型包裹为C++中的数据类型,然后传给dll中对应的函数进行计算。返回值如果为异常值,则进行报错,如果为正常值则将返回的单词链解构为python中的数据类型后,输出到UI界面上。示例代码及运行结果如下 :

def split2bytes(s):
    wordList = re.compile('[a-zA-z]+').findall(s)
    length = len(wordList)
    words = (c_char_p * length)()
    for p in range(length):
        b_str = bytes(wordList[p].encode('utf-8')).lower()
        words[p] = b_str

    return words, c_int(length)


def bytes2str(words, length):
    res = ""
    for i in range(length):
        res += words[i].decode()
        res += "\n"
    return res
dll = WinDLL(r".\\core.dll")
words, length = utils.split2bytes(words)
result = (c_char_p * 1000)()
head = c_char(head)
tail = c_char(tail)
reject = c_char(reject)
enable_loop = c_bool(enable_loop)

chainLen = dll.gen_chain_word(words, length, result, head, tail, reject, enable_loop)

if chainLen < 0:
    raise WordException(chainLen)

res = utils.bytes2str(result, chainLen)

正常运行结果:

请添加图片描述

异常报错:

请添加图片描述

13.结对过程

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

我们全程采用了线下结对的方法,所有的模块和单元项目的讨论都在一起完成,而构建和修正由二者分别完成,修正之后发送到github上面同时下次先线下结对的过程中进行讨论。

请添加图片描述

14.结对优缺点

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

对编程的优点和缺点

优点

  • 结对编程中一份代码会经过两人的审查,减少了bug的产生,提高了代码质量
  • 结对编程中两人可以互相学习,彼此互补。
  • 结对编程可以提高两人的沟通和协作能力。
  • 结对编程中能保持专注,两人会在无形中互相监督,不会走神开小差导致效率低下。

缺点

  • 结对编程需要考虑队友的想法,所有任务只有双方都确认通过后才能继续,如果不能保持一致就会陷入僵局。
  • 结对编程中如果在某一方面两人差距较大,则只能由擅长者一人驱动,另一人无法起到任何帮助,失去了结对意义。
  • 结对编程需要两人有大量的共有可支配时间。

个人评价

  • 优点
    • 代码结构与coding思路清晰
    • 善于合作,能清晰讲解自己的思路与做法
    • 有过GUI编写的经历
  • 缺点
    • 完成某一部分时容易过于细致(完美主义),导致整体进度延后
    • coding过程中容易笔误出现bug

队友评价:

  • 优点
    • 善于合作,能清晰讲解自己的思路与做法
    • 新工具上手较快(如各种测试框架和各种分析插件)
    • 执行性强,能及时完成任务
  • 缺点
    • 算法及coding能力较弱

15.小组对接(附)

我们与班上其他两位同学(学号:19374223 19374006)进行了core.dll的互换。因为我们均参照课程组约定的标准接口进行设计,因此在正常调用时可以直接进行替换。不一致之处主要在于异常情况的反馈与处理。问题及解决方案如下:

  • 异常返回值不一致

我们将无-r参数时的有环异常返回的异常码设置为0x80000001,而另一组将的异常码为-3。因此在互换后需要将CLI端和GUI端的异常码同步进行修改。

  • 缺少异常

我们的设计中,如果长度超过20000,则会提示“链过长异常”,而另一组并未对此进行限制,即使长度超过20000也会全部返回。为满足我们的设计,需要在前端对长度进行一次特判,如果超过长度限制则抛出异常。

另外,我们又和20373779、20373790小组的同学互换了模块。我们都是按照课程组约定的标准接口进行设计,因此模块也可以直接替换。不一致之处在于对于异常的反馈和处理,具体表现为异常返回值不一致:因此解决方案和上面相同。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值