软件工程实践第二次作业——个人实战

222200227黄茂林 2024-09-08 16:56:48
这个作业属于哪个课程软件工程实践
这个作业要求在哪里软件工程实践第二次作业
这个作业的目标完成对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 表格

PSPPersonal Software Process Stages预估耗时(分钟)实际耗时(分钟)Personal Software Process Stages
Planning计划1010Planning
• Estimate• 估计这个任务需要多少时间1010• Estimate
Development开发43103080Development
• Analysis• 需求分析(包括学习新技术)1380900• Analysis
• Design Spec• 生成设计文档5830• Design Spec
• Design Review• 设计复审2930• Design Review
• Coding Standard• 代码规范 (为目前的开发制定合适的规范)5830• Coding Standard
• Design• 具体设计5830• Design
• Coding• 具体编码692450• Coding
• Code Review• 代码复审11575• Code Review
• Test• 测试(自我测试,修改代码,提交修改)19201535• Test
Reporting报告12055Reporting
• Test Report• 测试报告4020• Test Report
• Size Measurement• 计算工作量4020• Size Measurement
• Postmortem & Process Improvement Plan• 事后总结, 并提出过程改进计划4015• Postmortem & Process Improvement Plan
合计44403145Total

3. 解题思路描述

首先,由于需要进行文件读写操作,然后需要编写业务流程,包括获取命令,判断命令的正确性,以及执行命令。最后将接口模块化与集成于 OlympicSearchUtil 类调用。

4. 接口设计和实现过程

这个程序分为四个类,分别为主类 OlympicSearch, 功能类 Total,功能类 MatchList,工具类 CommandProcessor。

OlympicSearch 类

OlympicSearch类是程序的主入口,包含main方法。其实现过程如下:

  1. 导入必要的 Java 类库:

    • java.io.File:用于文件操作
    • java.io.FileNotFoundException:处理文件未找到异常
    • java.io.PrintWriter:用于写入输出
    • java.util.Scanner:用于读取输入
  2. 定义main方法,接受命令行参数args

  3. 声明ScannerPrintWriter对象,用于处理输入和输出。

  4. 检查命令行参数:

    • 如果参数数量不等于 2(即没有指定输入和输出文件):
      • 创建一个从标准输入(控制台)读取的Scanner对象
      • 创建一个向标准输出(控制台)写入的PrintWriter对象
    • 否则(有两个文件参数):
      • 尝试创建基于输入文件的Scanner对象和基于输出文件的PrintWriter对象
      • 如果文件不存在,捕获FileNotFoundException,打印堆栈跟踪并返回
  5. 使用try-finally块来确保资源正确关闭:

    • try块中:
      • 创建CommandProcessor对象,传入scannerwriter
      • 调用CommandProcessorprocessCommands()方法处理命令
    • finally块中:
      • 关闭scannerwriter,确保资源释放

CommandProcessor 类

CommandProcessor类负责处理命令执行的逻辑。其实现过程如下:

  1. 导入必要的 Java 类库:

    • data.Country:用于处理国家数据
    • java.io.PrintWriter:用于写入输出
    • java.util.Scanner:用于读取输入
    • java.util.concurrent包中的类:用于处理并发操作
  2. 定义CommandProcessor类,包含以下私有成员变量:

    • scanner:用于读取输入
    • writer:用于写入输出
    • executor:线程池,用于执行异步任务
    • matchList:用于处理赛程相关命令
  3. 实现构造函数:

    • 接受ScannerPrintWriter作为参数
    • 初始化成员变量
    • 创建一个固定大小为 10 的线程池
    • 初始化MatchList对象
  4. 实现processCommands方法:

    • 使用try-catch-finally结构确保异常处理和资源释放
    • 创建一个Future对象,异步获取总奖牌榜数据
    • 使用while循环读取输入命令,直到遇到"exit"命令或输入结束
    • 对每个命令进行处理:
      • 如果是"total"命令,获取并打印总奖牌榜
      • 如果是"schedule"开头的命令,调用MatchList的方法处理
      • 如果是其他命令,输出错误信息
    • 每次命令处理后刷新输出流,确保内容被写入
    • 捕获并打印任何异常
    • finally块中关闭线程池

