软件工程第二次作业--文件读取

本文介绍了如何通过FastJson解析JSON数据、从网站抓取比赛数据、设计接口实现对世界游泳锦标赛跳水项目数据的统计,以及后续的性能改进、单元测试和异常处理。作者通过实际操作展示了软件工程实践中的关键步骤和经验体会。
摘要由CSDN通过智能技术生成
这个作业属于哪个课程2302软件工程社区
这个作业要求在哪里软件工程第二次作业–文件读取
这个作业的目标实现一个对世界游泳锦标赛跳水项目相关赛事数据进行统计的控制台程序
其他参考文献《构建之法》、CSDN、工程师的能力评估和发展单元测试和回归测试

1. Gitcode项目地址

仓库地址

2. PSP表格
PSPPersonal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划6040
• Estimate• 估计这个任务需要多少时间2015
Development开发9901375
• Analysis• 需求分析 (包括学习新技术)100200
• Design Spec• 生成设计文档3025
• Design Review• 设计复审2020
• Coding Standard• 代码规范 (为目前的开发制定合适的规范)3040
• Design• 具体设计6050
• Coding• 具体编码600660
• Code Review• 代码复审3020
• Test• 测试(自我测试,修改代码,提交修改)120360
Reporting报告6090
• Test Repor• 测试报告2030
• Size Measurement• 计算工作量1020
• Postmortem & Process Improvement Plan• 事后总结, 并提出过程改进计划3040
合计11101505
3. 解题思路描述
3.1 数据的获取

获取运动员的数据相对简单,此处以获取比赛结果的数据作为示例。

  1. 打开所要获取数据的网站(Competition Athletes | World Aquatics Official),找到界面下方导航栏下的RESULTS,点击进入后按F12打开开发者工具img
  2. 发现events.json的请求接口地址为https://api.worldaquatics.com/fina/competitions/2969/events
    在这里插入图片描述
  3. 分析events.json的结构,得到DisciplineList中的Id为比赛项目的Id,DisciplineName为比赛项目的名称
    在这里插入图片描述
  4. 在页面上点击切换比赛项目,注意到页面请求了对应比赛的json文件img
  5. 分析该请求的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数据的解析。

  1. 获取FastJson的jar包,使用IDEA导入到lib文件夹
  2. 使用http请求获取json数据,根据数据结构使用fastjson的JSON类将json转为JSONArray或JSONObject类,再根据json结构提取自己需要的数据,将数据映射到实体类
  3. 最后利用StringBuilder来拼接获得的数据用于最后的输出
3.3 input文件中信息的处理
  1. 使用 BufferedReader 来按行读取文件内容。
  2. 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. 异常处理
  1. 命令行错误处理:
       if (args.length != Consts.ARGS_LENGTH) {
           System.out.println("命令行参数格式错误!应为:Java -jar DWASearch.jar input.txt output.txt");
           System.exit(-1);
       }
  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. 心得体会

在本次软件工程实践作业中,我深刻认识到一个项目的完成远远超出了简单的让代码运行通过。以往的大作业通常只需要提交源代码,而这次的作业让我经历了从规划到实现再到测试编写报告的完整流程,让我意识到了自己在许多方面都不够严谨。

  • 在以往的开发中,我很少接触单元测试这个概念,也从未深入学习过如何编写和执行单元测试。然而,在这次作业中,我不得不学习并实践了单元测试的方法和技巧。通过编写全面、细致的单元测试,我能够更好地了解代码的功能和逻辑,并及时发现和修复潜在的问题。
  • 过去,我很少关注代码的性能问题,只注重功能的实现。然而,在这次作业中,我深刻认识到性能优化对于项目的重要性。通过使用性能测试工具,我能够确定代码中的性能瓶颈,并采取相应的优化措施,以提高代码在大规模数据处理时的效率和响应速度。
  • 自己在时间估计方面存在问题,未能准确评估所需的时间,导致在截止日期临近时才完成所有任务。这给我敲响了警钟,以后我会更加谨慎地评估项目所需的时间,并制定合理的计划,以保证能从容地完成项目。
  • 28
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值