软工实践寒假作业(2/2)
这个作业属于哪个课程 | <班级的链接> |
---|---|
这个作业要求在哪里 | <作业要求的链接> |
这个作业的目标 | GitHub 初使用、代码规范制定、需求分析、单元测试、覆盖率分析、性能分析 |
作业正文 | https://www.cnblogs.com/gnulxj/p/12322336.html |
其他参考文献 | JUnit5、JProfiler |
一、GitHub 仓库地址
https://github.com/xjliang/InfectStatistic-main
二、PSP 表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 40 | 30 |
Estimate | 估计这个任务需要多少时间 | 40 | 30 |
Development | 开发 | 465 | 590 |
Analysis | 需求分析 (包括学习新技术) | 60 | 45 |
Design Spec | 生成设计文档 | 40 | 30 |
Design Review | 设计复审 | 45 | 30 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 40 | 60 |
Design | 具体设计 | 60 | 30 |
Coding | 具体编码 | 120 | 200 |
Code Review | 代码复审 | 40 | 45 |
Test | 测试(自我测试,修改代码,提交修改) | 60 | 150 |
Reporting | 报告 | 190 | 220 |
Test Report | 测试报告 | 100 | 120 |
Size Measurement | 计算工作量 | 40 | 50 |
Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 50 | 50 |
合计 | 695 | 850 |
三、解题思路描述
1. 解析命令行参数
首先分析了一下命令行参数的结构,参数是键值对的形式,自然想到用 Map 映射;由于一个参数后可以跟多个参数值,就把 List 作为映射中的值。程序后续需要再使用到命令行参数,因此将用一个数据结构将这里的 Map 包装起来,再提供一些接口供后续程序的使用,如判断参数是否存在,根据参数名获取参数值等必需接口。
2. 统计日志文件
读取文件使用了文件的输入输出,我专门设计了一个类用于读取指定的日志文件。
统计日志文件前需根据给定的日期过滤掉该日期之后的日志,在将剩下的日志文件列表交给日志处理器处理。
日志处理器依次读取每个日志文件,我首先想到的是直接 if else if esse 判断每行特殊的 token,但后来考虑到这种方式不易扩展,后续如果在增加一些新的情况,可能要修改不止一处代码,扩展性较差。于是我开始考虑通过正则表达式匹配,尽管在性能上相对较差,但扩展起来相当方便,只需要一行正则就可搞定一种情况。
匹配指定情况后,需要将该结果存储起来,供后续输出使用,于是我又设计了一个统计类,用于存储各个省份包含了4 种人群的人数,使用 Map<String, Stat> 将省份名与统计结果映射起来。
3. 根据命令行参数输出结果到指定文件
输出前先通过参数判断是否有指定输出省份,获取待输出省份的列表。
如果待输出省份种有包含“全国”则需要将把所有省份的统计结果累加,统计出相应结果。
最后输出前判断是否指定了输出患病人群的类别,无指定直接输出所有类别的患病情况;有指定需要按指定顺序输出。
四、设计实现过程
程序模块设计
类图设计
算法流程
五、代码说明
-
存储命令行参数的数据结构如下:
class CommandArgs { private List<String> commandArgs = new ArrayList<>(); private Map<String, List<String>> optionValuesMap = new HashMap<>(); }
-
代码中使用正则表达式匹配各个省份的统计情况,正则表达式设计如下:
final static private String regex1 = "(^\\W+) 新增 感染患者 (\\d+)人"; final static private String regex2 = "(^\\W+) 新增 疑似患者 (\\d+)人"; final static private String regex3 = "(^\\W+) 感染患者 流入 (\\W+) (\\d+)人"; final static private String regex4 = "(^\\W+) 疑似患者 流入 (\\W+) (\\d+)人"; final static private String regex5 = "(^\\W+) 死亡 (\\d+)人"; final static private String regex6 = "(^\\W+) 治愈 (\\d+)人"; final static private String regex7 = "(^\\W+) 疑似患者 确诊感染 (\\d+)人"; final static private String regex8 = "(^\\W+) 排除 疑似患者 (\\d+)人";
-
取得正则表达式中字符串的方法如下,其中
regexGroup2
用于获取两个参数,regexGroup3
用于获取三个参数:public static List<String> regexGroup2(String soap, String regex) { final Pattern pattern = Pattern.compile(regex, Pattern.DOTALL); final Matcher matcher = pattern.matcher(soap); if (matcher.find()) { List<String> result = new ArrayList<>(); result.add(matcher.group(1)); result.add(matcher.group(2)); return result; } return null; } public static List<String> regexGroup3(String soap, String regex) { final Pattern pattern = Pattern.compile(regex, Pattern.DOTALL); final Matcher matcher = pattern.matcher(soap); if (matcher.find()) { List<String> result = new ArrayList<>(); result.add(matcher.group(1)); result.add(matcher.group(2)); result.add(matcher.group(3)); return result; } return null; }
-
统计类的数据结构如下:
class ProvinceStat { private int numIP; // #infected private int numSP; // #suspected private int numCure; // #cured private int numDead; // #dead public ProvinceStat() { this.numIP = 0; this.numSP = 0; this.numCure = 0; this.numDead = 0; } }
-
Lib.parse(String[] args) 是算法主流程,首先使用
ArgsParser
解析命令行参数,然后根据命令行参数获取需要解析的 log 文件列表,将该列表交给LogParser
解析得到各个省份的统计结果,最后通过outputResult
输出。public void parse(String[] args) throws IOException, DataFormatException { this.args = args; this.commandArgs = ArgsParser.parse(args); String logDir = commandArgs.getOptionValues("log").get(0); String logDeadline = "9999-99-99"; if (commandArgs.containsOption("date")) { logDeadline = commandArgs.getOptionValues("date").get(0); } String[] logFiles = getOlderFiles(logDir, logDeadline); for (int i = 0; i < logFiles.length; i++) { logFiles[i] = logDir + '\\' + logFiles[i]; } LogParser logParser = new LogParser(); this.provinceStatMap = logParser.parse(logFiles); outputResult(); }
-
LogParser.parse 用于解析日志文件,解析完成后返回结果映射:
for (String logFile : logFiles) { BufferedReader bufferedReader = new BufferedReader(new FileReader(logFile)); String line; System.out.println("parsing file \"" + logFile + "\"..."); while ((line = bufferedReader.readLine()) != null) { if (line.startsWith("//")) { // 跳过注释行 continue; } List<String> result; if ((result = regexGroup2(line, regex1)) != null) { ProvinceStat provinceStat = getProvinceStat(result.get(0)); provinceStat.incrNumIP(Integer.parseInt(result.get(1))); } else if ((result = regexGroup2(line, regex2)) != null) { // ... } el se if ((result = regexGroup3(line, regex3)) != null) { // ... } else if ((result = regexGroup3(line, regex4)) != null) { // ... } else if ((result = regexGroup2(line, regex5)) != null) { // ... } else if ((result = regexGroup2(line, regex6)) != null) { // ... } else if ((result = regexGroup2(line, regex7)) != null) { // ... } else if ((result = regexGroup2(line, regex8)) != null) { // ... } else { // exception System.out.println("exception"); throw new DataFormatException(); } } bufferedReader.close(); } return provinceStatMap;
六、单元测试截图和描述
All Tests
@Test01: args parser for log
用于测试对命令行参数 log
的解析是否正确。
@Test02: args parser for out
用于测试对命令行参数 out
的解析是否正确
@Test03: args parse for province
用于测试对命令行参数 province
的解析是否正确 (可以包含多个值)
@Test04: args parse for type
用于测试对命令行参数 type
的解析是否正确 (可以包含多个值)
@Test05: get older log file list
用于获取日期不超过指定日期的日志文件列表。
@Test06: decrease province IP Exception
用于测试减少患病人员数量越界异常(OutOfBoundException):0 + 3 - 6 = -3 < 0。
@Test07: increase IP of province
用于测试增加某个省份的患病数量:0 + 3 + 2 = 5。
@Test08: province migrate IP
用于测试省份患病人员迁移:
省份统计 | 迁移前 | 迁移后 |
---|---|---|
source | 0 + 3 = 3 | 3 - 2 = 1 |
target | 0 + 2 = 2 | 2 + 2 = 4 |
@Test09: province migrate SP
用于测试省份疑似病人员迁移:
省份统计 | 迁移前 | 迁移后 |
---|---|---|
source | 0 + 3 = 3 | 3 - 2 = 1 |
target | 0 + 2 = 2 | 2 + 2 = 4 |
@Test10: regex with 2 groups
用于测试解析带两个组的正则表达式。
七、单元测试覆盖率优化和性能测试
可以看到 ProvinceStat 覆盖率还有替身空间,于是我清理了一些不可能用到的接口,如死亡患者减少、治愈患者减少等接口,之后再进行测试,结果如下:
JProfiler 性能报告总览:
内存使用情况:
八、我的代码规范的链接
https://github.com/xjliang/InfectStatistic-main/blob/master/221701107/codestyle.md
九、心路历程与收获
博客连接 https://www.cnblogs.com/gnulxj/p/12322126.html
十、第一次作业中技术路线图相关的 5 个仓库
-
Spring Boot 使用的各种示例,以最简单、最实用为标准,此开源项目中的每个示例都以最小依赖,最简单为标准,帮助初学者快速掌握 Spring Boot 各组件的使用。
-
V 部落是一个多用户博客管理平台,采用 Vue+SpringBoot 开发。项目演示地址: http://45.77.146.32:8081/index.html
-
Spring MVC Showcase
通过小而简单示例演示 Spring MVC Web 框架的功能。在回顾了这个展示之后,您应该对 Spring MVC 可以做什么有一个很好的了解,并了解它的易用性。 -
欢迎使用 Spring Integration Samples 存储库,该存储库提供了 50 多个示例来帮助您学习 Spring Integration。为了简化您的体验,Spring Integration 示例分为 4 个不同的类别:basic,intermediate, advanced, applications。
-
📚 技术面试必备基础知识、Leetcode、计算机操作系统、计算机网络、系统设计、Java、Python、C++
(Done)