这个作业属于哪个课程 | 2302软件工程社区 |
---|---|
这个作业要求在哪里 | 软件工程第二次作业–文件读取 |
这个作业的目标 | 实现一个对世界游泳锦标赛跳水项目相关赛事数据进行统计的控制台程序 |
其他参考文献 | 《构建之法》、CSDN、工程师的能力评估和发展 、单元测试和回归测试 |
文章目录
1. Gitcode项目地址
2. PSP表格
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 40 |
• Estimate | • 估计这个任务需要多少时间 | 20 | 15 |
Development | 开发 | 990 | 1375 |
• Analysis | • 需求分析 (包括学习新技术) | 100 | 200 |
• Design Spec | • 生成设计文档 | 30 | 25 |
• Design Review | • 设计复审 | 20 | 20 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 30 | 40 |
• Design | • 具体设计 | 60 | 50 |
• Coding | • 具体编码 | 600 | 660 |
• Code Review | • 代码复审 | 30 | 20 |
• Test | • 测试(自我测试,修改代码,提交修改) | 120 | 360 |
Reporting | 报告 | 60 | 90 |
• Test Repor | • 测试报告 | 20 | 30 |
• Size Measurement | • 计算工作量 | 10 | 20 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 30 | 40 |
合计 | 1110 | 1505 |
3. 解题思路描述
3.1 数据的获取
获取运动员的数据相对简单,此处以获取比赛结果的数据作为示例。
- 打开所要获取数据的网站(Competition Athletes | World Aquatics Official),找到界面下方导航栏下的RESULTS,点击进入后按F12打开开发者工具
- 发现events.json的请求接口地址为
https://api.worldaquatics.com/fina/competitions/2969/events
- 分析events.json的结构,得到DisciplineList中的Id为比赛项目的Id,DisciplineName为比赛项目的名称
- 在页面上点击切换比赛项目,注意到页面请求了对应比赛的json文件
- 分析该请求的URL,发现
https://api.worldaquatics.com/fina/events/
加上events.json中比赛名对应的Id所拼接的新URL就是对应比赛结果的请求URL。根据这一特性,我们得出当需要查询某个比赛的结果时,可以先通过events获取对应比赛结果的Id,之后再通过上述的新URL获取对应比赛结果的json文件
3.2 JSON数据的解析
FastJson基于 ASM 的字节码生成和反射优化,使得它在解析和序列化 JSON 数据时非常快速,特别是在处理大型 JSON 数据等需要高性能的场景下。因此我们采用FastJson来进行JSON数据的解析。
- 获取FastJson的jar包,使用IDEA导入到lib文件夹
- 使用http请求获取json数据,根据数据结构使用fastjson的JSON类将json转为JSONArray或JSONObject类,再根据json结构提取自己需要的数据,将数据映射到实体类
- 最后利用StringBuilder来拼接获得的数据用于最后的输出
3.3 input文件中信息的处理
- 使用 BufferedReader 来按行读取文件内容。
- doCmd函数解析每一行input指令,根据指令内容决定输出错误提示或者调用core中的相关函数获取数据
4. 接口设计和实现过程
4.1 项目结构
├─consts
├─core
│ └─impl
├─model
├─render
└─util
consts:存放常量
core: 存放核心功能接口
model: 存放实体类
impl: 实现接口
render:存放渲染类
utils: 存放工具类
4.2 项目各类与方法
// 包含main函数,用于调用其他类,并将正确的命令行参数传给其他类
public class DWASearch{}
// 用于文件的读写和input内容的处理
public class FileRender{}
// 用于获取与处理Player与Result的信息的接口
public interface Search{}
// 实现Search接口
public class SearchImpl{}
// 用于获取http的请求
public class HttpUtil{}
4.3 设计流程图
- 获取选手列表(getPlayers()):
- 获取比赛信息(getResults()):
5. 关键代码展示
5.1 Search接口实现
- 获取选手列表:
@Override
public List<Player> getPlayers() {
if (playerList != null) {
return playerList;
}
try {
response = HttpUtil.get(Consts.PLAYERS_URL, null, null);
} catch (IOException e) {
throw new RuntimeException(e);
}
JSONArray array = JSON.parseArray(response.body);
List<Player> players = new ArrayList<>();
for (int i = 0; i < array.size(); i++) {
JSONObject object = array.getJSONObject(i);
JSONArray athletes = object.getJSONArray("Participations");
String country = object.getString("CountryName");
for (int j = 0; j < athletes.size(); j++) {
JSONObject athlete = athletes.getJSONObject(j);
String lastName = athlete.getString("PreferredLastName");
String firstName = athlete.getString("PreferredFirstName");
int gender = athlete.getIntValue("gender");
Player player = new Player(lastName + " " + firstName,
gender == 0 ? "Male" : "Female", country);
players.add(player);
}
}
playerList = players;
return players;
}
- 获取单项比赛记录:
@Override
public List<Result> getResults(String eventName) {
if (resultCache.containsKey(eventName)) {
Map<String, List<Result>> map = resultCache.get(eventName);
return map.get(Consts.FINAL_RESULT);
}
String eventId = getEventId(eventName);
try {
response = HttpUtil.get(Consts.RESULTS_URL + eventId, null, null);
} catch (IOException e) {
throw new RuntimeException(e);
}
JSONObject object = JSON.parseObject(response.body);
JSONArray heats = object.getJSONArray("Heats");
Map<String, List<Result>> map = new HashMap<>();
resultCache.put(eventName, map);
for (int i = 0; i < heats.size(); i++) {
JSONObject heat = heats.getJSONObject(i);
String heatName = heat.getString("Name");
JSONArray results = heat.getJSONArray("Results");
List<Result> resultList = new ArrayList<>();
for (int j = 0; j < results.size(); j++) {
JSONObject result = results.getJSONObject(j);
String totalPoints = result.getString("TotalPoints");
String fullName = result.getString("FullName");
if (fullName.contains("/")) {
String[] names = fullName.split(" / ");
Arrays.sort(names);
fullName = names[0] + " & " + names[1];
}
int rank = result.getIntValue("Rank");
JSONArray dives = result.getJSONArray("Dives");
String[] scores = new String[dives.size()];
for (int k = 0; k < dives.size(); k++) {
JSONObject dive = dives.getJSONObject(k);
scores[k] = dive.getString("DivePoints");
}
Result r = new Result(fullName, rank, String.join(" + ", scores) + " = " + totalPoints);
resultList.add(r);
}
map.put(heatName, resultList);
}
return map.get(Consts.FINAL_RESULT);
}
- 获取详细比赛结果(附加功能):
@Override
public List<Map<String, Result>> getDetailedResults(String eventName) {
if (detailedResultCache.containsKey(eventName)) {
return detailedResultCache.get(eventName);
}
if (!resultCache.containsKey(eventName)) {
getResults(eventName);
}
Map<String, List<Result>> map = resultCache.get(eventName);
Map<String, Map<String, Result>> flagMap = new HashMap<>();
List<Map<String, Result>> detailedResults = new ArrayList<>();
for (String heatName : map.keySet()) {
List<Result> results = map.get(heatName);
for (Result result : results) {
if (flagMap.containsKey(result.getFullName())) {
Map<String, Result> resultMap = flagMap.get(result.getFullName());
resultMap.put(heatName, result);
} else {
Map<String, Result> resultMap = new HashMap<>();
resultMap.put(heatName, result);
flagMap.put(result.getFullName(), resultMap);
detailedResults.add(resultMap);
}
}
}
// 排序
detailedResults.sort((a, b) -> {
int aFirstRank = getFirstRank(a);
int bFirstRank = getFirstRank(b);
return aFirstRank - bFirstRank;
});
detailedResultCache.put(eventName, detailedResults);
return detailedResults;
}
6. 性能改进
6.1 可改进点的分析
未优化前运行2k+行处理指令的时间:50秒28毫秒
- 分析点1 :字符串拼接:使用String进行字符串拼接会消耗较长的时间。这是因为在拼接时,它会使用StringBuilder,并调用append方法,最后再调用toString方法。每次拼接都要执行这些操作,导致性能代价较大。
- 分析点2 :文件读取方式:原先的文件读取方式使用了reader,这种方法进行了大量的I/O操作,并且每次都要将数据先读取为字节,然后进行转码。这种方式效率较低。
- 分析点3 :重复指令的处理:当输入文件中存在大量重复的指令行时,在编译过程中会处理重复的指令。某些代码可以避免重复执行,从而提高程序的运行效率。
6.2 对应的解决方法
分析点1:使用StringBuilder来拼接字符串,这样不会产生新的对象,减少了频繁创建对象的耗时。
if (cmd[0].equals("players")) {
if (cmd.length != 1) {
return "Error\n-----\n";
}
List<Player> players = search.getPlayers();
StringBuilder builder = new StringBuilder();
for (Player player : players) {
builder.append(player.toString()).append("-----\n");
}
return builder.toString();
}
分析点2:直接用BufferedReader来进行文件读取,每次读取一行文件,相对于read方法而言,减少了I/O操作,修改完后会发现速度明显快了许多,看来减少程序的I/O次数在优化过程中是十分必要的。
BufferedReader reader = new BufferedReader(new InputStreamReader(fins, "UTF-8"));
while ((lineStr = reader.readLine()) != null) {
if (!lineStr.equals("")) {
inputList.add(lineStr.trim());
}
}
分析点3:HashMap的查找操作的时间复杂度为O(1),因此可以使用HashMap进行缓存优化,因为它提供了快速的键值对查找和存储操作,可以快速地根据指令查找到对应的输出结果,而无需进行耗时的文件访问和解析操作,于是我们在SearchImpl类中缓存了选手数据与比赛结果数据。
public class SearchImpl implements Search {
private List<Player> playerList = null;
private Map<String, String> eventIdMap = null; //存储比赛名称和对应的id
private Map<String, Map<String, List<Result>>> resultCache = new HashMap<>();
}
优化后执行2k+行与之前相同的处理指令的运行时间:
7. 单元测试
本次使用了junit模块,对实现打印选手信息、打印比赛项目信息和详细信息功能的core进行了测试。
7.1 单元测试代码
/**
* 搜索接口实现测试
*/
public class SearchImplTest {
private Search search = new SearchImpl();
@Test
public void testGetPlayers() {
search.getPlayers().forEach(System.out::println);
}
@Test
public void testGetResultS() {
for (String match : Consts.MATCH_ARR) {
search.getResults(match).forEach(System.out::println);
}
}
@Test
public void testGetDetailedResults() {
for (String match : Consts.MATCH_ARR) {
search.getDetailedResults(match).forEach(System.out::println);
}
}
}
7.2 覆盖率测试情况
代码覆盖率均达到88%,符合单元测试标准
8. 异常处理
- 命令行错误处理:
if (args.length != Consts.ARGS_LENGTH) {
System.out.println("命令行参数格式错误!应为:Java -jar DWASearch.jar input.txt output.txt");
System.exit(-1);
}
- input指令异常处理:
try {
List<String> inputList = FileRender.getInput(args[0]);
List<String> dataList = FileRender.getData(inputList);
out = new BufferedWriter(new FileWriter(args[1]));
for (String str : dataList) {
out.write(str);
}
out.flush();
} catch (FileNotFoundException e) {
System.out.println("找不到指定文件!");
e.printStackTrace();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (out != null) {
out.close();
}
}
9. 心得体会
在本次软件工程实践作业中,我深刻认识到一个项目的完成远远超出了简单的让代码运行通过。以往的大作业通常只需要提交源代码,而这次的作业让我经历了从规划到实现再到测试编写报告的完整流程,让我意识到了自己在许多方面都不够严谨。
- 在以往的开发中,我很少接触单元测试这个概念,也从未深入学习过如何编写和执行单元测试。然而,在这次作业中,我不得不学习并实践了单元测试的方法和技巧。通过编写全面、细致的单元测试,我能够更好地了解代码的功能和逻辑,并及时发现和修复潜在的问题。
- 过去,我很少关注代码的性能问题,只注重功能的实现。然而,在这次作业中,我深刻认识到性能优化对于项目的重要性。通过使用性能测试工具,我能够确定代码中的性能瓶颈,并采取相应的优化措施,以提高代码在大规模数据处理时的效率和响应速度。
- 自己在时间估计方面存在问题,未能准确评估所需的时间,导致在截止日期临近时才完成所有任务。这给我敲响了警钟,以后我会更加谨慎地评估项目所需的时间,并制定合理的计划,以保证能从容地完成项目。