MatchList 类

MatchList类负责处理赛程相关的命令,主要实现从 API 获取数据并缓存结果。其实现过程如下:

  1. 导入必要的 Java 类库:

    • com.google.gson.Gson:用于解析 JSON 数据
    • com.google.gson.JsonArraycom.google.gson.JsonObject:用于处理 JSON 对象和数组
    • data.Match:用于存储比赛信息
    • java.io.BufferedReaderjava.io.InputStreamReader:用于读取 API 响应
    • java.io.PrintWriter:用于写入输出
    • java.net.HttpURLConnectionjava.net.URL:用于建立 HTTP 连接
    • java.util.Objects:用于对象比较
    • java.util.HashMapjava.util.Map:用于缓存数据
  2. 定义MatchList类:

    • 包含一个公共成员变量cache,类型为Map<String, Match[]>,用于缓存 API 请求结果
  3. 实现FindMatchList方法:

    • 接受一个命令字符串cmd和一个PrintWriter对象writer
    • 从命令中提取日期信息
    • 检查缓存中是否存在对应日期的数据:
      • 如果存在,调用MatchList_Print方法直接输出缓存结果
      • 如果不存在,调用fetchMatchesFromAPI方法获取数据,并将结果存入缓存,然后输出
  4. 实现fetchMatchesFromAPI方法:

    • 接受日期字符串作为参数
    • 构建 API 请求 URL,并建立 HTTP 连接
    • 读取 API 响应并解析 JSON 数据
    • 处理 JSONP 格式,提取有效的 JSON 字符串
    • 将 JSON 数据转换为Match对象数组
    • 返回Match对象数组
  5. 实现MatchList_Print方法:

    • 接受一个PrintWriter对象和一个Match对象数组
    • 如果数组为空,输出"N/A"和分隔线
    • 否则,遍历数组,输出每个Match对象的信息,添加分隔线

Total 类

Total类负责从 API 获取奥运会奖牌榜数据并提供数据输出功能。其实现过程如下:

  1. 导入必要的 Java 类库:

    • com.google.gson.Gson:用于解析 JSON 数据
    • com.google.gson.JsonArraycom.google.gson.JsonObject:用于处理 JSON 对象和数组
    • data.Country:用于存储国家奖牌信息
    • java.io.BufferedReaderjava.io.InputStreamReader:用于读取 API 响应
    • java.io.Writer:用于写入输出
    • java.net.HttpURLConnectionjava.net.URL:用于建立 HTTP 连接
  2. 定义Total类:

    • 包含一个常量TOTAL_API,用于存储 API 的 URL
    • 包含一个静态变量CYTotal,类型为Country[],用于存储奖牌榜数据
  3. 实现构造函数:

    • 建立 HTTP 连接,发送 GET 请求获取奖牌榜数据
    • 使用BufferedReader读取 API 响应
    • 使用Gson解析 JSON 数据,提取奖牌信息
    • 初始化CYTotal数组,存储每个国家的奖牌信息
  4. 实现Total_Print方法:

    • 接受一个Writer对象和一个Country[]数组
    • 如果数组不为空,遍历数组,输出每个Country对象的信息,添加分隔线
    • 如果数组为空,输出"没有数据"
  5. 实现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. 性能改进

  1. 使用 HashMap 缓存赛程数据:在 MatchList 类中使用 HashMap 缓存日期和对应的比赛数据,减少重复的 API 请求,提高数据获取效率。

  2. 使用 BufferedReader 提高读取效率:在 Total 和 MatchList 类中使用 BufferedReader 读取 API 响应,优化了数据读取的性能。

  3. 静态处理常量和格式化器:在 Total 类中,将 API URL 和数据格式化器定义为静态常量,避免在每次请求时重新分配内存。

  4. 异步处理奖牌数据获取:在 CommandProcessor 类中使用 ExecutorService 和 Future 异步获取奖牌数据,提升程序的响应速度。

  5. 减少重复对象创建:在 MatchList 类中,通过重用 Gson 对象来解析 JSON 数据,减少了对象创建的开销。

  6. 优化输出流刷新:在所有输出操作后立即刷新 PrintWriter,确保数据及时写入,提高了输出效率。

  7. 异常处理优化:通过 try-catch 块捕获并处理可能的异常,保证程序在异常情况下的稳定性和连续性。

