软件工程课程第二次作业
曾沅伟 M23120103 马超 M23185402
一、任务分工
曾沅伟:
①博客撰写;
②编写代码程序以实现“对历史成绩进行存储和查询”这一需求,包括用户名和成绩存储、成绩查询、历史成绩显示的功能;
③性能分析。
马超:
①编写代码程序以实现其他7个需求;
②单元测试;
③建立里程碑。
二、代码仓库中项目地址
MaChao_Zeng yuan wei: 作业实践 (gitee.com)
或者https://gitee.com/hezhiyong_edu/autumn-2023
三、PSP表格以及分析
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | 60 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 60 | 60 |
Development | 开发 | 1180 | 1300 |
· Analysis | · 需求分析 (包括学习新技术) | 240 | 260 |
· Design Spec | · 生成设计文档 | 120 | 130 |
· Design Review | · 设计复审 | 60 | 90 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 40 | 60 |
· Design | · 具体设计 | 120 | 140 |
· Coding | · 具体编码 | 410 | 420 |
· Code Review | · 代码复审 | 70 | 80 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 120 |
Reporting | 报告 | 260 | 300 |
· Test Repor | ·测试报告 | 100 | 120 |
· Size Measurement | ·计算工作量 | 60 | 70 |
· Postmortem & Process Improvement Plan | ·事后总结, 并提出过程改进计划 | 100 | 110 |
合计 | 1500 | 1660 |
从上面的PSP表格中可以得知,个人开发软件项目的部分过程的预估耗时和实际耗时存在一些偏差,总体来看是实际效率比预期效率稍低。究其原因是我们小组两人对整个软件开发的流程以及每个过程应该做什么工作还不是特别的清楚,同时我们本身不是计算机专业学生,对于编程这一块还有部分短板,在设计代码规范、编写代码以及代码复审这块实际用时较多。
四、每日软件工程学习日志
曾沅伟的学习日志:
学习时段 | 学习内容 | 收获体会 | 自我效率评价 |
10月17日1420-1630 | 个人比较喜欢体育,文章用足球团队流程以及篮球运动员的赛场数据深深吸引了我去阅读,也对PSP各个阶段有了新的认识 | 对自己喜欢的事情比较上心,总体效率较高 | |
10月17日1840-1930 | 制作PSP表格并填写各个阶段完成时间的估计值 | 表格制作还是比较简单的,但对于阶段完成时间的估计可能有许多不确定因素 | 总体效率较高 |
10月17日2000-2130 | 阅读邹欣老师的博客:源代码管理 | 这篇文章以老师和学生的对话为切入点,能将软件工程的质量和生活中建房子的质量很好的结合起来,我明白了源代码管理工具和构建系统对于软件质量的重要性,也通过10个实践问题对代码的编写、修改以及测试有了新的认识 | 总体效率较高 |
10月19日0900-1000 | 编写并在仓库中提交四则运算程序代码:历史成绩存储 | 使用了很久没有使用过的文件指针编写程序,重新唤起了记忆,感觉非常有成就感 | 一日之计在于晨,个人感觉能充分利用好时间,效率较高 |
10月19日1440-1650 | 编写并在仓库中提交四则运算程序代码:显示历史成绩 | 代码编写思路与上一步相似,进一步充实了整个程序框架 | 总体效率一般 |
10月19日1900-2100 | 对Commit message的作用、格式以及生成Change log有了初步的了解 | 总体效率较高 | |
10月20日0810-1000 | 编写并在仓库中提交四则运算程序代码:用户名和历史成绩查询 | 我在本科期间开发的“在线考试系统”中也有“用户名和历史成绩查询这一功能”,感觉找到了曾经的灵感,同时对文件指针的运用更加熟练 | 一日之计在于晨,个人感觉能充分利用好时间,效率较高 |
10月20日1100-1140 | 重点对单元测试的编写以及好的单元测试标准进行了阅读,发现与教授在课上讲的基本一致,能很好的理解其内涵和要点,同时也了解了回归测试 | 一上午都在编写程序代码以及撰写博客,到这时注意力容易分散,效率有所下滑 | |
10月21日1420-1500 | 完善PSP表格:填写各个阶段完成时间的实际值 | 通过估计值与真实值的比较,既能明显反映出开发软件项目效率的高低,也能衡量软件开发的质量和工作量 | 总体效率较高 |
马超的学习日志:
学习时段 | 学习内容 | 收获体会 | 自我效率评价 |
16日 | 程序编写和C语言基础知识学习 | 初步构建了四则运算基础框架,落实双人具体分工,编写main函数,确定程序中主要功能模块、自定义函数及重要变量。 学习C语言知识:C语言标准函数库,为后续程序中调用各种函数打好基础。 | 一般 |
17日 | 程序编写和C语言基础知识学习 | 学习C语言知识:随机数生成和switch函数使用。在程序中定义能生成随机整数和随机运算符的函数。 | 一般 |
18日 | 程序编写和C语言基础知识学习 | 学习C语言基础知识:如何定义宏。 完成文件头、主函数、随机数和运算符生成函数等代码上传同步。 | 一般 |
19日 | 程序编写和C语言基础知识学习 | 学习C语言基础知识:文件系统操作,学会如使用库函数数据写入本地或从本地读数据并赋给变量。对实施计算功能的函数定义具体算式生成细节。 | 一般 |
五、解题思路描述
(一)思考过程
1、分析本题程序要实现的需求,确定函数总体框架
首先要明确程序要实现的需求,即
①题目数量可以指定;
②支持加减乘除4种运算;
③每道题包含两个运算符;
④运算数为100以内的数字;
⑤保证答案在0..100之间;
⑥需要有答题功能并验证答案是否正确;
⑦可以判分并对历史成绩进行存储和查询;
⑧题目避免重复。
针对这8个需求,想要全部实现,初步判断至少需要8个函数,同时可能会增加实现“显示题目”这一隐含需求的函数。此外,“保证答案在0~100之间”和“需要有答题功能并验证答案是否正确”这两个需求有共同之处,可以用一个函数实现。最后,需要设计一个主函数(main)来调用这些函数。
2、做好编程以及博客撰写分工
后续通过和我的搭档密切协商,决定由我主要负责博客的撰写和程序性能分析,同时也编写代码程序以实现“对历史成绩进行存储和查询”这一需求,虽然只有一个需求,但这一需求可能需要的功能函数也较多,包括用户名和成绩存储、成绩查询、历史成绩显示等功能。
我的搭档负责实现其他需求的代码编写以及总程序的单元测试。
3、搜集相关资料
这一过程描述将在(二)找资料过程中具体体现。
4、分块编写程序代码
根据分工,需要我和搭档按照时间计划编写相关函数的代码,满足题中明确的编码要求,定期在仓库中提交、评审以及合并,最终完成整个程序代码的编写。
5、在仓库中提交、评审以及合并
需要我定期在仓库中提交代码,请搭档评审以及合并,最终完成整个程序代码的编写。
6、单元测试
正如教授在上课时所说,程序能正常运行,不代表它没有错,不存在bug。因此需要在完成整个程序代码编写的基础上,实现对程序的单元测试,以保证程序的正确性,这就需要借助Visual Studio 2022的编程环境通过编写单元测试代码来实现。
7、撰写博客
需要满足题中的博客撰写要求。
(二)找资料过程
1、阅读《现代软件工程讲义2 工程师的能力评估和发展》
寻找并学习与PSP表格的有关内容,了解个人软件开发流程的各个阶段,从他人对实现个人软件开发过程的时间记录和效率分析中获取灵感和启发,以便于自身更好完成“记录工程师如何实现需求的效率”这一任务。
2、阅读邹欣老师的博客:源代码管理
搜集并学习与源代码管理有关的知识点,了解源代码管理的10个实践问题,
进而掌握代码的编写、修改以及测试的具体流程和细节,为在Gitee上实现代码签入打下坚实理论基础。
3、阅读《阮一峰的博客:Commit message 和 Change log 编写指南》以进一步了解Commit。
4、阅读《邹欣老师的博客:现代软件工程讲义 2 开发技术 - 单元测试 & 回归测试》
期望获取有关利用编程环境编写单元测试代码以及如何设计测试用例的有关知识。
六、设计实现过程
(一)代码组织
根据需求进行代码功能划分,要实现的目标程序要包含以下功能:
(1)产生100以内的随机数;
(2)从加减乘除四种运算符中随机选出一个运算符;
(3)保证在一题中先进行乘除运算,后进行加减运算;
(4)用符号定义加减乘除四种运算符;
(5)显示题目;
(6)存储用户名和成绩;
(7)显示历史成绩;
(8)建立练习模块;
(9)查询历史成绩。
(二)函数关系
要实现(一)中的所有功能,需要两类函数:调用函数和被调用函数,包括1个主函数(调用函数)和9个被调用函数,分别为:
①int main();
②int generateRandomNumber();
③char generateRandomOperator();
④double calculate(int num1, char op1, int num2, char op2, int num3);
⑤double _calculate(double num1, char op, double num2);
⑥void displayQuestion(int num1, char op1, int num2, char op2, int num3);
⑦void storeScore(int score, char *username);
⑧void displayScores();
⑨void practice();
⑩void find();
②-⑩的9个函数分别对应9个要实现的功能,呈一一对应的关系,但⑨void practice()中需要调用②int generateRandomNumber()、③char generateRandomOperator()、④double calculate(int num1, char op1, int num2, char op2, int num3)、⑥void displayQuestion(int num1, char op1, int num2, char op2, int num3)、⑦void storeScore(int score, char *username)这5个函数,因此⑨void practice()可作为关键函数。
(三)关键函数流程图
如下是关键函数void practice()的流程图:
(四)单元测试设计
本次单元测试使用LDRA Testbed测试工具中的Tbrun进行,其具有操作简单、上手快、窗口化操作、无需编写脚本等优势,测试环境为虚拟机中的windows XP(32位)环境。
1、测试对象的选择
打开虚拟机中的单元测试工具LDRA Testbed,点击Testbed的菜单Fileàselect file 通过文件浏览窗口打开文件要分析的文件teamproject.c。
点击select之后,可以在工具快捷按钮栏的下方看见目前选择的文件。
2、单元测试模块Tbrun的打开
由Testbed进入Tbrun有三种模式,每种模式对应不同的测试类型,三种模式分别为:“Integration Unit / Module Test”、“Isolation Unit / Module Test”和“Unit Test Only”。由于我们使用的代码为C语言,故使用“Unit Test Only”模式,CPP的单元测试使用另外两种模式皆可。
在Testbed菜单TBrunàUnit Test Only点击,即可进入Tbrun模块。
3、测试序列(Sequence)的创建
在Tbrun中是使用测试序列(sequence)来容纳测试用例和测试数据的,进入Tbrun后第一步需要做的就是创建测试序列。
在Tbrun的菜单Sequence-àNew点击,弹出测试序列创建窗口,在“Sequence name”下面需要用户写入Sequence的名字;窗体下面的选项卡设置如下:
4、测试用例的创建
在Tbrun窗体的右上角File View窗口中,点击文件名能够展开显示文件中所包含的函数,把鼠标放到某个函数名上,右键弹出菜单点击“Create New Test Case”。
在此窗口中会显示函数的基本信息,可不用关注,直接点击按钮“Continue”向下进行,这样一个用例创建完成,可在用例管理窗口“Test Case View”中显示已经创建的用例,以数字编号用例,在“Variable I/O View”窗口中会显示此函数的输入输出参数。
在此窗口中会显示函数的基本信息,可不用关注,直接点击按钮“Continue”向下进行,这样一个用例创建完成,可在用例管理窗口“Test Case View”中显示已经创建的用例,以数字编号用例
在“Variable I/O View”窗口中会显示此函数的输入输出参数。
5、测试用例的输入输出参数IO值设定
测试用例的输入输出参数值的设定是测试的核心,在这一步决定了有什么样的输入和用例运行之后应该有什么样的输出(预期输出),从而实现了函数功能的验证。对于函数的输入输出不只有函数的参数,还包含函数内部使用的全局变量。
在“Variable I/O View”窗口中工具会显示出函数的参数和内部使用的全局变量,并会显示每个参数是输入还是输出,用户给每个参数赋值即可,注意输入以“I”标示的,输出是以“O”标示的,此处的输出是指预期的输出值。
对于标示为“I”的变量一定要有一个确定的值,要不让用例无法执行。
6、测试用例的执行
测试用例的IO设置设定之后,此用例就算构造完成了,接下来要做的就是执行用例,有两种运行方式,操作方法如下:
一键式运行用例;在测试用例管理窗口中的空白处右键可弹出菜单,点击“Run Test Case Driver”即可运行用例。
分步式执行用例;在测试用例管理窗口中的空白处右键可弹出菜单,按顺序点击“Generate Driver”、”Build Driver”、”Execute Driver”、(“Split Regression Driver Output”)、”Process Regression Driver Output”,每执行一步如果执行成功则会在旁边以“√”提示,如果不成功则没有“√”,请排查原因。
7、测试结果的查看
下面以十个用例测试的第一个,generateRandomNumber函数功能测试为例
单元测试其测试结果由两部分构成:函数功能验证和函数覆盖率信息。
函数功能的验证是通过用例的执行之后的用例确认对话框中的Pass/Fail结果反映的,我们可以点击界面中的Report,生成测试报告。
可以看到测试结果显示PASS通过。
以下为剩余9个用例的测试列表
七、性能分析及程序改进
(一)性能分析
使用Visual Studio 2022中内置的性能探测器对四则运算的总程序进行性能分析,分析内容包括:CPU使用率和内存使用率。
1、程序调试运行
2、生成性能探测报告
(1)内存使用率情况
从图中可以看出,进程内存基本上都是专用字节,总计796KB,是一个可以接受的范围。
(2)CPU使用率情况
从上图中可以看出,程序中CPU消耗最大的函数为main函数,其具体代码如下:
(二)程序改进
从以上性能分析情况得知,这个四则运算的总程序总体性能良好,但还可以从以下2个方面进行进一步改进:
1、使用 const 关键字声明常量,以提高代码的可读性。
如下是在practice()函数中的第185行利用const重新对常量isRepeated进行声明。
2、添加注释,以帮助其他开发人员理解代码的功能和实现方法。
在第249行添加注释“文件为空无法打开”,在第265行添加注释“提醒用户查询成功与否”。
八、代码说明
如下是关键函数⑨void practice()的关键代码:
void practice() // 定义练习模块函数
{
int numQuestions, score = 0; // 声明变量出题数,定义分数并初始化为0
char username[50];
printf("请输入用户名:");
scanf("%s", username); // 输入用户名
printf("请输入题目数量(结果保留小数点后两位): ");
scanf("%d", &numQuestions); // 输入出题数
int previousQuestions[100][5]; // 声明整型二维数组,保存历史题目,最多100题
for (int i = 0; i < numQuestions; i++)
{
int num1, num2, num3; // 声明3个随机数
double answer; // 声明一个结果数
char op1, op2; // 声明两个随机运算符
// 生成题目并验证是否重复
int isRepeated = 0;
while (1)
{
num1 = generateRandomNumber(); // 声明3个随机数,两个随机运算符
num2 = generateRandomNumber();
num3 = generateRandomNumber();
op1 = generateRandomOperator();
op2 = generateRandomOperator();
int idx = 0;
for (int j = 0; j < i; j++)
{
if (op1 == '/' && num2 == 0)
{
idx = 1;
break;
}
if (op2 == '/' && num3 == 0)
{
idx = 1;
break;
}
if (num1 == previousQuestions[j][0] && op1 == previousQuestions[j][1] && num2 == previousQuestions[j][2] && op2 == previousQuestions[j][3] && num3 == previousQuestions[j][4])
{
idx = 1;
break;
}
}
if (calculate(num1, op1, num2, op2, num3) >= 0 && calculate(num1, op1, num2, op2, num3) <= 100 && idx == 0)
break;
}
displayQuestion(num1, op1, num2, op2, num3); // 显示问题
printf("请输入答案: ");
scanf("%lf", &answer); // 输入答案
//;
double calculatedAnswer = calculate(num1, op1, num2, op2, num3); // 检查输入答案正确答案是否正确相符,并在0到100之间
if (fabs(round(answer * 100) - round(calculatedAnswer * 100)) < 1)
{
printf("回答正确!\n");
score++;
}
else
{
printf("回答错误。正确答案是: %lf\n", calculatedAnswer);
}
previousQuestions[i][0] = num1;
previousQuestions[i][1] = op1;
previousQuestions[i][2] = num2;
previousQuestions[i][3] = op2;
previousQuestions[i][4] = num3;
}
// 存储成绩
storeScore(score, username);
}
编写函数代码的思路:
主要是建立在对需求以及对应功能进行综合分析的基础上,结合前期已经声明过的相关函数,将一些目标功能进行整合,力争通过一个函数来实现这些功能的思路。
首先站在用户的角度上进行全流程思考:要想进行四则运算练习,必须先输入自己的用户名以及需要做题的数量,然后能根据系统随机产生随机数以及随机运算符号所形成的题目并进行相应作答,作答之后能显示相应答案以及判断是否与正确答案相符,最后知道自己的分数并自动存储至历史记录。
再站在我们结对两人的角度进行思考:可以在前期已经建立起的②int generateRandomNumber()、③char generateRandomOperator()、④double calculate(int num1, char op1, int num2, char op2, int num3)、⑥void displayQuestion(int num1, char op1, int num2, char op2, int num3)、⑦void storeScore(int score, char *username)这5个函数的基础上,增加代码把这些函数连接起来,将这个函数作为关键函数,也就是实现总体功能的主模块。
九、心路历程与收获
拿到这个项目问题之后,我们小组两人开始很疑惑,虽然都知道得先编程,并且写代码是关键,但对整个软件开发的流程以及每个过程应该做什么工作还不是特别的清楚。于是我决定先从PSP表格制作开始入手,从PSP表格去了解个人软件开发流程,所以我就去阅读了题中推荐的有关PSP的博客文章。有一篇文章用足球团队流程以及篮球运动员的赛场数据深深吸引了我,我也对PSP各个阶段有了全新的认识,以及构建起整个开发过程的框架。
之后结合构建之法的有序学习过程,我着手开始编写实现相关功能的函数代码,并对代码进行了性能分析。但这一过程耗费了大量的时间,因为对于编程这块我还达不到非常熟练的地步,我也是通过经常和搭档协商讨论,互相上传至代码仓库进行评审修改,最终才把程序完完整整地写出来。
在写代码的过程中我也有计划地开展博客的撰写,从最后结果来看,我们小组两人对此比较满意,所有的努力都没有白费,收获颇多。我至少对软件开发的整个流程及每个过程应该做什么工作有了清楚的认识,也通过构建之法相关博客的学习,对代码的设计规范、评审以及单元测试有了更深层次的理解,同时我的计算机编程能力也有所提高,希望能更好地完成后续作业任务。