688
社区成员
发帖
与我相关
我的任务
分享| 这个作业属于哪个课程 | 2023年福大-软件工程实践-W班 |
|---|---|
| 这个作业要求在哪里 | 软件工程实践第二次作业——个人实战 |
| 这个作业的目标 | 完成对澳大利亚网球公开赛相关数据的收集,并实现一个能够对赛事数据进行统计的控制台程序 |
| 其他参考文献 | 无 |
| PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 20 | 30 |
| - Estimate | - 估计这个任务需要多少时间 | 20 | 30 |
| Development | 开发 | 440 | 570 |
| - Analysis | - 需求分析 (包括学习新技术) | 60 | 40 |
| - Design Spec | - 生成设计文档 | 30 | 30 |
| - Design Review | - 设计复审 | 30 | 40 |
| - Coding Standard | - 代码规范(为目前的开发制定合适的规范) | 20 | 30 |
| - Design | - 具体设计 | 30 | 50 |
| - Coding | - 具体编码 | 150 | 200 |
| - Code Review | - 代码复审 | 30 | 60 |
| - Test | - 测试(自我测试,修改代码,提交修改) | 90 | 120 |
| Reporting | 报告 | 120 | 110 |
| - Test Repor | - 测试报告 | 60 | 60 |
| - Size Measurement | - 计算工作量 | 20 | 10 |
| Postmortem & Process Improvement Plan | - 事后总结, 并提出过程改进计划 | 40 | 40 |
| 合计 | 580 | 710 |
在拿到题目之后,我细致阅读。
我了解到此次实践希望我们能开发一款能处理从网络上爬取的 JSON 数据,并能按照用户的需求格式化输出到 文件 的 控制台程序。
开发语言可以从 C++ 和 Java 中择一。考虑到 Java 有 GSON 等较为成熟的 JSON 解析工具,我选择使用 Java 完成本次作业。
通过浏览澳大利亚网球公开赛网站的请求记录,我发现当访问 https://ausopen.com/schedule 某个日期的页面,会向服务器的 https://prod-scores-api.ausopen.com/year/2023/period/MD/day/1/schedule 接口请求赛程数据的 JSON 数据。
其中 /day/1/schedule 的数字 1 即为 Day 1 的比赛的数据。
通过编写简单的 Bash 脚本,我使用 wget 将 16 天的赛程数据爬取下来。
对于预选赛的数据,可以使用同样的方法爬取。
程序的输入为一个文本文件,输出也为一个文本文件。
程序读取输入文件中的指令,根据指令的要求在爬取数据中筛选出部分数据,格式化后输出至输出文件。
需要实现的指令有:
players 输出所有选手信息result 01xx 输出正式赛每日结果result Qx 输出资格赛结果程序需要具有能从文件中读取并解析 JSON 数据的能力,且程序需要有能从 JSON 中提取对应数据的能力。
程序需要能作为模块被引用,因此需要将实现每个指令对应的函数包装为接口。
在程序编写完成后,应对程序进行全面的单元测试。
在所有功能写作完成后,需要进行性能测试,尽可能提高性能。
对于文件读取,我选择使用 Java 内置的 FileStream 和 BufferedReader 包;
对于 JSON 解析,我选择使用曾经使用过的、较为熟悉的 GSON 包;
对于单元测试,我选择了 Java 社区较为主流的 JUnit 5 包。
对于性能改进方面,我选择使用 IDEA 自带的 InteliJ Profiler 分析程序的性能。
考虑到程序的可复用性,我采用了工厂方法设计模式,设计了名为 OutputDataFactory 的接口,内含 createOutputData 函数生产产品。 由 PlayerDataFactory 与 MatchDataFactory 继承。
产品则设计了 OutputData 接口,内含 formatOutput 将数据格式化为字符串输出。 由 PlayerInfoData 和 MatchResultData 继承。这些类和接口都封装在名为 Core 的类中,作为程序的核心类库。
在实现功能时,仅需创建 OutputDataFactory 的子类,并执行 createOutputData 获得产品,对产品执行 formatOutput 即可获取格式化后的字符串数据。
而读取输入文件、写入输出文件、执行命令、清空输入文件等功能则封装在名为 Utils 的类中,作为程序的工具类库。
项目流程图:

