这个作业属于哪个课程 | 2302软件工程 |
---|---|
这个作业的要求在哪里 | 软件工程第二次作业–文件读取 |
这个作业的目标 | 完成对世界游泳锦标赛相关数据的收集,并实现一个能够对赛事数据进行统计的控制台程序 |
其他参考文献 | 《构建之法》《源代码管理》 《阿里巴巴代码规范》 |
文章目录
1. gitcode项目地址
2. PSP表格
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 35 |
* Estimate | 估计这个任务需要多少时间 | 20 | 25 |
Development | 开发 | 500 | 540 |
* Analysis | 需求分析 (包括学习新技术) | 120 | 150 |
* Design Spec | 生成设计文档 | 60 | 50 |
* Design Review | 设计复审 | 30 | 30 |
* Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 50 | 45 |
* Design | 具体设计 | 90 | 100 |
* Coding | 具体编码 | 300 | 320 |
* Code Review | 代码复审 | 60 | 50 |
* Test | 测试(自我测试,修改代码,提交修改) | 100 | 110 |
* Reporting | 报告 | 150 | 150 |
* Test Report | 测试报告 | 60 | 50 |
* Size Measurement | 计算工作量 | 60 | 50 |
* Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 60 | 60 |
合计 | 1690 | 1760 |
3. 解题思路描述
3.1 json文件的获取与解析
本次作业中我直接使用了助教提供的json文件,没有手动进行网络爬取。这里我将项目所需的所有json数据文件放到项目下,并进行了重命名以便后续的读取和解析操作。
- 对于功能一来说,我们需要获取所有运动员的信息,可以定位到athletes.json这个文件下。需要读取这个文件并解析这个文件,文件的结构如下图所示:
从json文件可以观察到,最外层是一个json数组,数组中的每个对象是每个国家的信息,其中键为CountryName对应的值是运动员的国家名称,再下一层可以看到键是Participations对应的值就是这个国家所有的参赛运动员组成的数组。对象中的PreferredFirstName键与PreferredFirstName键的值共同组成我们需要的Full Name信息。键是Gender的值对应运动员的性别,0为男,1为女,需要简单处理处理一下再输出。理清楚了文件结构和我们需要的信息后,就可以开始编码解析json数据了。
- 对于功能二来说,我们需要解析是的指令对应的json文件,如result women 1m springboard 指令,则会去加载解析项目目录src下的women_1m_springboard.json文件,该文件的结构如下:
可以观察到所有比赛 (包括初赛、半决赛、决赛) 的结果位于键为Heats的数组中,数组中是所有比赛的数据,其中键Name的值为Final的是总决赛的数据。定位到总决赛数据的对象后,可以观察到有键为Results的数组,这个数组就是总决赛的比赛结果数组。其中Rank对应的是选手的排名,FullName对应的是选手的全名,Dives对应的是得分情况的数组,数组中对象的DivePoint是单次得分情况。这些是我们需要获取并输出到文件中的数据。
在这两个功能的实现中,我使用谷歌的Gson库解析json文件,将json文件解析成json对象,然后将json对象转换成java的Bean对象,最后将java对象封装成List返回给调用者。
3.2 读取类路径下的文件
使用ClassLoader类加载器的getResourceAsStream来加载类路径下的文件,并把所有的命令以字符串集合的形式返回。
3.3 数据的封装
我在entity包下创建了Player和Result这两个实体类,分别用来封装运动员,决赛结果的信息。两个类都重写了对应的toString方法,可以输出题目要求格式的运动员和决赛结果的信息。
- 运动员类
//运动员
public class Player {
//选手全名
private String fullName;
//选手性别
private String gender;
//选手国籍
private String country;
public Player() {
}
....setter 和 getter省略
/**
* 以要求的格式重写toString方法
* @return 返回要求格式的字符串
*/
@Override
public String toString() {
return "Full Name:" + fullName + '\n' +
"Gender:" + gender + '\n' +
"Country:" + country + '\n';
}
}
- 决赛结果类:
//决赛结果
public class Result {
//选手全名
private String fullName;
//选手名次
private String finalRank;
//选手成绩
private List<String> finalScore;
....构造函数和 setter 和 getter省略
//将分数列表转化为需要的字符串
private String toScoreString(List<String> scoreList) {
if (scoreList == null) {
return "*\n";
}
BigDecimal totalScore = scoreList.stream()
.map(BigDecimal::new)
.reduce(BigDecimal.ZERO, BigDecimal::add);
String scoreString = scoreList.stream()
.collect(Collectors.joining(" + ", "", " = " + totalScore + "\n"));
return scoreString;
}
public String toString() {
return
"Full Name:" + fullName + "\n" +
"Rank:" + finalRank + "\n" +
"Score=" + toScoreString(finalScore);
}
3.4 项目打包为jar包
直接使用idea来打包项目。先配置一下Aritifacts,设置好打包方式、包名、主类,以及jar包的输出路径。通过Build->Build Artifacts->Build进行打包。
4. 接口设计和实现过程
4.1 在Lib类中主要有三个接口
- public List<Player>getAllPlayers();
功能:负责获得所有的运动员信息,并且封装成含有Player对象的集合。
实现:通过JsonParseUtils工具类的parsePlayers方法获得Player对象的集合并返回,JsonParseUtils中根据传入的解析后的json文件的根对象JsonElement,根据前面分析的层级结构使用Gson解析出需要的数据,并做相应的封装。
- public List<Result>getFinalResults(String jsonFilePath);
功能:负责获得所有的决赛信息,并且封装成含有Result对象的集合
实现: 同理,该接口通过JsonParseUtils工具类的parseResults方法获得Result对象的集合并返回,JsonParseUtils中根据传入的解析后的json文件的根对象JsonElement来逐级提取提取需要的信息,并做相应的封装。
- public void executeCommand(String command, String outputFile);
功能:执行传入的命令和要输出的文件的路径,将执行命令的结果写入到对应的文件中。
实现:根据传入的命令进入不同的分支,执行不同的功能,调用上面不同的接口来获取数据,最后将数据以要求的格式的写入到对应路径下的文件中。
代码的独到之处就是使用了Gson来解析json文件,逐层提取所需信息并封装成Bean对象,最后转化好格式后向指定文件输出。
4.2 调用层级图
5. 关键代码展示
5.1获得全部运动员信息的接口代码展示:
/**
* 解析json数据封装成对象返回
* @param jsonData Jason的Data
* @return 返回运动员列表
*/
public static List<Player> parsePlayers(JsonElement jsonData) {
if (jsonData == null) return null;
List<Player> result = new ArrayList<>();
JsonArray jsonArray = jsonData.getAsJsonArray();
for (JsonElement jsonElement : jsonArray) {
JsonObject countryWithPlayers = jsonElement.getAsJsonObject();
String country = countryWithPlayers.get("CountryName").getAsString();
JsonArray players = countryWithPlayers.getAsJsonArray("Participations");
for (JsonElement playerElement : players) {
JsonObject playerJson = playerElement.getAsJsonObject();
Player player = new Player();
String lastName = playerJson.get("PreferredLastName").getAsString();
String firstName = playerJson.get("PreferredFirstName").getAsString();
String fullName = lastName + " " + firstName;
int gender = playerJson.get("Gender").getAsInt();
player.setCountry(country);
player.setFullName(fullName);
player.setGender(gender == 0 ? "Male" : "Female");
result.add(player);
}
}
return result;
}
5.1 获得所有决赛信息的接口代码展示:
/**
* 获取比赛结果
* @param jsonData 解析后的第一个元素
* @return 返回结果列表
*/
public static List<Result> parseFinalResult(JsonElement jsonData) {
if (jsonData == null) return null;
JsonArray heats = jsonData.getAsJsonObject().getAsJsonArray("Heats");
List<Result> resultList = new ArrayList<>();
for (JsonElement heat : heats) {
JsonObject eventResults = heat.getAsJsonObject();
//说明是 FinalResult
if ("Final".equals(eventResults.get("Name").getAsString())){
JsonArray resultData = eventResults.getAsJsonArray("Results");
for (JsonElement element : resultData) {
JsonObject player = element.getAsJsonObject();
JsonArray dives = player.getAsJsonArray("Dives");
ArrayList<String> scoreList = new ArrayList<>();
for (JsonElement dive : dives) {
JsonObject diveObj = dive.getAsJsonObject();
scoreList.add(diveObj.get("DivePoints").getAsString());
}
String fullName = player.get("FullName").getAsString();
Result result = new Result(fullName, player.get("Rank").getAsString(), scoreList);
resultList.add(result);
}
}
}
return resultList;
}
6. 性能改进
读取和解析json文件是一个很耗时的IO操作,这里为了简单,我直接使用了内存中的HashMap来缓存。当执行命令的时候,先以对应命令查询缓存,如果缓存命中,有对应的content则直接返回缓存中的数据,如果没有命中再去读取和解析json文件,并把解析后的内容设置到缓存中,大大减少了IO操作的耗时。
7. 单元测试
使用junit来进行测试,编写测试类,设计测试单元,传入不同的参数,运行测试单元。
主要测试的就是Lib接口下的接口/功能,包括获取所有运动员信息、获取所有决赛的结果信息、执行不同的指令,向指定文件中输出结果的功能。测试类如下:
当运行测试test01时,测试执行指令功能,向对应文件输出运动员信息,覆盖率如下:
当运行整个测试类( 10个测试用例 )的时候可以观察到,代码的覆盖率很高,符合要求:
8. 异常处理
主要采用try…catch来进行异常处理,其中包括对文件的异常处理以及对数据流的异常处理。出现异常时,会在控制台有相应的错误提示信息,如读取的文件不存在,数据文件不存在,读取时出现错误等信息。
9. 心得体会
通过这次作业,我不仅学会了如何解析复杂的JSON数据,还深入了解了Git和GitCode仓库在代码管理中的重要性。在处理复杂的JSON结构时,我需要仔细分析数据的层级关系和逻辑,确保能够正确地提取所需信息。在这个过程中,我逐渐掌握了处理复杂JSON数据的技巧,同时也意识到了规划和设计的重要性。
积极运用Git来管理代码也是这次作业的一部分。将代码提交到GitCode仓库不仅方便自己管理代码版本,还可以随时回溯之前的版本以及对比代码的变化,保证代码的可追溯性和可靠性。通过Git的使用,我也更加注重了代码的整洁性和规范性,因为良好的代码结构和规范能够提高代码的可读性和可维护性。
另外,我还学会了使用JUnit进行测试,这样可以帮助我验证代码的正确性,确保代码的质量和稳定性。通过编写测试用例和运行单元测试,我可以更加自信地对代码进行修改和重构。这也使得我的代码更加可靠,也为未来的项目开发奠定了良好的基础。
总的来说,通过这次作业,我不仅学会了处理复杂JSON数据,还深入了解了Git的使用和代码管理的重要性,同时也学会了使用JUnit进行测试,这些技能对我未来的项目开发都将大有裨益。同时,我也意识到在开始项目之前,应该对整体流程和所需工具有一个清晰的规划,这样可以避免后期频繁调整和修改代码带来的麻烦。及时记录和总结遇到的问题和解决方案也很重要,方便日后回顾和学习经验。