这个作业属于哪个课程 | 2302软件工程社区 |
---|---|
这个作业要求在哪里 | 软件工程第二次作业–文件读取 |
这个作业的目标 | 完成对世界游泳锦标赛跳水项目相关数据的收集,并实现一个能够对赛事数据进行统计的控制台程序 |
其他参考文献 | 工程师的能力评估和发展、源代码管理、单元测试和回归测试 |
文章目录
1.Gitcode项目地址
2.PSP表格
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 25 |
• Estimate | • 估计这个任务需要多少时间 | 30 | 15 |
Development | 开发 | 600 | 720 |
• Analysis | • 需求分析(包括学习新技术) | 200 | 180 |
• Design Spec | • 生成设计文档 | 60 | 45 |
• Design Review | • 设计复审 | 40 | 30 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 60 | 40 |
• Design | • 具体设计 | 90 | 80 |
• Coding | • 具体编码 | 420 | 360 |
• Code Review | • 代码复审 | 120 | 100 |
• Test | • 测试(自我测试,修改代码,提交修改) | 120 | 90 |
Reporting | 报告 | 180 | 180 |
• Test Repor | • 测试报告 | 80 | 90 |
• Size Measurement | • 计算工作量 | 60 | 40 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 60 | 60 |
合计 | 2150 | 2055 |
3.解题思路描述
3.1json文件的解析与读取
在进行实际开发的过程中,我所使用的是助教提供的json文件,其中存放运动员信息的json文件结构如图所示:
通过该结构,我初步锁定了我所需要的数据,其中红框区域内的CountryName、Gender、PreferredLastName、PreferredFirstName组成了功能1:输出所有选手信息 所需的完整信息;经过分析可以将CountryName单独提取,再从Participations内提取性别和姓名信息。
然后是具体到单项运动项目,以女子一米跳板为例,通过对数据的分析,我在图上圈出了了能够表示重要信息的属性及其值,需要用到的有效数据有DisciplineName、TotalPoints、DivePoints、Rank以及FullName,通过读取这几项数据并且规范化最后输出的格式,就能实现对赛事结果的正确打印。这也是我读取数据的代码的设计思路。
3.2正确去读取所需数据的思路
在分析数据结构的过程中我发现数据量很大,但是实现功能只需要特定项,也就是需要对数据进行逐层剖析,去定位所需要的数据,因此我准备通过遍历匹配的方式进行数据定位。
4.接口设计和实现过程
4.1接口设计
由程序功能的设计需求可知,程序需要输出所有选手信息和决赛每个运动项目结果,因此我在Lib类中封装实现两种功能各自所需的静态方法,方便主函数直接通过接口进行调用以实现对应功能。其中包括了controller()、readinput()、getjsonpath()、readJsonFile()、outputAthletesInfo()、outputEventResult()这五个函数(形参已作省略)。这些函数之间的关系以及如何协作实现功能如以下流程图所示:
- 首先需要设计并实现outputAthletesInfo()静态方法,用于实现读取选手信息的功能,通过导包阿里巴巴的fastjson2,用其中的相关方法处理json文件,并根据题目所给的数据格式进行打印输出。
- 然后要设计第二个功能,输出每个运动项目的结果。这两个功能的设计过程有相似之处,也就是对于json数据的解析,由于所给出的json文件中数据有多层嵌套,数组嵌套元素,元素又嵌套着数组,所以分析数据的过程就像一个剥洋葱的过程,需要一层一层地解析,以获取对应的数据,并且在多层嵌套的循环中需要在正确的位置完成数据在文件中的写入工作,解析数据是完成算法的关键之处。
- 由于功能要求实现读取多行input.txt中的指令并逐条实现,因此该算法的独到之处就在于我设计了一个专门用于读取指令的函数,并且将正确的指令和各指令所对应的json文件路径二者以键值对的方式封装在一个HashMap数组中,以便于在程序执行时迅速判断input中指令的格式正确与否以及迅速定位目标路径,进行数据提取输出。
4.2实现过程
- 先完成阿里巴巴fastjson2的导包工作,后续将调用其中JSONObject、JSONArray类下的相关方法进行json数据的解析读取。
- 然后再实现readinput()函数,以流的形式读取input.txt中的内容,转换成字符串数组,方便多条指令的格式判断以及顺序执行。
- 接着要实现getjsonpath()和readJsonFile()两个函数,根据需要获取到索要解析的json文件路径,可以将里面的数据转换成json字符串形式。需要注意的是,不能用常规操作文件的方法来读取.jar中的资源文件,但可以通过Class类的getResourceAsStream()方法来获取,这样才能使得打包成jar包后能正确定位目标文件,所以要通过以下代码创建输入流,就不会出现找不到文件路径的问题
InputStream is = this.getClass().getClassLoader().getResourceAsStream(fileName);
- 以上操作完成后,就可以在outputAthletesInfo()、outputEventResult()中多次使用所导包的fastjson2下的相关函数,通过循环嵌套逐层获取所需的数据并输出到output.txt中。
- 最后实现controller()函数,在主函数中只需要调用controller()函数,就可以自动识别input.txt中的指令、判断指令正确与否、按顺序实现正确指令所对应的功能,以及按要求对错误的指令进行报错输出。
5.关键代码展示
- 实现打印所有选手信息的函数
public static void outputAthletesInfo(){
try {
FileOutputStream fos = new FileOutputStream("src\\data\\output");
JSONArray jsonArray = JSONArray.parseArray(readJsonFile("src\\data\\athletes.json"));
for (int i = 0; i < jsonArray.size(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
//获取国家名
String CountryName = jsonObject.getString("CountryName");
//去获取选手的信息
JSONArray Participations = jsonObject.getJSONArray("Participations");
for (int j = 0; j < Participations.size(); j++) {
String fullname = Participations.getJSONObject(j).getString("PreferredFirstName") + " " + Participations.getJSONObject(j).getString("PreferredLastName");
fos.write(("FullName:"+fullname+"\r\n").getBytes());
String gender = (Participations.getJSONObject(j).getInteger("Gender") == 1 ? "Male" : "Female");
fos.write(("Gender:" + gender+"\r\n").getBytes());
fos.write(("Country:" + CountryName+"\r\n").getBytes());
fos.write(("-----"+"\r\n").getBytes());
}
}
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
- 实现输出决赛每个运动项目结果的函数
public void outputEventResult(String command,String filename){
try {
FileOutputStream fos=new FileOutputStream(filename,true);
//获取项目信息
JSONObject jsonObject=JSONObject.parseObject(readJsonFile(getjsonpath(command)));
//开始分解项目中的各项具体信息
JSONArray Heats=jsonObject.getJSONArray("Heats");
//直接取第一项,是决赛的数据
JSONObject jsonObject1=Heats.getJSONObject(0);
JSONArray Results=jsonObject1.getJSONArray("Results");
//获取各个选手的得分情况
for (int j = 0; j < Results.size(); j++) {
JSONObject selfresult=Results.getJSONObject(j);
String fullname=selfresult.getString("FullName");//名字
List<String> doublename=new ArrayList<>();//记录双人赛两人名字
String Doublename="";//记录最终输出的双人名格式
String rank=selfresult.getString("Rank");
String totalpoints=selfresult.getString("TotalPoints");//总分
String detailscore="";
//判断双人赛
if(selfresult.containsKey("Competitors")&&selfresult.get("Competitors")!=null){
JSONArray competitors=selfresult.getJSONArray("Competitors");
for (int n = 0; n < competitors.size(); n++) {
JSONObject eachcompetitor=competitors.getJSONObject(n);
String eachname=eachcompetitor.getString("LastName")+" "+eachcompetitor.getString("FirstName");
doublename.add(eachname);
}
Collections.sort(doublename);//将选手名字从小到大排
Doublename="'"+ doublename.get(0)+" & "+doublename.get(1)+"'";
}
//接着去获取单次得分
JSONArray dives=selfresult.getJSONArray("Dives");
for (int k = 0; k < dives.size(); k++) {
JSONObject eachscore=dives.getJSONObject(k);
String EachScore=eachscore.getString("DivePoints");
if(k!=dives.size()-1){
detailscore=detailscore+EachScore+"+";
}
else{
detailscore=detailscore+EachScore;
}
}
//对获取的数据字符串进行拼接以实现输出正确格式的结果,分单人和双人项目
if(selfresult.containsKey("Competitors")&&selfresult.get("Competitors")!=null){
fos.write(("FullName:"+Doublename+"\r\n").getBytes());
fos.write(("Rank:"+rank+"\r\n").getBytes());
fos.write(("Score:"+detailscore+"="+totalpoints+"\r\n").getBytes());
fos.write(("-----"+"\r\n").getBytes());
}
else {
fos.write(("FullName:"+fullname+"\r\n").getBytes());
fos.write(("Rank:"+rank+"\r\n").getBytes());
fos.write(("Score:"+detailscore+"="+totalpoints+"\r\n").getBytes());
fos.write(("-----"+"\r\n").getBytes());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
6.性能改进
6.1需要改进的地方:
- 一开始编写程序的时候,我是用if-else if的形式去判断所读到的input中的指令,然后再定位目标json文件,再进行数据解析,这样子代码冗长且可读性差,也影响运行速度,因此需要改进对于指令的判断部分。
- 第二个需要改进的地方则是,我发现代码在打包成jar包后无法获取到同目录下json文件路径,需要进行改进。
- 另一个需要改进的地方是可以使用BufferdWriter包装FileWriter作输入流,提高运行速度。
6.2具体改进方案
- 首先针对更高效定位指令指向的json文件,可以利用HashMap的键值对特性,将正确指令字符串同所指向的json文件的路径一一对应后装入HashMap,便于更快定位。
static HashMap<String,String> command=new HashMap<>(){
{
put("result men 1m springboard","data/result/Men 1m Springboard.json");
...//此处作省略展示
put("result women 10m platform detail","data/result/Women 10m Synchronised.json");
}
};
- 然后是解决jar包无法找到json文件,通过Class类的getResourceAsStream()方法来解决问题
public String readJsonFile(String fileName) throws IOException{
InputStream is = this.getClass().getClassLoader().getResourceAsStream(fileName);
String jsonStr = "";
if(is.available()!=0){
BufferedReader in = new BufferedReader(new InputStreamReader(is, "UTF-8"));
StringBuffer buffer = new StringBuffer();
String line = "";
while ((line = in.readLine()) != null){
buffer.append(line);
}
jsonStr = buffer.toString();
return jsonStr;
}
else {
throw new FileNotFoundException(fileName+"文件不存在!");
}
}
- 最后是用BufferdWriter包装FileWriter作输入流
BufferedReader in = new BufferedReader(new InputStreamReader(is, "UTF-8"));
StringBuffer buffer = new StringBuffer();
7.单元测试
在测试过程中,我所使用的工具是IDEA编译器内的Juit,首先先通过maven依赖导包所需工具类
在导包完成后编写相关的测试函数,这里我对主函数的测试模拟了打包成jar包后与同目录下输入输出文本的交互情景(通过文件绝对路径进行模拟),而对于其他功能类函数的测试则编写了具体测试函数:
对主函数的测试函数:
import org.junit.jupiter.api.Test;
import static org.junit.Assert.*;
public class DWASearchTest {
@Test
//参数个数测试
void Test1_incorrect_parameters_num_1(){
DWASearch.main(new String[]{"C:\\Users\\youngz\\Desktop\\FINAL\\src\\input"});//参数个数错误
}
@Test
void Test1_correct_parameters_num_2(){
DWASearch.main(new String[]{"C:\\Users\\youngz\\Desktop\\FINAL\\src\\input","C:\\Users\\youngz\\Desktop\\FINAL\\src\\output"});//参数正确
}
@Test
void Test1_incorrect_parameters_num_3() {
DWASearch.main(new String[]{"C:\\Users\\youngz\\Desktop\\FINAL\\src\\input", "C:\\Users\\youngz\\Desktop\\FINAL\\src\\output", "output.txt"});//参数个数错误
}
//参数格式测试
@Test
void Test2_incorrect_parameters1() {
DWASearch.main(new String[]{"input", "output"});//格式错误
}
@Test
void Test2_incorrect_parameters2() {
DWASearch.main(new String[]{"input.txt", "output"});//格式错误
}
@Test
void Test2_incorrect_parameters3() {
DWASearch.main(new String[]{"input", "output.txt"});//格式错误
}
@Test
void Test2_incorrect_parameters4() {
DWASearch.main(new String[]{"1.txt", "2.txt"});//格式错误
}
@Test
void Test2_incorrect_parameters5() {
DWASearch.main(new String[]{"input.txt", "output.txt"});//格式错误
}
}
测试结果如下:
- Test1_incorrect_parameters_num_1()的测试结果:
可以看到当参数个数传递错误时,测试失败且方法覆盖率低,是因为参数错误导致程序无法顺利运行,提前结束,执行代码少,所以代码覆盖率也低。下方参数个数异常的测试方法也都测试失败以及有覆盖率低的特点。- Test1_incorrect_parameters_num_2()的测试结果:
在输入正确参数的情况下,测试通过且类的覆盖率达到百分百的同时也有较高的代码和方法覆盖率。
对Lib内封装的方法进行测试:
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.*;
public class LibTest {
@Test
void LibTest1() {
Lib lib=new Lib();
lib.readinput("C:\\Users\\youngz\\Desktop\\FINAL\\src\\input");
}
@Test
void LibTest2() {
Lib lib=new Lib();
lib.outputAthletesInfo("C:\\Users\\youngz\\Desktop\\FINAL\\src\\output");
}
@Test
void LibTest3() {
Lib lib=new Lib();
lib.readinput("C:\\Users\\youngz\\Desktop\\FINAL\\src\\input");
lib.outputAthletesInfo("C:\\Users\\youngz\\Desktop\\FINAL\\src\\output");
}
@Test
void LibTest4() {
Lib lib=new Lib();
lib.readinput("C:\\Users\\youngz\\Desktop\\FINAL\\src\\input");
lib.outputEventResult("result women 10m platform","C:\\Users\\youngz\\Desktop\\FINAL\\src\\output");
}
@Test
void Libtest5(){
Lib lib=new Lib();
List<String> commands =lib.readinput("C:\\Users\\youngz\\Desktop\\FINAL\\src\\input");
lib.controller(commands,"C:\\Users\\youngz\\Desktop\\FINAL\\src\\output");
}
}
测试结果如下:
各个测试函数在输入正确格式的参数后都测试成功,此处我没有运行主函数的测试,所以对于Lib的类覆盖应该是达到了100%,方法的覆盖率和代码覆盖率都较高,符合预期的结果。
8.异常处理
- 主要采用try…catch来进行异常处理,其中包括对文件的异常处理以及对数据流的异常处理,如下面输出选手信息的函数:
public static void outputAthletesInfo(){
try {
FileOutputStream fos = new FileOutputStream("src\\data\\output",true);
...//此处作省略展示
}
fos.close();
catch (Exception e) {
e.printStackTrace();
}
}
- 也有对于需要调用者自己处理的异常,通过抛出异常throws IOException的方式来返回给调用者,如下面获取json文件内数据的函数:
public String readJsonFile(String fileName) throws IOException{
InputStream is = this.getClass().getClassLoader().getResourceAsStream(fileName);
String jsonStr = "";
if(is.available()!=0){
...//此处作省略展示
}
else {
throw new FileNotFoundException(fileName+"文件不存在!");
}
}
9.心得体会
通过本次实践作业我学习到了很多以前没有接触过的知识,发现自己在很多知识分支有欠缺,特别是最后在将主程序打包成jar包并用cmd运行时总是出错,后来才知道是路径获取的问题,通过查阅CSDN上的资料才解决了问题。最重要的是学会了怎么分析json数据结构以及获取并处理json字符串,对java编程过程更为熟悉了,收获很大。