软件工程课程第二阶段结对项目(功能扩展)
曾沅伟 M23120103 马超 M23185402
一、任务分工
曾沅伟:博客撰写
马超:编写实现扩展功能的代码并将代码提交至代码仓库
二、代码仓库中项目地址
MaChao_Zeng yuan wei: 作业实践 (gitee.com)
或者https://gitee.com/hezhiyong_edu/autumn-2023
三、新增功能描述
我们小组在课堂上所选择的扩展功能为:针对错题,可以生成类似题目。
为此,我们小组对此功能做如下描述:
“类似题目”是指2个运算符与做错的那道题一致,3个数是随机数。如果答错题目,就会生成“类似题目”,生成的数量就以新生成“类似题目”的答题情况为准,用户连续答对2道题,就跳出“类似题目”循环,继续完成剩下未出完的正常随机练习题;如果“类似题目”还答错系统就自动接着出“类似题目”,直到用户连续2答对道“类似题目”为止。
四、代码改动及函数关系
(一)原有程序代码
改动集中在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; // 声明两个随机运算符
// 生成题目并验证是否重复
const 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(); // 声明返回100以内随机数函数
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(); // 声明查询模块函数
在void practice()中,调用函数为
①int generateRandomNumber(); // 返回100以内随机数
②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);// 存储模块,将用户名和成绩存入本地
(三)改动之后的代码
void practice() // 定义练习模块函数
{
int numQuestions, score = 0; // 声明变量出题数,定义分数并初始化为0
char username[50];
printf("请输入用户名:");
scanf("%s", username); // 输入用户名
printf("请输入题目数量(结果保留小数点后两位): ");
scanf("%d", &numQuestions); // 输入出题数
int previousQuestions[100][5]; // 声明整型二维数组,保存历史题目,最多100题
/改动部分//
int numQ = 0; // 设置一个新的整型变量numQ,用于对应事先声明的previousQuestions二维数组中每道题的位置,主要作用是将嵌套的扩展模块中随机生成的错误练习题随机数与之前题目不重复
/改动部分///
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++) // 保证没有生成分母为0的除法,保证结果100以内
{
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) // 确保生成结果为0-100之间
break;
}
//改动部分///
previousQuestions[numQ][0] = num1; // 将随机生成的题目放入二维数组,前面设置的numQ对应每道题位置
previousQuestions[numQ][1] = op1;
previousQuestions[numQ][2] = num2;
previousQuestions[numQ][3] = op2;
previousQuestions[numQ][4] = num3;
numQ++; // 随机出题数量增加,numQ数值自动增加,方便在嵌套模块中以numQ为for语句循环条件,确保生成的错题是同一类型(在2个运算符不变的情况下,3个操作数随机生成,且与前面的所有操作数不重复
///改动部分//
displayQuestion(num1, op1, num2, op2, num3); // 显示问题
printf("请输入答案: ");
scanf("%lf", &answer); // 输入答案
//;
double calculatedAnswer = calculate(num1, op1, num2, op2, num3); // 检查输入答案正确答案是否正确相符,精度保持在小数点后两位
if (fabs(round(answer * 100) - round(calculatedAnswer * 100)) < 1)
{
printf("回答正确!\n");
score++;
}
else
{
///改动部分//
int cnt_idx = 0; // 声明一个整型变量,用来定义答错题生成同类型题目的答题情况,同类型题目答对一次+1
//////////改动部分//
printf("回答错误。正确答案是: %lf\n", calculatedAnswer);
//////扩展模块//
while (1) // 设置无限循环条件
{
if (cnt_idx == 2) // 在同类型题目答题中连续答对两次,则跳出循环,继续剩余的正常答题流程
break; // 跳出循环
int isRepeated = 0;
while (1)
{
num1 = generateRandomNumber(); // 生成3个随机数,运算符不生成随机,继续沿用错题的运算符,以实现同类型题目的功能
num2 = generateRandomNumber();
num3 = generateRandomNumber();
int idx = 0;
for (int j = 0; j < numQ; j++) // 以numQ为循环条件,与前面已出的题作比较,除运算符外避免重复
{
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;
}
previousQuestions[numQ][0] = num1;
previousQuestions[numQ][1] = op1;
previousQuestions[numQ][2] = num2;
previousQuestions[numQ][3] = op2;
previousQuestions[numQ][4] = num3;
numQ++;
displayQuestion(num1, op1, num2, op2, num3);
printf("[当前正在做错题同类题,已做对:%d]请输入答案: ", cnt_idx);
scanf("%lf", &answer);
double calculatedAnswer = calculate(num1, op1, num2, op2, num3);
if (fabs(round(answer * 100) - round(calculatedAnswer * 100)) < 1) // 比对答题结果,精度2位小数
{
printf("回答正确!\n");
score++; // 正常记总分
cnt_idx++; // 回答正确记为答对一道同类型题
}
else
{
printf("回答错误。正确答案是: %lf,不好意思接着练!\n", calculatedAnswer);
cnt_idx = 0; // 回答错误,同类型题目答题得分直接归零,以设计意在实现连续答对两次同类型题目才跳出该循环的目的
}
}
}
}
/扩展模块/
// 存储成绩
storeScore(score, username);
}
(四)改动之后的函数关系
从改动结果来看,改动后的程序是在原有程序上先增设2个整形变量(numQ、 cnt_idx)以及5个二维数组(previousQuestions[numQ][0]、previousQuestions[numQ][1] 、previousQuestions[numQ][2] 、previousQuestions[numQ][3]、previousQuestions[numQ][4]),再在if else分支语句下建立一个2次循环嵌套的while循环语句(其中嵌套的一个while循环语句又嵌套了一个for循环语句),进行“生成错题类似题目”功能模块的扩展。此过程并没有新增函数,以及调用原有函数,因此函数关系基本保持不变。
五、已有代码的可维护性和可扩展性分析评价
(一)可维护性分析评价
代码可维护性是指在软件的开发和维护过程中,代码的质量高,易于修改同时便于用户理解。我们可以从使用命名规范和注释的程度、是否遵循 DRY 原则、是否使用性能分析、项目代码量的多少以及小组成员的开发水平对代码进行相应的可维护性分析评价。
1、使用命名规范和注释的程度
从程序中可以看出,我们小组对于变量名的定义和函数名的声明都比较清晰,命名规则一致,基本上是采用了易于用户读懂的英文(大概是英语四级的水平)进行变量和函数的声明,几乎避免了缩写和未定义的术语,例如 char username[50],很明显这是定义了一个限额为50个字符的用户名字符串变量;还有void storeScore(int score, char *username),这是声明了一个将用户名和分数进行存储的函数。
此外,对于关键的函数调用声明、变量定义、输入输出语句,程序都做了相应的比较清晰的有意义注释,例如:
previousQuestions[numQ][0] = num1; // 将随机生成的题目放入二维数组,前面设置的numQ对应每道题位置
previousQuestions[numQ][1] = op1;
previousQuestions[numQ][2] = num2;
previousQuestions[numQ][3] = op2;
previousQuestions[numQ][4] = num3;
numQ++; // 随机出题数量增加,numQ数值自动增加,方便在嵌套模块中以numQ为for语句循环条件,确保生成的错题是同一类型(在2个运算符不变的情况下,3个操作数随机生成,且与前面的所有操作数不重复。
并且对程序所做的注释占到整个程序代码的20%以上,符合相关注释量标准。
2、是否遵循DRY原则
DRY原则即“不要重复自己”(Don't Repeat Yourself),这意味着我们在编写程序时要避免出现多个相同的代码段,以此来保证代码的模块化和封装性。
从整个程序来看,除了如下相同的代码段,其余地方基本上没有出现相同的代码段,遵循了DRY原则。
对于此处代码段,if括号里的条件判断语句不一样,但判断输出结果是一致的,即“idx=1;break;”,可以考虑将3个条件判断语句进行组合进行相应改进,如下图:
3、是否使用性能分析
在结对项目1的基础上,继续使用Visual Studio 2022中内置的性能探测器对改进后的四则运算总程序进行性能分析,分析内容包括:CPU使用率和内存使用率。
如下是开启性能探测器之后的命令行窗口:
(1)内存使用率情况分析
从上图可以看出,进程内存基本上都是专用字节,总计744KB,是一个可以接受的范围。
(2)CPU使用率情况分析
从上图中可以看出,函数外部调用所占CPU比重较大。其原因是在程序中,主要是通过main函数调用其他函数来实现四则运算的显示、计算、判错,成绩存储和查询以及出类似错题的功能。
4、项目代码量的多少
已有程序代码总计327行,属于长度适中的程序,对普通程序员来说是容易进行修改的。
5、小组成员的开发水平
我们小组成员既不是计算机专业的学生,也不是专业的软件开发者和程序员,但都稍微了解一些C语言的编程知识,在能看懂代码的基础上,还能编写相应的程序,同时对自己写的代码能进行单元测试和性能分析。最重要的是,我们对《软件工程》课程态度认真,孜孜不倦,始终怀着一颗进取的心。总体来说,我们小组成员开发水平一般,但也能保证程序具有较好的可维护性。
(二)可扩展性分析评价
可扩展性也是一个评价代码质量非常重要的标准。代码的可扩展性是指在不修改或少量修改原有代码的情况下,通过扩展的方式添加新的功能代码。我们可以从模块(函数)之间的耦合程度、每一模块是否完成单一功能以及是否遵循开放封闭原则进行代码的可扩展性分析评价。
1、模块(函数)之间的耦合程度
已有程序中除了main函数和void practice()函数中嵌套调用了其他函数,其余各函数之间相互独立,彼此不存在调用关系,并且相互依赖程度较低,函数之间的耦合程度较低。
2、每一模块是否完成单一功能
程序中除了main函数和void practice()函数的其他8个函数分别实现单一功能,比如void displayScores()函数实现显示历史成绩的功能,char generateRandomOperator()实现从加减乘除四种运算符中随机选出一个运算符的功能。并且从这次对软件功能进行扩展之后的代码改动情况来看,并没有新增一个函数来实现扩展功能,没有改变程序的主体结构,仅在分支语句的范围内进行扩展,说明已有程序还有较大的扩展空间。
3、是否遵循开放封闭原则
添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。开放封闭并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。从此次代码功能扩展来看,我们小组成员并没有在原有代码上进行过多的改动,而是通过新增代码来实现扩展功能,说明该软件的功能是开放的,同时也是封闭的,基本遵循了开放封闭的原则,即该软件允许未来功能的扩展,同时也不影响现有的代码。
综上所述,已有程序的可维护性和可扩展性都较好。