(点击可页内跳转)
1. 项目地址
2. PSP表格
3. 解题思路描述
———3.1项目分工
———3.2需求分析
———3.3类的组织
4.分工部分设计实现(不包括队友部分)
5.单元测试
6.心得感悟
团队项目已经上传GitHub。
控制台项目地址:https://github.com/Archie7777/Arithmetic-Problems
GUI版本:https://github.com/Archie7777/Shitty-Game
队友博客网址:https://blog.csdn.net/weixin_43407283/article/details/86478548
PSP | Personal Software Process Stages | 计划用时(min) | 实际用时(min) |
---|---|---|---|
PLANNING | 计划 | - | - |
----estimate | 估计这个任务需要多长时间 | 10 | 10 |
DEVELOPMENT | 开发 | - | - |
----analysis | 需求分析(包括学习新技术) | 120 | 200 |
----design spec | 生成设计文档 | 60 | 80 |
----design review | 设计复审(和同事审核设计文档) | 30 | 20 |
----coding standard | 代码规范(为目前的开发制定合适的规范) | 10 | 10 |
----design | 具体设计 | 20 | 60 |
----coding | 具体编码 | 1200 | 1800 |
----code review | 代码复审 | 300 | 300 |
----test | 测试(自我测试,修改代码,提交修改) | 120 | 200 |
REPORTING | 报告 | ||
test report | 测试报告 | 20 | 60 |
size measurement | 计算工作量 | 10 | 10 |
postmortem & process improvement plan | 事后总结,并提出过程改进计划 | 180 | 100 |
合计 | 2080 | 2850 |
因为需要实现的功能较少,且不是特别复杂,在设计初期决定采用瀑布模型进行开发,在编程实现过程中一有进展就签入,保证了工程逐步进展。
3.1项目分工
在项目的设计初期,我们将项目划分成了2个部分,运算表达式生成部分(也就是工程主体)和GUI部分。最后决定由另一名同学完成GUI和主要数据结构的设计和实现,我写参数传入,主函数,运算表达式生成的实现部分。
因为两个人都从来没用过C#,所以我和队友打算用C#在VS下完成项目开发,顺便还能学习一门新的语言。
3.2需求分析
我们组先仔细研读了项目的题目,提取出其中的关键信息如下:
1.第一阶段:
a)生成1000道不重复的题目写入文件中
b)实现表达式求值功能
c)支持真分数的四则运算
d)程序接受用户输入的对错并完成判断,给出总共 对/错 数量
2.第二阶段:
添加乘方运算
3.第三阶段:
实现图形用户界面GUI
3.3类的组织
我们将程序的主题分为6个类:
ArgumentParse:参数分析,用来分析命令行输入,向程序内部传递命令行参数。
Component:表达式构件类(抽象类),目前没有用到。
Expression:表达式类。
Number:操作数类,操作数的相关操作都在其中,允许真分数运算。把整数也看做是分数,只是整数的分母默认为1。
Operation:运算基本操作类,并将结果输出。
Program:主函数类,程序的执行入口,用户可以选择不同难度级别。
在具体实现过程中,我承担了Program.cs(主程序类)、ArgumentParser.cs(参数分析类)和Expression.cs(表达式操作类)三个类的编写。
1.主程序Program.cs:
首先说一下主程序Program.cs的执行顺序:
当从命令行传入参数时,先用传入的参数初始化一个ArgumentParser型对象。然后通过这个ArgumentParser型对象方法接口的调用,对参数进行判断,使完成相关操作。
-g输入时:
1)如果选择输入的参数中含有-c或-d表示有乘方操作。
2)用户可以自行选择难度,如果输入的参数中有-h,就生成比较难的题目。
-p输入时:
只有用户输入stop,才会跳出循环,游戏结束,程序终止。
主函数代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace mathproblem
{
class Program
{
static void Main(string[] args)
{
ArgumentParser argument = new ArgumentParser(args);
if (argument.GetLength() == 0)
{
System.Console.WriteLine("Please enter an argument.");
System.Console.WriteLine("Usage:\n" +
"generate 1000 problems: -g absolute_path_of_output_file\n" +
"play games: -p num_of_problem_you_want_to_play\n" +
"solve problems: -s absolute_path_of_problem_file");
return;
}
if (argument.Get(1) == "-g")
{
if (argument.Has("-d") && argument.Has("-c"))
Console.WriteLine("Please input correct argument");
else if (argument.Has("-d"))
{
Expression.SetPrintType(Expression.PowerOpPrintType.doubleStar);
Expression.SetPowerOp(Expression.IsPowerOp.yes);
}
else if (argument.Has("-c"))
{
Expression.SetPrintType(Expression.PowerOpPrintType.caret);
Expression.SetPowerOp(Expression.IsPowerOp.yes);
}
else
Expression.SetPowerOp(Expression.IsPowerOp.no);
if (argument.Has("-h"))
Expression.SetDifficulty(Expression.Difficulty.hard);
using (System.IO.StreamWriter file = new System.IO.StreamWriter(@"./Expressions.txt", false))
{
for (int i = 0; i < argument.ToInt(2); i++)
Generate(file);
}
return;
}
if (argument.Get(1) == "-p")
{
int correctNum = 0;
int wrongNum = 0;
while (true)
{
Expression ep = new Expression();
while (ep.IsInvalid())
{
ep = new Expression();
}
ep.PrintExpression();
string answer = Console.ReadLine();
if (answer == "stop")
{
Console.WriteLine("正确数量:{0} 错误数量:{1}", correctNum, wrongNum);
break;
}
if (answer == ep.GetAnswerString())
correctNum++;
else wrongNum++;
}
return;
}
System.Console.WriteLine("Please enter correct argument.");
return;
}
static void Generate(System.IO.StreamWriter file)
{
Expression ep = new Expression();
while (ep.IsInvalid())
{
ep = new Expression();
}
ep.PrintExpressionWithAnswerToFile(file);
}
}
}
2. 参数分析类ArgumentParser.cs:
初始化之后,内部的各种方法基本上就是一些直接get成员变量值的操作了。
参数分析类ArgumentParser代码如下:
class ArgumentParser
{
public ArgumentParser(string[] args)
{
arguments = new string[args.Length];
for (int i = 0; i < args.Length; i++)
arguments[i] = args[i];
}
public bool Has(string arg)
{
for (int i = 0; i < arguments.Length; i++)
if (arg == arguments[i]) return true;
return false;
}
public bool NotHas(string arg)
{
return !Has(arg);
}
public int GetLength()
{
return arguments.Length;
}
public string Get(int num)
{
return arguments[num - 1];
}
public int ToInt(int num)
{
return Convert.ToInt32(arguments[num - 1]);
}
private string[] arguments;
}
3.表达式分析类Expression.cs:
可以根据用户选择的难度生成不同的表达式树:
如果是用户在命令行没有输入-h,默认就是生成简单树;如果输入了-h就是困难模式,生成困难树。
需要注意的异常处理:一旦发生除零现象,表示表达式无效,需要重新生成
简单表达式就是“A 操作符 B”,表达式对应的二叉树是简单树(只有2层)。左孩子存放左操作数A,右孩子存放右操作数B,根节点存放操作符
生成表达式简单树的过程:
1)左孩子节点随机选择0-21范围内的操作数,
2)根节点根据用户选择是否有乘方操作来选择。
——2.1如果用户选择无乘方操作,就在{+,-,* ,/}四种运算中随机选择
——2.2 else,如果用户选择有乘方操作,就在{+,-,* ,/}五种运算中随机选择
3)右孩子节点根据根节点的选择结果来定
如果根节点是普通运算符,那么右结点的值就在0-21之间随机选择
如果根节点是乘方运算符,那么右结点的值就是0-3之间随机选择
4)简单树生成完毕
如果需要输出,就用ChangeToString()将表达式树转化为字符串,存入string变量expression中,等待输出。
生成困难树的步骤是一个递归过程,困难树从根节点开始向下逐层生成,第一层根节点必须是运算符,第二层、第三层可以是数字也可以是运算符,第四层(如果有的话)则必须是数字:
1)如果当前是根节点(第1层),就在这个节点处生成运算符
2)如果还没有达到第8个结点(当前处于第2层或第3层)
——2.1)有1/2的概率选择在此处生成运算符
——2.2)否则,在此处继续延伸(生成左子树,右子树),直到达到第8个结点
3)树中节点数已经达到8及以上(到达第4层),就停止向下继续延伸,只生成当前节点的数值即可。
测试主要针对主函数模块和表达式生成模块进行,在单元测试过程中,均检查模块在执行过程中所有使用到的变量是否符合预定值,以及输出是否规范。
用例目的 | 输入 | 期待输出 | 实际输出 |
---|---|---|---|
零参数输入 | mathproblem.exe | 参数输入提示字符 | 参数输入提示字符 |
无效命令行输入 | mathproblem.exe -g -c -d | “Please input correct argument” | “Please input correct argument” |
正常输入 | mathproblem.exe -g -c | 包含1000个带答案的简单表达式的文件 | 包含1000个带答案的简单表达式的文件 |
测试困难模式 | mathproblem.exe -g -h | 包含1000个带答案的困难表达式的文件 | 包含1000个带答案的困难表达式的文件 |
这次结对项目的经历让我深切体会到团队协作的重要性。软件开发在于团队小而精,在只有两个人的团队中,交流方便,思路清晰,开发效率相当高。可见软件开发并不是人越多,干的越快,效果越好。
但是在两个人相互配合的过程中也遇到了一些问题,两个人的代码风格差别很大,所以必须依照一些编程规范才能不影响双方的配合。
小组成员在配合的过程中,也遇到了下面这些由于编程经验不足造成的问题:
1)变量命名不规范:命名过于精简,造成语意不明
2)if语句嵌套使用过多,会影响程序的可读性,使程序逻辑混乱。
3)代码注释过少会给读者带来很大困难。