111
社区成员




这个作业属于哪个课程 | 软件工程实践 |
这个作业要求在哪里 | 软件工程实践第二次作业 |
这个作业的目标 | 完成对2024年巴黎奥运会相关数据的收集,并实现一个能够对国家排名及奖牌个数统计的控制台程序。 |
其他参考文献 | 《构建之法》 |
fork仓库的项目地址:222200227黄茂林
提交PR的助教仓库地址:222200227黄茂林
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 |
首先,由于需要进行文件读写操作,然后需要编写业务流程,包括获取命令,判断命令的正确性,以及执行命令。最后将接口模块化与集成于 OlympicSearchUtil 类调用。
这个程序分为四个类,分别为主类 OlympicSearch, 功能类 Total,功能类 MatchList,工具类 CommandProcessor。
OlympicSearch
类是程序的主入口,包含main
方法。其实现过程如下:
导入必要的 Java 类库:
java.io.File
:用于文件操作java.io.FileNotFoundException
:处理文件未找到异常java.io.PrintWriter
:用于写入输出java.util.Scanner
:用于读取输入定义main
方法,接受命令行参数args
。
声明Scanner
和PrintWriter
对象,用于处理输入和输出。
检查命令行参数:
Scanner
对象PrintWriter
对象Scanner
对象和基于输出文件的PrintWriter
对象FileNotFoundException
,打印堆栈跟踪并返回使用try-finally
块来确保资源正确关闭:
try
块中:CommandProcessor
对象,传入scanner
和writer
CommandProcessor
的processCommands()
方法处理命令finally
块中:scanner
和writer
,确保资源释放CommandProcessor
类负责处理命令执行的逻辑。其实现过程如下:
导入必要的 Java 类库:
data.Country
:用于处理国家数据java.io.PrintWriter
:用于写入输出java.util.Scanner
:用于读取输入java.util.concurrent
包中的类:用于处理并发操作定义CommandProcessor
类,包含以下私有成员变量:
scanner
:用于读取输入writer
:用于写入输出executor
:线程池,用于执行异步任务matchList
:用于处理赛程相关命令实现构造函数:
Scanner
和PrintWriter
作为参数MatchList
对象实现processCommands
方法:
try-catch-finally
结构确保异常处理和资源释放Future
对象,异步获取总奖牌榜数据while
循环读取输入命令,直到遇到"exit"命令或输入结束MatchList
的方法处理finally
块中关闭线程池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
方法:
Match
对象数组Match
对象数组实现MatchList_Print
方法:
PrintWriter
对象和一个Match
对象数组Match
对象的信息,添加分隔线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 的 URLCYTotal
,类型为Country[]
,用于存储奖牌榜数据实现构造函数:
BufferedReader
读取 API 响应Gson
解析 JSON 数据,提取奖牌信息CYTotal
数组,存储每个国家的奖牌信息实现Total_Print
方法:
Writer
对象和一个Country[]
数组Country
对象的信息,添加分隔线实现getCYTotal
方法:
CYTotal
数组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();
}
}
功能: 处理输入命令并执行相应的操作。
关键代码:
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)");
}
}
功能: 处理赛程相关的命令,缓存和获取比赛数据。
关键代码:
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];
}
}
功能: 获取并处理奥运会奖牌榜数据。
关键代码:
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 获取数据、处理命令输入、缓存结果以及输出信息的关键实现。同时每个类都专注于特定的功能模块。
使用 HashMap 缓存赛程数据:在 MatchList 类中使用 HashMap 缓存日期和对应的比赛数据,减少重复的 API 请求,提高数据获取效率。
使用 BufferedReader 提高读取效率:在 Total 和 MatchList 类中使用 BufferedReader 读取 API 响应,优化了数据读取的性能。
静态处理常量和格式化器:在 Total 类中,将 API URL 和数据格式化器定义为静态常量,避免在每次请求时重新分配内存。
异步处理奖牌数据获取:在 CommandProcessor 类中使用 ExecutorService 和 Future 异步获取奖牌数据,提升程序的响应速度。
减少重复对象创建:在 MatchList 类中,通过重用 Gson 对象来解析 JSON 数据,减少了对象创建的开销。
优化输出流刷新:在所有输出操作后立即刷新 PrintWriter,确保数据及时写入,提高了输出效率。
异常处理优化:通过 try-catch 块捕获并处理可能的异常,保证程序在异常情况下的稳定性和连续性。
这些性能改进措施提高了程序的运行效率和响应速度,同时也增强了代码的可维护性和可读性。
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类测试单元代码同理省略 ...
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");
}
}
使用 try-catch 块处理网络和 IO 异常:在 Total 和 MatchList 类中,使用 try-catch 块处理网络连接和数据读取过程中可能出现的异常,确保程序在异常情况下能够输出错误信息并继续运行。
捕获并处理 JSON 解析异常:在解析 API 返回的 JSON 数据时,使用 try-catch 块捕获可能的解析异常,防止因格式不符或数据缺失导致程序崩溃。
返回错误代码处理非法命令:在 CommandProcessor 类中,通过 isAvaliableCommand 方法返回错误代码来处理非法命令,并在输出文件中记录错误信息。
资源关闭保证:在所有涉及 IO 操作的类中,确保 BufferedReader 和 HttpURLConnection 等资源在使用完毕后被正确关闭,避免资源泄露。
参数检查:在 OlympicSearch 类中,检查命令行参数的数量,如果不足两个,将默认启动控制台输入命令。
这次任务的核心编程逻辑并不复杂,但由于缺乏对完整软件开发流程的经验,在诸如 Git 版本控制、测试环境搭建以及 Maven 项目管理等方面遇到了不少挑战。这些环节占用了大量时间,远超过实际编码的时间投入。然而,这些经历是软件工程中不可或缺的学习过程。随着不断实践和积累,我相信这些技能将逐渐内化为开发工作中的自然习惯。
这次实践深刻地展示了软件开发的多面性和复杂性。它让我意识到,编写代码仅仅是整个软件工程中的一小部分。版本控制、测试策略、持续集成等环节同样至关重要。如何有效地利用版本控制系统、设计全面的测试用例、实现自动化测试流程,这些都是需要我们不断学习和探索的领域。此外,项目的成功还高度依赖于前期的需求分析和架构设计。只有在这些基础工作做好的情况下,后续的编码过程才能更加顺畅和高效。这次作业不仅提升了我的技术能力,也让我对软件工程的整体流程有了更全面的认识。