文章目录
这个作业属于哪个课程 | https://bbs.csdn.net/forums/ssynkqtd_06?typeId=6299198 |
---|---|
这个作业要求在哪里 | https://bbs.csdn.net/topics/618087255 |
这个作业的目标 | 完成对世界游泳锦标赛跳水项目相关数据的收集,并实现一个能够对赛事数据进行统计的控制台程序 |
其他参考文献 | 构建之法、单元测试教程、Java开发手册、菜鸟教程 |
Gitcode项目地址
https://gitcode.net/CEOLucifer/project-java
PSP表格
阶段 | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|
计划 | ||
· 估计这个任务需要多少时间 | 20 | 20 |
开发 | ||
· 需求分析(包括学习新技术) | 180 | 200 |
· 生成设计文档 | 30 | 20 |
· 设计复审 | 30 | 40 |
· 代码规范 | 20 | 30 |
· 具体设计 | 60 | 60 |
· 具体代码 | 480 | 800 |
· 代码复审 | 30 | 60 |
· 测试(自我测试,修改代码,提交修改) | 250 | 600 |
报告 | ||
· 测试报告 | 40 | 60 |
· 计算工作量 | 25 | 30 |
· 事后总结,并提出过程改进计划 | 120 | 120 |
合计 | 1285 | 2040 |
解题思路描述
获取数据
数据使用了助教提供的相关数据
分析json结构,编写对应数据类
athletes.json
是一个数组,每个元素对应一条记录,这个记录包含国家、这个国家的选手数组。
根据这个文件,我们可以获取所有选手的信息。
而这些信息正是功能1实现的基础。
根据问题域和系统责任,得到对应类图如下:
有了这些类,我可以很轻松地将这个json文件反序列化出对应的对象,并根据这些对象进行处理。
event.json
是一个对象。Sports字段是一个数组,而这个数组只有一个元素,这个元素的DisciplineList字段是一个数组,其每一个元素就是一个项目,如Women 1m Springboard、Women 3m Springboard、Women 10m Platform等等,包含了一个项目的重要数据,如Id(用于找到项目具体数据的对应的json文件),还有HeatList(一个数组,包含这个项目有哪些比赛阶段)。
综合分析来看,似乎只有DisciplineList是有用的数据,而其他的数据都无关紧要,并不符合需求,因此只要序列化出这个DisciplineList即可。
根据问题域和系统责任,得到对应类图如下:
results文件夹下的各个.json文件
是一个对象,表示一个项目的具体数据。
文件名正好是对应项目的Id。
- DisciplineName是项目名称。
- Heats是一个数组,每个元素表示这个项目的一个阶段。
- PhaseName是阶段名。
- Results是一个数组,每个元素是一个选手的结果。
- TotalPoints是总分
- Rank是排名
- FullName是选手姓名
- Dives是一个数组,每个元素是一次跳水的数据
- DivePoints是本次跳水的分数
- JudgesScores是数组,每个元素是一个裁判的评分。这个数据其实是没有用处的,应该忽略。
这些数据是功能2的基础。
根据问题域和系统责任,得到对应类图如下:
接口设计和实现过程
根据问题域和系统责任,得到核心模块Core的类图如下:
该图展示了Core对外提供的接口。
-
OutputAllPlayers()
- 读取Country和Participation数据;
- 合成字符串:遍历participants,获取每一个选手的信息,按照输出要求,合成字符串;
- 写入到output.txt。
-
OutputFinalResultOf()
- 获取相应项目数据;
- 查找Finals数据;
- 合成字符串:读取每一个Result,按照输出要求,合成字符串;
- 写入到output.txt。
关键代码展示
/**
* 输出所有选手信息到output.txt
*/
static public void OutputAllPlayers() {
if (isOutputAllPlayersStringCached) // 如果有缓存,输出
{
FileHelper.WriteAllText(Config.instance.outputPath, outputAllPlayersCacheString);
return;
}
LoadCountries();
LoadParticipations();
String content = "";
for (Participation participation : participations) {
content += "Full Name:" + participation.PreferredFirstName + " " +
participation.PreferredLastName + "\n";
if (participation.Gender == 0)
content += "Gender:Male\n";
else
content += "Gender:Female\n";
content += "Country:" + participation.NAT + "\n";
content += "-----\n";
}
FileHelper.WriteAllText(Config.instance.outputPath, content);
// 缓存
outputAllPlayersCacheString = content;
isOutputAllPlayersStringCached = true;
}
/**
* 输出指定项目的决赛结果
*
* @param event_name
* @return 若有Final结果,返回true;若没有该项目,或者该项目没有Final结果,返回false。
*/
static public boolean OutputFinalResultOf(String event_name) {
LoadDisciplineMap();
if (!disciplineMap.containsKey(event_name)) // 没有这个项目
{
OutputNA();
return false;
}
LoadDiscipline2(event_name);
// 读取比赛结果文件
Discipline2 discipline2 = discipline2Map.get(event_name);
// 获取Finals结果们
List<Result> results = null;
for (Heat2 each : discipline2.Heats) {
if (each.PhaseName.equals("Finals")) {
results = each.Results;
break;
}
}
// 主要的输出逻辑
String content = "";
for (Result result : results) {
content += "Full Name:" + result.FullName + "\n";
content += "Rank:" + result.Rank + "\n";
String score_str = "Score:";
int length = result.Dives.size();
for (int i = 0; i < length - 1; ++i)
score_str += result.Dives.get(i).DivePoints + " + ";
score_str += result.Dives.get(length - 1).DivePoints + " = " + result.TotalPoints + "\n";
content += score_str;
content += "-----\n";
}
FileHelper.WriteAllText(Config.instance.outputPath, content);
return true;
}
性能优化
优化前
存在以下问题,会对程序的性能造成显著影响:
- 完全相同的命令,结果一般是一样的。但是,程序在每次处理他们的时候完全是从零开始的,存在重复的工作。
- 读取文件时,并没有缓存读取结果,一个文件可能被重复读取了好几次。
优化过程和思路
对于一个命令,都会有对应输出,且命令和输出时一一对应的。
基于这个原理,用一个字典,键表示命令字符串,值表示输出字符串。当存在遇到已经处理过的命令时,程序直接取出结果作为输出;否则,缓存该执行结果于字典。
但是一般结果内存占用比较大,属于空间换时间。
优化后
优化前后的性能对比:
- 处理1000次players命令
@Test
public void TestOutputAllPlayers_1000() {
for (int i = 0; i < 1000; ++i)
Core.OutputAllPlayers();
}
-
优化前
用时931ms。
-
优化后
用时282ms。
- 处理重复的result命令
@Test
public void TestRepeatCommand() {
for (int i = 0; i < 2000; ++i)
Core.OutputFinalResultOf("women 1m springboard");
for (int i = 0; i < 2000; ++i)
Core.OutputFinalResultOf("men 1m springboard");
}
- 优化前
用时5063ms
- 优化后
用时680ms
单元测试
public class CoreTest {
@Before
public void BeforeEach() {
Config.LoadConfig();
FileHelper.DeleteThenCreateFile(Config.instance.outputPath);
}
@Test
public void TestOutputAllPlayers() {
Core.OutputAllPlayers();
}
@Test
public void TestOutputAllPlayers_1000() {
for (int i = 0; i < 1000; ++i)
Core.OutputAllPlayers();
}
@Test
public void TestRepeatCommand() {
for (int i = 0; i < 2000; ++i)
Core.OutputFinalResultOf("women 1m springboard");
for (int i = 0; i < 2000; ++i)
Core.OutputFinalResultOf("men 1m springboard");
}
// #region 断言true
@Test
public void Test_1() {
assertEquals(Core.OutputFinalResultOf("women 1m springboard"), true);
}
@Test
public void Test_2() {
assertEquals(Core.OutputFinalResultOf("women 3m springboard"), true);
}
@Test
public void Test_3() {
assertEquals(Core.OutputFinalResultOf("women 10m platform"), true);
}
@Test
public void Test_4() {
assertEquals(Core.OutputFinalResultOf("women 3m synchronised"), true);
}
@Test
public void Test_5() {
assertEquals(Core.OutputFinalResultOf("women 10m synchronised"), true);
}
@Test
public void Test_6() {
assertEquals(Core.OutputFinalResultOf("men 1m springboard"), true);
}
@Test
public void Test_7() {
assertEquals(Core.OutputFinalResultOf("men 3m springboard"), true);
}
@Test
public void Test_8() {
assertEquals(Core.OutputFinalResultOf("men 10m platform"), true);
}
@Test
public void Test_9() {
assertEquals(Core.OutputFinalResultOf("men 3m synchronised"), true);
}
@Test
public void Test_10() {
assertEquals(Core.OutputFinalResultOf("men 10m synchronised"), true);
}
// #endregion
// #region 断言false
@Test
public void Test_11() {
assertEquals(Core.OutputFinalResultOf("what the hell is that"), false);
}
@Test
public void Test_12() {
assertEquals(Core.OutputFinalResultOf("men 99999m synchronised"), false);
}
@Test
public void Test_13() {
assertEquals(Core.OutputFinalResultOf("men 114514m synchronised"), false);
}
@Test
public void Test_14() {
assertEquals(Core.OutputFinalResultOf("men 1919810m synchronised"), false);
}
@Test
public void Test_15() {
assertEquals(Core.OutputFinalResultOf("boom"), false);
}
// #endregion
}
单元测试设计的分析
- 首先,测试覆盖了对各种项目决赛结果的输出。这包括了正确项目名和错误项目名的测试。
- 其次,包含了输出性能的测试。TestOutputAllPlayers_1000,TestRepeatCommand分别包含了对大量重复命令的性能测试。
异常处理
- 文件不存在
这是程序中主要的异常。数据文件的路径是用配置文件配置的,如果填写的路径不正确,会抛出异常。
/**
* 读取文件的全部文本
*
* @param file_path
* @return
*/
static public String ReadAllText(String file_path) {
String encoding = "UTF-8";
File file = new File(file_path);
Long filelength = file.length();
byte[] filecontent = new byte[filelength.intValue()];
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
fis.read(filecontent);
return new String(filecontent, encoding);
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
try {
if (fis != null)
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 将文本追加写入到指定文件
*
* @param file_path
* @param content
* @return
*/
static public void WriteAllText(String file_path, String content) {
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file_path, true);
fos.write(content.getBytes());
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fos != null)
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 对于指定路径,创建一个文件。如果路径的原先文件存在,先删除。
*
* @param file_path
*/
static public void DeleteThenCreateFile(String file_path) {
File file = new File(file_path);
if (file.exists())
file.delete();
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
心得体会
-
遇到的难点:
- json文件的解析。这次作业的json文件数据量太大,而且结构比较复杂。
- 单元测试。我之前没有进行过单元测试的编写的经验。
- 打包jar。
-
学到的东西:
- git的使用更加熟练,学习了fork和发起pull request。
- 单元测试的概念和实现。
- 打包jar。