78
社区成员
题目修改自现代软件工程作业 – 计算最长英语单词链
实现一个计算最长英语单词链的程序
阅读《构建之法》第一章至第三章的内容,并在下方作业里体现出阅读后的成果。特别是第2章中的效能分析及个人软件开发流程(PSP)。
要求使用C/C++/C#语言实现,并基于CMake构建项目。对同学们的开发环境不做限制,仅提供建议,推荐使用CLion,也可使用VS、VSCode等,但是请注意本次作业的最终测试运行环境为64-bit Windows 10/11,采用C++14标准,CMAKE版本为3.24.2,编译器采用MinGW-w64 9.0,并基于Make 4.2.1进行构建。请自行测试可移植性。附:下载链接:Clion,CMake。
提交的代码要求消除所有的编译器警告,并截图展示在博客中。
完成项目的首个版本之后,请使用性能分析工具来找出代码中的性能瓶颈并进行改进。该部分不对具体性能分析工具进行限制,仅提供建议,Linux下可使用gperftools, Windows下可使用Visual Studio性能探查器。
使用单元测试对项目进行测试,写出至少10个测试用例确保你的程序能够正确处理各种情况,并使用插件查看测试分支覆盖率等指标。 该部分不对具体单元测试工具进行限制,仅提供建议,Linux下可使用gtest搭配gcov完成,Windows下可选择Visual Studio插件OpenCppCoverage或搭配使用WSL从而适用Linux方案。
使用Github/Gitee/GitLink(以下简写为Github)来管理源代码和测试用例,代码有进展即签入Github,签入记录不合理的项目会被助教抽查询问项目细节。
按照要求发布博客,利用在构建之法中学习到的相关内容,结合个人项目的实践经历,撰写解决项目的心路历程与收获。博客与Github项目明显不符的作业将取消作业成绩。
撰写一个博客,要求参见博客作业要求。
在个人博客上发布项目源代码(包含单元测试用例)的Github链接,以便助教下载,助教将会在测试环境中检查程序的正确性。
正确的程序会再进行性能测试,根据性能的好坏进行评分;不正确的程序没有性能的分数。
实现一个命令行程序Wordlist.exe
,对于包含有 N个不同的英语单词的文本,要求程序可以快速找出最长的能首尾相连的英语单词链,注意每个单词最多使用一次,且单词大小写不敏感:
其中,单词的定义为:被非英文字符间隔的连续英文字符序列
单词链的定义为:由至少2个单词组成,前一单词的尾字母为后一单词的首字母,且不存在重复单词
例如,给出单词文本为:
Hello WoRld! Softw
are_eng1neer
从文本中可以提取出的单词为:
hello world softw are eng neer
在默认情况下,输入的单词文本需要保证无法构成英语单词环(即末尾单词尾字母为首个单词首字母的单词链),对于不满足该条件的单词文本,程序不应该求解,而是给予用户错误提示。后续将加入参数-r来指定在存在单词环时进行求解。特别指出,单词的输出单词链中的位置顺序与其输入顺序无关。
我们对最长的定义可以分为两种:最多单词数量和最多字母数量,将对应到不同的参数。
在命令行中使用-n
参数统 计该单词文本中共有多少条单词链,包含嵌套单词链。
如:
Wordlist.exe -n absolute_path_of_word_list
注意:
woo oom moon noox
这里在满足不包含单词环的情况下,包含6个单词链,请在返回单词链数6的同时,每一行输出一条单词链,不同单词用空格分隔。
输出示例:
6
woo oom
moon noox
oom moon
woo oom moon
oom moon noox
woo oom moon noox
在命令行中使用 -w
参数加文件名的形式计算最多单词数量的英语单词链,并将结果输出至文件,例如:
Wordlist.exe -w absolute_path_of_word_list 程序将从路径中读取单词文本,并将最长单词链输出至与
Wordlist.exe
同目录的solution.txt
中,每次生成的txt文件需要覆盖上次生成的txt文件。
注意:
Algebra
Apple
Zoo
Elephant
Under
Fox
Dog
Moon
Leaf
Trick
Pseudopseudohypoparathyroidism
输出示例:
将结果输出到文件中,每行仅包含一个单词,单词为小写,例如:
algebra
apple
elephant
trick
在命令行中使用 -c
参数加文件名的形式计算字母最多的英语单词链,并将结果输出至文件,例如:
Wordlist.exe -c absolute_path_of_word_list
注意:
pseudopseudohypoparathyroidism
moon
在命令行中使用 -h
参数加字母的形式,指定单词链的首字母,例如:
Wordlist.exe -h e -w absolute_path_of_word_list
注意:
elephant
trick
在命令行中使用 -t
参数加字母的形式,指定单词链的尾字母,例如:
Wordlist.exe -t t -w absolute_path_of_word_list
输出示例:
algebra
apple
elephant
在命令行中使用 -j
参数加字母的形式,指定不允许出现的首字母,例如:
Wordlist.exe -j a -w absolute_path_of_word_list
注意:
elephant
trick
本需求为性能测试部分主要考察内容,请同学们选取合适的算法并进行优化
在命令行中使用 -r
参数,表示允许单词文本隐含单词环,注意文本中每种单词只能使用一次(即使单词在文本中多次出现,也只能使用一次),如:
Wordlist.exe -r -w absolute_path_of_word_list
注意:
Element
Heaven
Table
Teach
Talk
输出示例:
table
element
teach
heaven
此处table
-element
构成单词环,如果命令行中不包含 -r 参数,则此种情况应当做异常处理。
现在已经有了一个单词链计算程序的命令行版本,如果想让大家都能实际使用它,还需要一个简单的界面。请为你们的程序做一个GUI界面(不限制GUI的编程语言),并附上一个简单的使用说明。界面需正确实现下述功能,会按点给分:
-n -w -c -h -t -r -j
这七个参数的功能,对于异常情况需要给予用户适当反馈提示(3')【注意】选择完成本附加题目的同学,需要将GUI与单词计算模块作为两个工程开发,后者可以作为依赖库为前者提供调用接口,但不可以把两个工程直接混在一起。 GUI相关的部分也需要提供新的可执行文件,放在根目录的guibin/文件夹下。
项目要求:
本项目对于输入的参数和文件名的顺序没有假设。
在完成这一阶段的任务之后,使用git tag step1
标记第一阶段已经完成,并在Push到Github上时使用--tags
参数把tag也推送到Github,例如git push origin --tags
。
在第一阶段中,我们使用各种语言实现了一个命令行求解最长单词链的小程序。下面我们将逐步将我们的小程序升级为能稳定运行,给用户提供服务的软件。
大家的代码都各有特色,大家写的“软件”也有一定的用处。如果现在我们要把这个功能放到不同的环境中去(例如,命令行,Windows图形界面程序,网页程序,手机App),就会碰到困难:许多同学的代码都散落在各个函数中,很难把剥离出来作为一个独立的模块运行以满足不同的需求。
我们看到,不同的代码解决不同层面的问题:
有些是计算数据的(例如计算单词链)
有些是控制输入的(例如scanf,cin,图形界面的输入字段)
有些是数据可视化的(例如printf,cout,println,DrawText) 有些则更为特殊,是架构相关的(例如main函数,并不是所有的程序都需要某个特定格式的main) 这些代码的种类不同,混杂在一起对于后期的维护扩展很不友好,所以它们的组织结构就需要精心的整理和优化。
我们希望把计算单词链的功能独立出来,成为一个独立的模块(class library, DLL, 或其它),这样的话,命令行和GUI的程序都能使用同一份代码。为了方便起见,我们称之为计算核心"Core模块",这个模块至少可以在几个地方使用:
命令行测试程序使用
在单元测试框架下使用
与数据可视化部分结合使用 把计算核心在单元测试框架中做过完备的测试后,我们就可以在算法层级保证了这个模块的正确性。
但我们知道软件并非只有计算核心,实际的软件是交付给最终用户的软件,除了计算核心外,还需要有一定的界面和必要的辅助功能。那么这个Core模块和使用它的其他模块之间是什么关系呢?它们要通过一定的 API(Application Programming Interface) 来和其他模块交流。这个API接口应该怎么设计呢?为了简单,我们可以从下面的最简单的接口开始:
int gen_chain(char* words[], int len, char* result[]);
这个函数接受三个参数,words
为输入的单词列表,len
为单词列表的长度,result
存放单词链,函数返回值为单词链长度。
假设我们用类Core
封装了这个接口,我们的测试程序可以是非常简单的:
char* input[4] = {"END", "OF", "THE", "WORLD"};
char* result[4] = {0};
/* 调用Core中封装好的函数 */
int len = Core::gen_chain(input, 4, result);
Assert(len == 2);
当然,我们这里的判断并不充分,仅判断了单词链长度,没有判断单词链的合法性,但同学们在测试时不能这样“偷懒”。
我们要把第一阶段中实现的功能封装成独立的模块并一一进行测试,比如读取单词文本文件、输出打印等。建议大家在每一步只增量修改一个模块并做测试。这里的测试包括新模块的单元测试与原功能的回归测试。每实现一个新的功能,要保证以前运行正确的例子继续是正确的。通过这样的回归测试,可以保证自己实现的系统始终是满足预定状态约束的。(请看书中关于单元测试,回归测试的内容)在确认修改的功能正确之后再签入代码。
项目要求:
在第一阶段代码的基础上增量修改,将所有与计算单词链相关的功能封装到名为core.dll
的动态链接库模块中,并通过API接口与命令行测试程序和GUI程序等进行交互。
测试将在命令行程序上进行,动态链接库的API接口定义并不影响测试行为
为了方便各组之间交换,以下为建议的API接口行为,各组可自定义类似的API接口,但需自行在互换前后端时与其他组进行协商。
实现int gen_chains_all(char* words[], int len, char* result[])
接口,其中前三个参数已经在上文进行了说明,函数返回所有符合定义的单词链,函数返回值为单词链的总数
实现int gen_chain_word(char* words[], int len, char* result[], char head, char tail, char reject, bool enable_loop)
接口,计算最多单词数量的最长单词链,其中前三个参数已经在上文进行了说明,head
和tail
分别为单词链首字母与尾字母约束(如果传入0,表示没有约束),reject
为单词链中单词不允许出现的首字母约束,当enable_loop
为true
时表示允许输入单词文本中隐含“单词环”
实现int gen_chain_char(char* words[], int len, char* result[], char head, char tail, char reject, bool enable_loop)
接口,计算最多字母数量的最长单词链,参数意义同gen_chain_word
指针数组result的长度上限为20000,超出上限时报错并保证返回值正确,此时输出到solution.txt中的单词链可以为空。
关于如何申请内存空间的问题,各组可以自己选定在动态链接库内/外部申请或释放空间,这并不在课程组要求之内,但是需要保证不出现内存泄露问题,及时释放空间。
关于如何定义和处理异常,可以参考Modern C++ best practices for exceptions and error handling。课程组并不定义任何实际的异常类型,仅指出部分的可能出现的异常状况,并不代表代码只应该处理这些异常。程序员应对自己的代码健壮性负责,定义并处理包括数值溢出以及除零在内的各种可能出现的异常。
如果采用推荐的API接口,由于各组之间需要互换前后端,且推荐的API接口中返回值已经具有实际意义,因此不宜采用直接返回报错码的方式处理,因此各位不要在返回值上承载异常信息,保证返回值正确。
对gen_chain_word
,gen_chains_all
和gen_chain_char
或其他自定义接口进行测试,确保模块的所有API接口都进行了单元测试,把单元测试代码Push到Github上(注意避免把单元测试的结果Push到Github上)
在完成这一阶段的任务之后,使用git tag step2
标记第二阶段已经完成,并在Push到Github上时使用--tags
参数把tag也推送到Github,例如git push origin --tags
。
博客要求:
若采用上述给定接口,请详细介绍你对于上述接口的实现方式;若采用自定义接口,请详细介绍接口设计的思路和实现方式,并说明如何使用这些接口。
选择部分单元测试代码发布在博客中,并说明测试的函数,构造测试数据的思路。
将单元测试得到的测试覆盖率截图发表在博客中。总体覆盖率达到90%以上则可获得单元测试部分的得分。
在上面我们只讨论了正确的输入下,我们对于程序输出的期待。但如果程序的输入出现了错误,比如命令行参数是其他字符,或者有多个无意义参数等等,你又该怎么办呢?要怎样才能告诉函数的调用者“你错了”?又该如何方便地告诉函数的调用者“哪里错了”?在这种时候,我们一般会定义各种异常(Exception),让Core
在碰到各种异常情况的时候,能给调用者充分的错误信息。当然,我们同样要进行增量修改:
项目要求:
设计好异常的种类与错误提示,例如让程序支持“首尾字母约束不合法”或“单词文本隐含单词环”异常。
在Core
模块中实现抛出异常的功能,并撰写测试用例:传进去一个错误的参数或给出一个错误的单词文本,期望能捕获这个异常。如果没有,测试就报错。
回归测试所有以前的功能,保证以前的功能还能继续工作。
在完成这一阶段的任务之后,使用git tag step3
标记第三阶段已经完成,并在Push到Github上时使用--tags
参数把tag也推送到Github。
博客要求:
在博客中详细介绍对哪些异常进行了处理以及每种异常的设计目标。
每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。
在前面的工作中,有的小组使用命令行,保证了程序的正确性,还有的小组在此基础上绘制了GUI界面。既然各组同学都写了高质量的各个模块,而且模块之间的关系是明确定义的,一致的,那么,小组A的测试模块就可以测试小组B的核心模块;小组C的用户界面模块就可以和小组B的核心模块结合起来,正常运行。对吧?
那么现在,请你(假设为A)寻找另外一个小组(假设为B),与他们交换核心模块与界面模块,并测试一下下面的情况:
根据与合作小组对接过程中出现的问题,寻找并改进模块中的bug。这部分修改需要另开一个新的分支dev-combine
,并Push到Github上。
博客要求:
在博客中指明合作小组两位同学的学号,分析两组不同的模块合并之后出现的问题,为何会出现这样的问题,以及是如何根据反馈改进自己模块的。
测试内容
我们都知道健壮性对于软件来说是非常必要的,所以本次自动测试我们也会加入各种各样出错情况的测试。助教测试时将会选择不同种类的出错场景,要求开发者程序不会崩溃的情况下,能够尽可能精确报错(就像编译器一样)。你可以有“容错性”的出错设计,但必须输出必要的提示或说明。
测试包括三部分:正确性测试(正确场景)、鲁棒性测试(错误场景)、性能测试,提交的所有程序都要进行正确性测试和鲁棒性测试,正确性测试通过的程序需要进行性能测试。
测试须知
提交到 Github 上的项目请注意以下几点:
/bin/
的文件夹(这里 /
表示项目的根目录),该文件夹中必须含有可执行文件与相关的所有依赖库。Wordlist.exe
,位于 /bin/
中。solution.txt
与可执行文件在同一目录下,生成文件时请使用相对路径!WordListProject/ # 项目名字可自行指定
├── bin # 助教测试用文件夹
│ ├── Wordlist.exe
│ ├── core.dll # 核心模块 DLL
| └── lib.dll # 其他模块 DLL
├── README.md
├── src
│ └── main.cpp
└── test # 测试相关代码
└── test_wordlist.cpp
注:GitHub 上的项目不应包含 WordListProject
文件夹,而应包含其内容。即项目主页应该看到 bin/
,README.md
,src/
和 test/
,而非仅有 WordListProject/
。
发表在你的个人博客上,也可以同时转发到你的团队博客上来增加你们团队博客的人气。博客共 50 分,具体要求如下:
源代码管理评分:
该评分主要通过源代码管理中的commit注释信息,增量修改的内容,是否有运行说明,每个阶段是否打上了标签等内容给分。
第一阶段:
该评分将进行这-n -w -c -h -t -r -j
七个参数的正确性测试,对于前六个参数,输入的单词数量范围为0-10000;对于-r
参数,输入单词数量的范围为0-100,要求程序在 300s 内给出结果,超时则认定运行结果无效。
第二、三阶段:
将针对上述七个参数进行鲁棒性测试,可能测试的内容包括且不限于:
错误的命令、错误的参数、大小写、错误的参数组合、错误的文件格式等。
要求必须正常结束,崩溃不得分。
错误无任何提示,不得分。
错误种类较多,提示合理,得正分。
性能评分
当第一阶段评分为满分时才可以参与性能评分环节,所以请各位同学务必保证自己程序的正确性,该阶段没有时间的最小要求限制。
性能评分将采取档级评分制度,助教将根据同学们的程序跑同一数据耗费的时间长度将程序分为若干档。
附加需求:GUI(10')
该评分将进行用户交互界面的测试
第四阶段:模块松耦合(10')
在结对项目博客中按照阶段四的博客要求添加相应内容(5') 最终的对接效果(5')
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | ||
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | ||
· Design Spec | · 生成设计文档 | ||
· Design Review | · 设计复审 (和同事审核设计文档) | ||
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | ||
· Design | · 具体设计 | ||
· Coding | · 具体编码 | ||
· Code Review | · 代码复审 | ||
· Test | · 测试(自我测试,修改代码,提交修改) | ||
Reporting | 报告 | ||
· Test Report | · 测试报告 | ||
· Size Measurement | · 计算工作量 | ||
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | ||
合计 |
助教在测试时,将以命令行运行可执行文件的方式进行批量测试,参数及其约定如下:
参数名字 | 参数意义 | 范围限制 | 用法示例 |
---|---|---|---|
-n | 需要求出单词文本能够构成所有单词链的数目 | 绝对或相对路径 | 示例:Wordlist.exe -n input.txt [表示从input.txt中读取单词文本,计算能构成单词链的总数目] |
-w | 需要求出单词数量最多的单词链 | 绝对或相对路径 | 示例:Wordlist.exe -w input.txt [表示从input.txt中读取单词文本,计算单词数量最多的单词链] |
-c | 需要求出字母数量最多的单词链 | 绝对或相对路径 | 示例:Wordlist.exe -c input.txt [表示从input.txt中读取单词文本,计算字母数量最多的单词链] |
-h | 指定单词链首字母 | a-z,A-Z | 示例:Wordlist.exe -h a -w input.txt [表示从input.txt中读取单词文本,计算满足首字母为a的、单词数量最多的单词链] |
-t | 指定单词链尾字母 | a-z,A-Z | 示例:Wordlist.exe -t a -c input.txt [表示从input.txt中读取单词文本,计算满足尾字母为a的、字母数量最多的单词链] |
-j | 指定不允许出现的首字母 | a-z,A-Z | 示例:Wordlist.exe -j a -w input.txt [表示从input.txt中读取单词文本,计算在不允许出现首字母为a的单词的情况下单词数量最多的单词链] |
-r | 允许单词文本中隐含单词环 | 示例:Wordlist.exe -r -w input.txt [表示从input.txt中读取单词文本,计算单词数量最多的单词链,即使单词文本中隐含单词环也需要求解] |