产品接口于两个产品类。
public interface OutputData {
String formatOutput();
}
/***
* This is a class for storing the player data.
*/
public static class PlayerInfoData implements OutputData {
public String fullName;
public String gender;
public String nationality;
@Override
public String formatOutput() {
......
}
}
/***
* This is a class for storing the match result data.
*/
public static class MatchResultData implements OutputData {
public String time;
public String winner;
public String score;
@Override
public String formatOutput() {
......
}
}
工厂接口与两个工厂类。
public interface OutputDataFactory {
Vector<OutputData> createOutputData(Utils.InputCommand inputCommand, String inputFileFolder) throws IOException;
}
public static class PlayerInfoDataFactory implements OutputDataFactory {
/***
* This method get info of all players and return as Object.
* @param inputCommand The input command.
* @param inputFileFolder The folder of the player info file.
* @return The info of all players.
*/
@Override
public Vector<OutputData> createOutputData(Utils.InputCommand inputCommand, String inputFileFolder) throws IOException {
......
}
}
public static class MatchResultDataFactory implements OutputDataFactory {
/***
* This method get result of match and return as Object.
* @param inputCommand The input command.
* @param inputFileFolder The folder of the match result file.
* @return The result of the match.
*/
public Vector<OutputData> createOutputData(Utils.InputCommand inputCommand, String inputFileFolder) throws IOException {
......
}
}
读取文件内容(使用 Apache Commons IO)
public static String readContentFromFile(String path) throws IOException {
LineIterator it = FileUtils.lineIterator(new File(path), "UTF-8");
StringBuilder content = new StringBuilder();
try {
while (it.hasNext()) {
String line = it.nextLine();
content.append(line);
}
} finally {
LineIterator.closeQuietly(it);
}
return content.toString();
}
执行命令前使用正则表达式检查输入的命令是否合法。
public static void executeCommand(InputCommand inputCommand, String inputFileFolder, String outputFilePath) {
// Check whether the command is valid.
if (!Objects.equals(inputCommand.command, "players") && !Objects.equals(inputCommand.command, "result")) {
......
}
if (Objects.equals(inputCommand.command, "result")) {
// Check whether the argument is valid.
Pattern pattern1 = Pattern.compile("01[0-9]{2}");
Matcher matcher1 = pattern1.matcher(inputCommand.arg);
Pattern pattern2 = Pattern.compile("Q[1-4]");
Matcher matcher2 = pattern2.matcher(inputCommand.arg);
if (matcher1.find()) {
// Is 01xx type arg.
int dateNum = Integer.parseInt(inputCommand.arg);
if (dateNum < 116 || dateNum > 129) {
......
}
} else if (matcher2.find()) {
// Is Qx type arg.
int quarterNum = Integer.parseInt(inputCommand.arg.substring(1));
if (quarterNum < 1 || quarterNum > 4) {
......
}
} else {
// Is not 01xx or Qx type arg.
......
}
}
// Execute command
......
}
使用 IDEA 的 InteliJ Profiler 工具分析程序的性能,可以发现 readContentFromFile 函数中通过 BufferReader 读取 JSON 文件的步骤花费了较多的时间。
通过将 BufferedReader 更换为 Apache Commons IO 的 FileUtils 之后,可以观测到主线程消耗的时间缩短了约 25%。
修改前性能:

修改后性能:

我设计了 13 组单元测试,对 Lib 中每个函数都进行了包括正确输入和错误输入的测试。
测试结果如下:

Lib 库方法覆盖率达到 100%,行覆盖率达到 83%。
覆盖率截图:

对文件打开失败、找不到文件等异常做了异常处理。
通过此次作业,我复习了一下 Java 的语法,发现没忘记太多。
我先尝试使用接口方式设计 Lib 的框架,在编写部分函数后发现重复的代码很多,也没有满足开闭原则。正好那天设计模式课上讲到了工厂模式,我一看,这不是我想要的吗。深知 Java 喜欢用这种“鬼方法”,我何不自己也尝试一下呢。
浅尝之后发现确实能复用很多代码,添加新功能的时候只需要编写对应的工厂类和产品类,目标很明确。缺点是要建很多类,但有 IDE 和 AI 的帮忙之下问题不是很大。
这也是我第一次写这么多文档。Javadoc 确实能“逼迫”着我把文档写清楚,为后面接盘的人带来福音(笑)。
作业完成的很好,美中不足就是异常处理部分,太过于简略