105&250-高级软件工程2017第3次作业
小组成员
2017282110250 王婷婷
2017202110105 张芷祎
github地址
https://github.com/setezzy/Calculator_GUI
PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 5 | 5 |
· Estimate | · 估计这个任务需要多少时间 | 5 | 5 |
Development | 开发 | 1390 | 1750 |
· Analysis | · 需求分析 (包括学习新技术) | 120 | 120 |
· Design Spec | · 生成设计文档 | 60 | 60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 60 | 40 |
· Design | · 具体设计 | 120 | 120 |
· Coding | · 具体编码 | 600 | 900 |
· Code Review | · 代码复审 | 200 | 240 |
· Test | · 测试(自我测试,修改代码,提交修改) | 200 | 240 |
Reporting | 报告 | 250 | 250 |
· Test Report | · 测试报告 | 120 | 120 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 120 | 120 |
合计 | 1645 | 2005 |
需求分析
目标
本软件的主要目标如下:
1.提供一个四则运算测试系统,出一定量的四则运算习题给小学生练习或测试
2.方便教师或家长督促学生学习,帮助小学生巩固知识
用户特点
本系统最终用户为在校小学生/学生家长/教师。小学生利用该系统进行基本的整数、分数混合四则运算,暂不考虑负数计算;学生家长或老师能利用该系统给学生出题以便测试小学生学习情况。
假定和约束
用户权限:本系统考虑给用户单独使用,故未设置访问权限
开发语言:Java
开发期限:2周
需求规定
1.对功能的规定
a. 菜单选择功能。用户点击不同菜单项可选择退出系统,切换语言以及查看使用说明
b. 出题功能。用户输入的题目数量,系统显示对应的四则运算习题
c. 计时功能。计时非强制性,用户可点击开始计时按钮开启测试模式,在规定时间内完成答题;用户也可直接开始答题,作为练习
d. 正误判断功能。要求用户提交答案后进行正误判断,并给出答题正确率
e. 记录历史答题数量功能。要求能退出程序再启动时能增量显示用户历史答题正确数目和错误数目
f. 判断用户输入。用户输入答案时判断其输入答案是否有效:答案不为空,且不是包含字母或汉字等非法字符的字符串。
2.对性能的规定
a. 性能稳定,不出现显示错误、处理错误、答案错误的情况
b. 本软件可在任意系统下运行
3.运行环境规定
1.设备:笔记本/台式电脑
2.接口:无
功能图
详细设计
设计思路
1.系统后台功能
在我的第二次作业中已描述过生成算式以及算式计算的设计思路,本系统以上一次控制台程序为基础进行扩展,故在此不作赘述。
2.用户交互功能
考虑到时间因素,和同伴商量后决定用简单稳定的Java Swing做界面。
a. 多语言:最开始拿到该需求的时候的确没想到把不同语言的语句抽出来作为资源文件,所以尝试了一下用户选择一种语言后就重载面板,但是写了一段后觉得这种实现方式下代码冗余,而且语言模块与其他模块不能做到松耦合,不方便后续扩展。后面经老师提醒,学习了参考博客里其他同学的做法,与同伴讨论过后决定将语言都写到txt中作为资源文件,从而进行调用和显示。
b. 计时功能: 计时功能比较容易实现,具体思路为继承Runnable接口开启一个计时线程,当秒计数到60,向分钟进位,同时秒数归零,同样地,当分钟计数到60,向小时进位,同时分钟数归零。用户点击计时按钮时开启线程,在label上显示时间,之后线程暂停1秒再循环执行,达到动态显示时间效果。后期我们扩展了需求,当用户点击计时按钮则进入自测模式,到规定时间后系统弹出Message Dialog,提示用户时间已到,用户可选择继续答题或离开(目前将每道题的答题时间设定为50秒,在开始界面的使用帮助中说明)。
c. 历史对错题量记录功能: 增量计算用户每次做对的题目数量和做错的题目数量,写入文件,用户进入答题界面时从文件中读取该数量。
d. 错题导出功能:该功能为我们的扩展功能。用户提交答案后,可以选择导出做错的习题到txt文件。实现方式为:写文件时先获取当前日期及时间,作为区分不同次错题记录的标记;做错的题目用wrongQues[]保存,遍历数组并写入文件,方便用户后期查看。
e. 用户输入合法性判断: 该功能为我们的扩展功能。用户提交答案时,若检测到答案域为空,弹出消息框提示用户输入答案;若检测到答案包含非法字符(字母,汉字等),将会清空答案域,弹出消息框提示用户重新输入。
设计实现
程序流图
类图
关键函数流程图
对于生成和计算算式函数,在此不再说明。这里给出读取语言资源文件函数和提交答案后的判断函数流程图。
代码说明
语言切换功能
语言切换通过读取语言资源文件实现。默认界面为简体中文,可切换成繁体中文和英文。考虑到最后程序能在任意平台运行运行,将资源文件放在了class文件目录下的resources文件夹,通过getClass().getResourceAsStream(path)方式获得文件,这样能保证打包成jar运行时仍能读取到资源文件。
public String readTxtLine(int language, int lineNo) {
String txtPath;
if (language==TRADITIONAL_CHINESE){
txtPath="/main/resources/traditional.txt";
}
else if (language==ENGLISH) {
txtPath="/main/resources/english.txt";
}
else txtPath="/main/resources/simplified.txt" ;
String line = "";
String encoding="UTF-8";
try {
InputStream is=this.getClass().getResourceAsStream(txtPath);
reader=new BufferedReader(new InputStreamReader(is,encoding));
int i = 0;
while (i < lineNo) {
line = reader.readLine();
i++;
}
reader.close();
} catch (Exception e) {
// TODO: handle exception
}
return line;
}
增量保存用户对错题数功能
通过文件读写方式实现,历史记录存在historyCount.txt中:
//读取
try{
//定义路径为jar包所在路径
String root=System.getProperty("user.dir");
String path=root+"//historyCount.txt";
File file2=new File(path);
reader = new BufferedReader(new FileReader(file2));
rRead=reader.readLine();
wRead=reader.readLine();
reader.close();
}catch(IOException e){
e.printStackTrace();
}
if(rRead==null&&wRead==null){
rRead="0";
wRead="0";
}
rLabel.setText(readTxtLine(language,CORRECT_QUANTITY)+rRead);
wLabel.setText(readTxtLine(language,ERROR_QUANTITY)+wRead);
//写入
rightCount=rightCount+Integer.parseInt(rRead);
wrongCount=wrongCount+Integer.parseInt(wRead);
Integer right=new Integer(rightCount);
Integer wrong=new Integer(wrongCount);
try {
String root=System.getProperty("user.dir");
String path=root+"//historyCount.txt";
File file=new File(path);
writer = new BufferedWriter(new FileWriter(file));
writer.write(right.toString());
writer.newLine();
writer.write(wrong.toString());
writer.close();
}catch (IOException ie){
ie.printStackTrace();
}
错题导出功能
//导出错误题目
outQsButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String root=System.getProperty("user.dir");
String path=root+"//wrongQues.txt";
File file=new File(path);
try {
OutputStream os=new FileOutputStream(file,true);
SimpleDateFormat date=new SimpleDateFormat("yyyy-MM-dd HH:MM:SS");
os.write('\r');
os.write('\n');
os.write(('['+date.format(new Date())+']').getBytes()); //先写一行当前日期和时间
os.write('\r');
os.write('\n');
for(int i=0;i<wrongQues.length;i++){
os.write(wrongQues[i].getBytes());
os.write('\r');
os.write('\n');
}
os.close();
JOptionPane.showMessageDialog(null,readTxtLine(language,CORRECT_EXPORT));
} catch (FileNotFoundException e1) {
JOptionPane.showMessageDialog(null,readTxtLine(language,WRONG_EXPORT));
} catch (IOException e1) {
JOptionPane.showMessageDialog(null,readTxtLine(language,WRONG_EXPORT));
}
}
});
计时功能
private class MyRunable implements Runnable{
private int hour = 0;
private int min = 0;
private int sec = 0;
private NumberFormat format = NumberFormat.getInstance(); //将数字格式化处理
private String getTime(){
++sec;
if(sec == 60) {
++min;
sec = 0;
}
if(min == 60) {
++hour;
min = 0;
}
return showTime();
}
private String showTime(){
//时间显示形式
return format.format(hour)+":"+format.format(min)+":"+format.format(sec);
}
@Override
public void run() {
format.setMinimumIntegerDigits(2);
format.setGroupingUsed(false);
while(true) {
if(rootPaneCheckingEnabled) {
if(isRun) { //若点击计时按钮
getTime();
timeLabel.setText(showTime());
}
}
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
}
}
}
}
用户输入判断功能
for(i=0;i<mainForm.num;i++){
ans[i] = Simplify.gcd(NewCalculate.newcalculate(ques[i]));
if (ansField[i].getText().equals("")) {
//题目未答完时弹出提示
JOptionPane.showMessageDialog(null, readTxtLine(language,WAITING_FOR_INPUT) + i + readTxtLine(language,QUESTION_NUMBER),
readTxtLine(language,WRONG), JOptionPane.ERROR_MESSAGE);
break;
}
//输入包含其他字符时弹出提示
else if(!ansField[i].getText().matches("^[0-9/]+$")){
ansField[i].setText(null);
JOptionPane.showMessageDialog(null, readTxtLine(language,WAITING_FOR_INPUT) + i + readTxtLine(language,QUESTION_NUMBER),
readTxtLine(language,WRONG), JOptionPane.ERROR_MESSAGE);
break;
}
//显示判断结果
else{
if(ansField[i].getText().equals(ans[i])) { //答案正确
checkLabel[i].setText(readTxtLine(language,CORRECT));
ansLabel[i].setText(":)");
rightCount++;
}
else{ //答案错误
checkLabel[i].setText(readTxtLine(language,WRONG));
ansLabel[i].setText(ans[i]);
wrongCount++;
wrongQues[j]=ques[i];
j++;
}
}
}
测试运行
由于核心代码未做改动,所以单元测试的测试用例运行情况与第二次作业相同,测试用例及覆盖率在第二次作业中有细节描述。
基本功能运行界面
以简体中文和英文为例,开始界面如下:
下拉菜单栏:
使用说明:
四则运算界面(以中文为例,展示了提交答案后的界面),最终结果统计了用户用时及正确率:
以生成一道题为例,当用户所用时间超过我们规定的用时(50s),弹出消息窗。用户选择"是"则继续计时,按原有流程进行,选择"否"则退出程序:
用户提交答案后点击导出错题,以导出时间作为每次导出的tag:
模拟用户可能出现的输入错误进行测试
用户输入想要完成的习题数目,当输入为空或输入数字过大时,弹出警告:
用户未答完题就提交答案时,四则运算系处理如下:
用户输入非法字符串时,系统会弹出警告并清空该答案域,要求用户重新输入答案:
用户未完成习题就想导出错题:
合作情况
王婷婷
这次结对编程,我非常感谢我的小伙伴,因为运算的核心代码用的是芷祎的,所以我们项目的开始阶段是芷祎在写代码,我在一边看着,从她身上我也学到了很多。
只要你可以保住本心,
看得出对错,
辩得出真假,
又有什么难关是熬不过去的。
就像雪越下越大,
越下越猛,
可终究是会停的
感触比较深的一件事是,我们一起讨论多语言的问题,比较哪一种方法更好。我们参考了一些博客,由于整个窗体显示汉字的地方不多,用哪一种方法都觉得不太好。我说:“不管用啥方法,先把功能实现出来就好。”小伙伴直接否定了说:“那可不行”。就这一句话让我对小伙伴肃然起敬,这不仅仅是一次作业,更是对自己的一次磨砺,怎么能投机取巧呢。最后我们实现是多语言的主要办法是读取txt文档,实现多语言的切换,这样在后期功能扩展时只需修改资源文件,而代码改动量很少,实现模块间的松耦合。
张芷祎
这是我第一次用结对编程的模式开展项目,我觉得的确是起到了1+1>2的效果。
在前期小伙伴充当领航员角色,我则负责驾驶员角色,后期角色调换。在项目步入尾声的时候,为了让系统更加完善,我们互相会提出新的功能需求,轮流编程实现。也非常感谢小伙伴能和我相互督促,在提出修改意见时能快速实现功能需求,产生分歧也很少。
结对编程的好处是效率会大大提高,对于某些问题同伴往往会提供一些不同的思路,让自己有一拍桌子恍然大悟的感觉。在代码复审阶段结对编程带来的效益也是非常大的,代码复审目的就是找出错误、提高代码质量,而两人合作检查代码速度更快,也能摆脱个人思维限制,就像一位老师调侃说的“自己为什么老是检查不出错误,别人一看就能指出来呢?——就是对自己写的代码太自信呗!” 就比如我前几天找一个数组越界错误,我说这代码逻辑没问题呀,然而小伙伴很迅速地就指出来了:“你是不是前面初始化时数组长度不够?” 我:“??!!!”
项目小结
王婷婷
很荣幸有这一次结对编程的机会,让我更加深入的了解了软件工程,在于小伙伴芷祎的合作中,我学会了很多东西,包括软件开发过程中代码管理方面的知识,以及面对难题,沉下心思考并想办法克服困难解决问题的能力。我们此次项目的不完善之处在于界面有点丑,我相信它会变得更漂亮,只是时间紧迫。
中期汇报进度的时候,发现群里的小伙伴都是蛮厉害的,其中周明浩同学那一组就获得了老师们的欣赏。我也特别佩服他们这么短的时间把系统做的很流畅并且兼容性良好。后面我会多和小伙伴交流和学习,争取自己也得到提高。
张芷祎
做这个项目让我收获很多,就自学能力、思维能力而言都有提高吧。我们其实完成基本功能比较快,就是后面想尽可能从用户角度考虑一些需求,然后就修修补补,期间当然免不了出现不符合自己预期的结果,但也能耐着性子继续思考继续修复错误了。
在做项目期间遇到之前很少注意的问题就是如何在程序打包成jar之后仍能读取程序需要的资源文件?想到这个问题后就知道肯定不能以普通形式的文件url来定位资源文件,找资料之后发现Object类中有一个getClass()方法,能够在程序运行时动态获取资源文件。不过可能因为jar是只读文件,想要写文件的话还是得放在jar包外面。
总之与小伙伴的结对编程过程使我受益匪浅,开发效率大大提高。不过时间比较紧,有些语法没有掌握透彻,还有例如编程思想、程序设计方法等等都是在做项目时要非常重视的,这些知识点自己也需要多多学习。