这些性能改进措施提高了程序的运行效率和响应速度,同时也增强了代码的可维护性和可读性。

7. 单元测试

Total 类与 MatchList 类单元测试

  1. 测试 Total 空数据输出("没有数据")
  2. 测试 Total 非空数据输出格式
  3. 测试 MatchList 空数据输出("N/A")
  4. 测试 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 单元测试

  1. 测试 total 命令的命令输入与输出
  2. 测试 schedule 命令的输入与输出(带空格)
  3. 测试不合法命令(以 invalid 为例)
  4. 测试大写命令(以 Total)
  5. 测试超出日期(以 schedule 0707 为例)
  6. 测试多条指令执行(total schedule 0726 invalid)
  7. 测试非法日期执行(以 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");
    }
}

测试单元运行截图

img

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

img

8. 异常处理

  1. 使用 try-catch 块处理网络和 IO 异常:在 Total 和 MatchList 类中,使用 try-catch 块处理网络连接和数据读取过程中可能出现的异常,确保程序在异常情况下能够输出错误信息并继续运行。

  2. 捕获并处理 JSON 解析异常:在解析 API 返回的 JSON 数据时,使用 try-catch 块捕获可能的解析异常,防止因格式不符或数据缺失导致程序崩溃。

  3. 返回错误代码处理非法命令:在 CommandProcessor 类中,通过 isAvaliableCommand 方法返回错误代码来处理非法命令,并在输出文件中记录错误信息。

  4. 资源关闭保证:在所有涉及 IO 操作的类中,确保 BufferedReader 和 HttpURLConnection 等资源在使用完毕后被正确关闭,避免资源泄露。

  5. 参数检查:在 OlympicSearch 类中,检查命令行参数的数量,如果不足两个,将默认启动控制台输入命令。

9. 心得体会

这次任务的核心编程逻辑并不复杂,但由于缺乏对完整软件开发流程的经验,在诸如 Git 版本控制、测试环境搭建以及 Maven 项目管理等方面遇到了不少挑战。这些环节占用了大量时间,远超过实际编码的时间投入。然而,这些经历是软件工程中不可或缺的学习过程。随着不断实践和积累,我相信这些技能将逐渐内化为开发工作中的自然习惯。

这次实践深刻地展示了软件开发的多面性和复杂性。它让我意识到,编写代码仅仅是整个软件工程中的一小部分。版本控制、测试策略、持续集成等环节同样至关重要。如何有效地利用版本控制系统、设计全面的测试用例、实现自动化测试流程,这些都是需要我们不断学习和探索的领域。此外,项目的成功还高度依赖于前期的需求分析和架构设计。只有在这些基础工作做好的情况下,后续的编码过程才能更加顺畅和高效。这次作业不仅提升了我的技术能力,也让我对软件工程的整体流程有了更全面的认识。

...全文
333 回复 打赏 收藏 转发到动态 举报
AI 作业
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

111

社区成员

发帖
与我相关
我的任务
社区描述
202401_CS_SE_FZU
软件工程 高校
社区管理员
  • FZU_SE_TeacherL
  • 言1837
  • 防震水泥
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