软件工程实践第二次作业——个人实战
| 这个作业属于哪个课程 | 软件工程实践 |
| 这个作业要求在哪里 | 软件工程实践第二次作业 |
| 这个作业的目标 | 完成对2024年巴黎奥运会相关数据的收集,并实现一个能够对国家排名及奖牌个数统计的控制台程序。 |
| 其他参考文献 | 《构建之法》 |
目录
- 1. Gitcode 项目地址
- 2. PSP 表格
- 3. 解题思路描述
- 4. 接口设计和实现过程
- OlympicSearch 类
- CommandProcessor 类
- MatchList 类
- Total 类
- 5. 关键代码展示
- OlympicSearch 类
- CommandProcessor 类
- MatchList 类
- Total 类
- 6. 性能改进
- 7. 单元测试
- Total 类与 MatchList 类单元测试
- CommandProcessor 单元测试
- 测试单元运行截图
- 覆盖率运行截图【Total类】【MatchList类】【CommandProcessor类】
- 8. 异常处理
- 9. 心得体会
1. Gitcode 项目地址
fork仓库的项目地址:222200227黄茂林
提交PR的助教仓库地址:222200227黄茂林
2. PSP 表格
| PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) | Personal Software Process Stages |
|---|---|---|---|---|
| Planning | 计划 | 10 | 10 | Planning |
| • Estimate | • 估计这个任务需要多少时间 | 10 | 10 | • Estimate |
| Development | 开发 | 4310 | 3080 | Development |
| • Analysis | • 需求分析(包括学习新技术) | 1380 | 900 | • Analysis |
| • Design Spec | • 生成设计文档 | 58 | 30 | • Design Spec |
| • Design Review | • 设计复审 | 29 | 30 | • Design Review |
| • Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 58 | 30 | • Coding Standard |
| • Design | • 具体设计 | 58 | 30 | • Design |
| • Coding | • 具体编码 | 692 | 450 | • Coding |
| • Code Review | • 代码复审 | 115 | 75 | • Code Review |
| • Test | • 测试(自我测试,修改代码,提交修改) | 1920 | 1535 | • Test |
| Reporting | 报告 | 120 | 55 | Reporting |
| • Test Report | • 测试报告 | 40 | 20 | • Test Report |
| • Size Measurement | • 计算工作量 | 40 | 20 | • Size Measurement |
| • Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 40 | 15 | • Postmortem & Process Improvement Plan |
| 合计 | 4440 | 3145 | Total |
3. 解题思路描述
首先,由于需要进行文件读写操作,然后需要编写业务流程,包括获取命令,判断命令的正确性,以及执行命令。最后将接口模块化与集成于 OlympicSearchUtil 类调用。
4. 接口设计和实现过程
这个程序分为四个类,分别为主类 OlympicSearch, 功能类 Total,功能类 MatchList,工具类 CommandProcessor。
OlympicSearch 类
OlympicSearch类是程序的主入口,包含main方法。其实现过程如下:
导入必要的 Java 类库:
java.io.File:用于文件操作java.io.FileNotFoundException:处理文件未找到异常java.io.PrintWriter:用于写入输出java.util.Scanner:用于读取输入
定义
main方法,接受命令行参数args。声明
Scanner和PrintWriter对象,用于处理输入和输出。检查命令行参数:
- 如果参数数量不等于 2(即没有指定输入和输出文件):
- 创建一个从标准输入(控制台)读取的
Scanner对象 - 创建一个向标准输出(控制台)写入的
PrintWriter对象
- 创建一个从标准输入(控制台)读取的
- 否则(有两个文件参数):
- 尝试创建基于输入文件的
Scanner对象和基于输出文件的PrintWriter对象 - 如果文件不存在,捕获
FileNotFoundException,打印堆栈跟踪并返回
- 尝试创建基于输入文件的
- 如果参数数量不等于 2(即没有指定输入和输出文件):
使用
try-finally块来确保资源正确关闭:- 在
try块中:- 创建
CommandProcessor对象,传入scanner和writer - 调用
CommandProcessor的processCommands()方法处理命令
- 创建
- 在
finally块中:- 关闭
scanner和writer,确保资源释放
- 关闭
- 在
CommandProcessor 类
CommandProcessor类负责处理命令执行的逻辑。其实现过程如下:
导入必要的 Java 类库:
data.Country:用于处理国家数据java.io.PrintWriter:用于写入输出java.util.Scanner:用于读取输入java.util.concurrent包中的类:用于处理并发操作
定义
CommandProcessor类,包含以下私有成员变量:scanner:用于读取输入writer:用于写入输出executor:线程池,用于执行异步任务matchList:用于处理赛程相关命令
实现构造函数:
- 接受
Scanner和PrintWriter作为参数 - 初始化成员变量
- 创建一个固定大小为 10 的线程池
- 初始化
MatchList对象
- 接受
实现
processCommands方法:- 使用
try-catch-finally结构确保异常处理和资源释放 - 创建一个
Future对象,异步获取总奖牌榜数据 - 使用
while循环读取输入命令,直到遇到"exit"命令或输入结束 - 对每个命令进行处理:
- 如果是"total"命令,获取并打印总奖牌榜
- 如果是"schedule"开头的命令,调用
MatchList的方法处理 - 如果是其他命令,输出错误信息
- 每次命令处理后刷新输出流,确保内容被写入
- 捕获并打印任何异常
- 在
finally块中关闭线程池
- 使用
MatchList 类
MatchList类负责处理赛程相关的命令,主要实现从 API 获取数据并缓存结果。其实现过程如下:
导入必要的 Java 类库:
com.google.gson.Gson:用于解析 JSON 数据com.google.gson.JsonArray和com.google.gson.JsonObject:用于处理 JSON 对象和数组data.Match:用于存储比赛信息java.io.BufferedReader和java.io.InputStreamReader:用于读取 API 响应java.io.PrintWriter:用于写入输出java.net.HttpURLConnection和java.net.URL:用于建立 HTTP 连接java.util.Objects:用于对象比较java.util.HashMap和java.util.Map:用于缓存数据
定义
MatchList类:- 包含一个公共成员变量
cache,类型为Map<String, Match[]>,用于缓存 API 请求结果
- 包含一个公共成员变量
实现
FindMatchList方法:- 接受一个命令字符串
cmd和一个PrintWriter对象writer - 从命令中提取日期信息
- 检查缓存中是否存在对应日期的数据:
- 如果存在,调用
MatchList_Print方法直接输出缓存结果 - 如果不存在,调用
fetchMatchesFromAPI方法获取数据,并将结果存入缓存,然后输出
- 如果存在,调用
- 接受一个命令字符串
实现
fetchMatchesFromAPI方法:- 接受日期字符串作为参数
- 构建 API 请求 URL,并建立 HTTP 连接
- 读取 API 响应并解析 JSON 数据
- 处理 JSONP 格式,提取有效的 JSON 字符串
- 将 JSON 数据转换为
Match对象数组 - 返回
Match对象数组
实现
MatchList_Print方法:- 接受一个
PrintWriter对象和一个Match对象数组 - 如果数组为空,输出"N/A"和分隔线
- 否则,遍历数组,输出每个
Match对象的信息,添加分隔线
- 接受一个
Total 类
Total类负责从 API 获取奥运会奖牌榜数据并提供数据输出功能。其实现过程如下:
导入必要的 Java 类库:
com.google.gson.Gson:用于解析 JSON 数据com.google.gson.JsonArray和com.google.gson.JsonObject:用于处理 JSON 对象和数组data.Country:用于存储国家奖牌信息java.io.BufferedReader和java.io.InputStreamReader:用于读取 API 响应java.io.Writer:用于写入输出java.net.HttpURLConnection和java.net.URL:用于建立 HTTP 连接
定义
Total类:- 包含一个常量
TOTAL_API,用于存储 API 的 URL - 包含一个静态变量
CYTotal,类型为Country[],用于存储奖牌榜数据
- 包含一个常量
实现构造函数:
- 建立 HTTP 连接,发送 GET 请求获取奖牌榜数据
- 使用
BufferedReader读取 API 响应 - 使用
Gson解析 JSON 数据,提取奖牌信息 - 初始化
CYTotal数组,存储每个国家的奖牌信息
实现
Total_Print方法:- 接受一个
Writer对象和一个Country[]数组 - 如果数组不为空,遍历数组,输出每个
Country对象的信息,添加分隔线 - 如果数组为空,输出"没有数据"
- 接受一个
实现
getCYTotal方法:- 返回存储奖牌榜数据的
CYTotal数组
- 返回存储奖牌榜数据的
5. 关键代码展示
OlympicSearch 类
- 功能: 程序的主入口,负责初始化输入输出并调用命令处理器。
- 关键代码:
public static void main(String[] args) { Scanner scanner; PrintWriter writer; if (args.length != 2) { scanner = new Scanner(System.in); writer = new PrintWriter(System.out); } else { String inputFile = args[0]; String outputFile = args[1]; try { scanner = new Scanner(new File(inputFile)); writer = new PrintWriter(new File(outputFile)); } catch (FileNotFoundException e) { e.printStackTrace(); return; } } try { CommandProcessor processor = new CommandProcessor(scanner, writer); processor.processCommands(); } finally { scanner.close(); writer.close(); } }
CommandProcessor 类
功能: 处理输入命令并执行相应的操作。
关键代码:
public void processCommands() { try { System.out.println("正在读入命令......(input.txt)"); Future<Country[]> future = executor.submit(() -> new Total().getCYTotal()); while (scanner.hasNextLine()) { String command = scanner.nextLine().trim().toLowerCase(); if (command.equals("exit")) { System.out.println("Exiting program..."); break; } else if (command.equals("total")) { Country[] CYTotal = future.get(); // 阻塞直到异步计算完成 Total.Total_Print(writer, CYTotal); } else if (command.startsWith("schedule ")) { matchList.FindMatchList(command, writer); } else { writer.write("ERROR\n"); writer.write("-----\n"); } writer.flush(); } } catch (Exception e) { e.printStackTrace(); } finally { executor.shutdown(); System.out.println("结果已输出.....(output.txt)"); } }
MatchList 类
功能: 处理赛程相关的命令,缓存和获取比赛数据。
关键代码:
public void FindMatchList(String cmd, PrintWriter writer) { String date = cmd.substring(9).trim(); if (!isValidDateFormat(date)) { writer.write("Error\n"); writer.write("-----\n"); } else if (cache.containsKey(date)) { // 如果缓存中存在,直接使用缓存的结果 MatchList_Print(writer, cache.get(date)); } else { // 如果缓存中不存在,从API获取数据 Match[] matches = fetchMatchesFromAPI(date); cache.put(date, matches); // 将结果存入缓存 MatchList_Print(writer, matches); } } private Match[] fetchMatchesFromAPI(String date) { try { String TOTAL_API = pre + date + end; HttpURLConnection connection = (HttpURLConnection) new URL(TOTAL_API).openConnection(); connection.setRequestMethod("GET"); BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); StringBuilder response = new StringBuilder(); String inputLine; while ((inputLine = in.readLine()) != null) { response.append(inputLine); } in.close(); Gson gson = new Gson(); JsonObject jsonResponse = gson.fromJson(response.toString(), JsonObject.class); JsonArray matchList = jsonResponse.getAsJsonObject("data").getAsJsonArray("matchList"); Match[] matches = new Match[matchList.size()]; for (int i = 0; i < matchList.size(); i++) { JsonObject matchInfo = matchList.get(i).getAsJsonObject(); String time = matchInfo.get("startdatecn").getAsString(); String sport = matchInfo.get("itemcodename").getAsString(); String venue = matchInfo.get("venuename").getAsString(); String awayName = matchInfo.get("awayname").getAsString(); String homeName = matchInfo.get("homename").getAsString(); String title = matchInfo.get("title").getAsString(); String name = (!Objects.equals(awayName, "") && !Objects.equals(homeName, "")) ? title + " " + awayName + " VS " + homeName : title; matches[i] = new Match(time, sport, name, venue); } return matches; } catch (Exception e) { e.printStackTrace(); return new Match[0]; } }
Total 类
功能: 获取并处理奥运会奖牌榜数据。
关键代码:
public Total() { try { HttpURLConnection connection = (HttpURLConnection) new URL(TOTAL_API).openConnection(); connection.setRequestMethod("GET"); BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); StringBuilder response = new StringBuilder(); String inputLine; while ((inputLine = in.readLine()) != null) { response.append(inputLine); } in.close(); Gson gson = new Gson(); JsonObject jsonResponse = gson.fromJson(response.toString(), JsonObject.class); JsonArray medalsList = jsonResponse.getAsJsonObject("data").getAsJsonArray("medalsList"); CYTotal = new Country[medalsList.size()]; for (int i = 0; i < medalsList.size(); i++) { JsonObject medalInfo = medalsList.get(i).getAsJsonObject(); String name = medalInfo.get("countryid").getAsString(); int gold = medalInfo.get("gold").getAsInt(); int silver = medalInfo.get("silver").getAsInt(); int bronze = medalInfo.get("bronze").getAsInt(); int rank = medalInfo.get("rank").getAsInt(); CYTotal[i] = new Country(name, gold, silver, bronze, rank); } } catch (Exception e) { e.printStackTrace(); } } public static void Total_Print(Writer writer, Country[] CYTotal) throws IOException { if (CYTotal != null && CYTotal.length != 0) { for (Country country : CYTotal) { try { writer.write(String.valueOf(country)); writer.write("-----"); writer.write(System.lineSeparator()); } catch (Exception e) { e.printStackTrace(); } } } else { writer.write("没有数据"); } }
这些代码片段展示了如何从 API 获取数据、处理命令输入、缓存结果以及输出信息的关键实现。同时每个类都专注于特定的功能模块。
6. 性能改进
使用 HashMap 缓存赛程数据:在 MatchList 类中使用 HashMap 缓存日期和对应的比赛数据,减少重复的 API 请求,提高数据获取效率。
使用 BufferedReader 提高读取效率:在 Total 和 MatchList 类中使用 BufferedReader 读取 API 响应,优化了数据读取的性能。
静态处理常量和格式化器:在 Total 类中,将 API URL 和数据格式化器定义为静态常量,避免在每次请求时重新分配内存。
异步处理奖牌数据获取:在 CommandProcessor 类中使用 ExecutorService 和 Future 异步获取奖牌数据,提升程序的响应速度。
减少重复对象创建:在 MatchList 类中,通过重用 Gson 对象来解析 JSON 数据,减少了对象创建的开销。
优化输出流刷新:在所有输出操作后立即刷新 PrintWriter,确保数据及时写入,提高了输出效率。
异常处理优化:通过 try-catch 块捕获并处理可能的异常,保证程序在异常情况下的稳定性和连续性。
这些性能改进措施提高了程序的运行效率和响应速度,同时也增强了代码的可维护性和可读性。
7. 单元测试
Total 类与 MatchList 类单元测试
- 测试 Total 空数据输出("没有数据")
- 测试 Total 非空数据输出格式
- 测试 MatchList 空数据输出("N/A")
- 测试 MatchList 非空数据输出格式
public class MatchListTest {
// 测试正常数据
@Test
public void testMatchListPrintWithNormalData() {
// 准备测试数据
Match[] matches = {
new Match("2024-09-07 10:00:00", "Basketball", "Quarter Final Team A VS Team B", "Main Stadium"),
new Match("2024-09-07 15:00:00", "Soccer", "Semi Final Team C VS Team D", "Secondary Stadium")
};
// 创建 MatchList 实例并设置 TMatchs
MatchList matchList = new MatchList();
// 使用 StringWriter 捕获输出
StringWriter writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
// 调用方法
matchList.MatchList_Print(printWriter,matches);
// 获取结果
printWriter.flush();
String result = writer.toString();
// 定义预期输出格式
String expected = "time:10:00\n" +
"sport: Basketball\n" +
"name: Quarter Final Team A VS Team B\n" +
"venue: Main Stadium\n" +
"-----\n" +
"time:15:00\n" +
"sport: Soccer\n" +
"name: Semi Final Team C VS Team D\n" +
"venue: Secondary Stadium\n" +
"-----\n";
String normalizedResult = result.replace("\r\n", "\n");
String normalizedExpected = expected.replace("\r\n", "\n");
assertEquals(normalizedExpected, normalizedResult);
}
// 测试空数据
@Test
public void testMatchListPrintWithEmptyData() {
// 准备空测试数据
Match[] emptyMatches = new Match[0];
// 创建 MatchList 实例并设置 TMatchs
MatchList matchList = new MatchList();
// 使用 StringWriter 捕获输出
StringWriter writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
// 调用方法
matchList.MatchList_Print(printWriter,emptyMatches);
// 获取结果
printWriter.flush();
String result = writer.toString();
// 定义预期输出格式
String expected = "N/A"+"\n"
+ "-----"+"\n"; // 空数据时应该没有输出
// 断言预期结果
assertEquals(expected, result);
}
}
// ... Total类测试单元代码同理省略 ...
CommandProcessor 单元测试
- 测试 total 命令的命令输入与输出
- 测试 schedule 命令的输入与输出(带空格)
- 测试不合法命令(以 invalid 为例)
- 测试大写命令(以 Total)
- 测试超出日期(以 schedule 0707 为例)
- 测试多条指令执行(total schedule 0726 invalid)
- 测试非法日期执行(以 schedule 2003 为例)
class CommandProcessorTest {
private CommandProcessor processor;
private ByteArrayOutputStream outputStream;
private PrintWriter writer;
@TempDir
Path tempDir;
@BeforeEach
void setUp() {
outputStream = new ByteArrayOutputStream();
writer = new PrintWriter(outputStream);
}
//检测total命令的输入与输出
@Test
void testTotalCommand() throws IOException {
String input = "total\nexit\n";
Scanner scanner = new Scanner(new ByteArrayInputStream(input.getBytes()));
processor = new CommandProcessor(scanner, writer);
processor.processCommands();
String output = outputStream.toString();
assertTrue(output.contains("gold"), "Output should contain 'gold'");
assertTrue(output.contains("silver"), "Output should contain 'silver'");
assertTrue(output.contains("bronze"), "Output should contain 'bronze'");
assertTrue(output.contains("total"), "Output should contain 'total'");
}
//检测大写Total命令的输入与输出
@Test
void testUpTotalCommand() throws IOException {
String input = "Total\nexit\n";
Scanner scanner = new Scanner(new ByteArrayInputStream(input.getBytes()));
processor = new CommandProcessor(scanner, writer);
processor.processCommands();
String output = outputStream.toString();
assertTrue(output.contains("ERROR"), "Output should contain 'ERROR'");
}
//检测schedule 0707错误日期处理(包括空格检测)
@Test
void testErrorScheduleCommand() throws IOException {
String input = "schedule 0707\nexit\n";
Scanner scanner = new Scanner(new ByteArrayInputStream(input.getBytes()));
processor = new CommandProcessor(scanner, writer);
processor.processCommands();
String output = outputStream.toString();
assertTrue(output.contains("N/A"), "Output don't should contain the date 'N/A' in correct date");
}
//检测schedule 2003错误日期处理(包括空格检测)
@Test
void testNoScheduleCommand() throws IOException {
String input = "schedule 2003\nexit\n";
Scanner scanner = new Scanner(new ByteArrayInputStream(input.getBytes()));
processor = new CommandProcessor(scanner, writer);
processor.processCommands();
String output = outputStream.toString();
assertTrue(output.contains("Error"), "Output should contain the date 'Error' in correct date");
}
//检测schedule 0726正确处理(包括空格检测)
@Test
void testScheduleCommand() throws IOException {
String input = "schedule 0726\nexit\n";
Scanner scanner = new Scanner(new ByteArrayInputStream(input.getBytes()));
processor = new CommandProcessor(scanner, writer);
processor.processCommands();
String output = outputStream.toString();
assertFalse(output.contains("N/A"), "Output don't should contain the date 'N/A' in correct date");
}
@Test
void testInvalidCommand() throws IOException {
String input = "invalid\nexit\n";
Scanner scanner = new Scanner(new ByteArrayInputStream(input.getBytes()));
processor = new CommandProcessor(scanner, writer);
processor.processCommands();
String output = outputStream.toString();
assertTrue(output.contains("ERROR"), "Output should contain 'ERROR' for invalid command");
}
//批量命令输入测试
@Test
void testMultipleCommands() throws IOException {
String input = "total\nschedule 0726\ninvalid\nexit\n";
Scanner scanner = new Scanner(new ByteArrayInputStream(input.getBytes()));
processor = new CommandProcessor(scanner, writer);
processor.processCommands();
String output = outputStream.toString();
assertTrue(output.contains("gold"), "Output should contain 'gold'");
assertFalse(output.contains("N/A"), "Output don't should contain the date 'N/A' in correct date");
assertTrue(output.contains("ERROR"), "Output should contain 'ERROR' for invalid command");
}
}
测试单元运行截图

覆盖率运行截图【Total类】【MatchList类】【CommandProcessor类】

8. 异常处理
使用 try-catch 块处理网络和 IO 异常:在 Total 和 MatchList 类中,使用 try-catch 块处理网络连接和数据读取过程中可能出现的异常,确保程序在异常情况下能够输出错误信息并继续运行。
捕获并处理 JSON 解析异常:在解析 API 返回的 JSON 数据时,使用 try-catch 块捕获可能的解析异常,防止因格式不符或数据缺失导致程序崩溃。
返回错误代码处理非法命令:在 CommandProcessor 类中,通过 isAvaliableCommand 方法返回错误代码来处理非法命令,并在输出文件中记录错误信息。
资源关闭保证:在所有涉及 IO 操作的类中,确保 BufferedReader 和 HttpURLConnection 等资源在使用完毕后被正确关闭,避免资源泄露。
参数检查:在 OlympicSearch 类中,检查命令行参数的数量,如果不足两个,将默认启动控制台输入命令。
9. 心得体会
这次任务的核心编程逻辑并不复杂,但由于缺乏对完整软件开发流程的经验,在诸如 Git 版本控制、测试环境搭建以及 Maven 项目管理等方面遇到了不少挑战。这些环节占用了大量时间,远超过实际编码的时间投入。然而,这些经历是软件工程中不可或缺的学习过程。随着不断实践和积累,我相信这些技能将逐渐内化为开发工作中的自然习惯。
这次实践深刻地展示了软件开发的多面性和复杂性。它让我意识到,编写代码仅仅是整个软件工程中的一小部分。版本控制、测试策略、持续集成等环节同样至关重要。如何有效地利用版本控制系统、设计全面的测试用例、实现自动化测试流程,这些都是需要我们不断学习和探索的领域。此外,项目的成功还高度依赖于前期的需求分析和架构设计。只有在这些基础工作做好的情况下,后续的编码过程才能更加顺畅和高效。这次作业不仅提升了我的技术能力,也让我对软件工程的整体流程有了更全面的认识。


