这个作业属于哪个课程 | |
---|---|
这个作业要求在哪里 | |
这个作业的目标 | GitHub 初使用、代码规范制定、需求分析、单元测试、覆盖率分析、性能分析 |
作业正文 | 就是本文 |
其他参考文献 | JUnit5、JProfiler |
一、GitHub 仓库地址
https://github.com/xjliang/InfectStatistic-main
二、PSP 表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 45 |
Estimate | 估计这个任务需要多少时间 | 40 | 30 |
Development | 开发 | 40 | 45 |
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 | 报告 | 100 | 45 |
Test Report | 测试报告 | 100 | 120 |
Size Measurement | 计算工作量 | 40 | 50 |
Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 50 | 50 |
合计 | 865 | 985 |
三、解题思路描述
解析命令行参数
首先分析了一下命令行参数的结构,参数是键值对的形式,自然想到用 Map 映射;由于一个参数后可以跟多个参数值,就把 List 作为映射中的值。程序后续需要再使用到命令行参数,因此将用一个数据结构将这里的 Map 包装起来,再提供一些接口供后续程序的使用,如判断参数是否存在,根据参数名获取参数值等必需接口。
统计日志文件
读取文件使用了文件的输入输出,我专门设计了一个类用于读取指定的日志文件。
统计日志文件前需根据给定的日期过滤掉该日期之后的日志,在将剩下的日志文件列表交给日志处理器处理。
日志处理器依次读取每个日志文件,我首先想到的是直接 if else if esse 判断每行特殊的 token,但后来考虑到这种方式不易扩展,后续如果在增加一些新的情况,可能要修改不止一处代码,扩展性较差。于是我开始考虑通过正则表达式匹配,尽管在性能上相对较差,但扩展起来相当方便,只需要一行正则就可搞定一种情况。
匹配指定情况后,需要将该结果存储起来,供后续输出使用,于是我又设计了一个统计类,用于存储各个省份包含了4 种人群的人数,使用 Map
根据命令行参数输出结果到指定文件
输出前先通过参数判断是否有指定输出省份,获取待输出省份的列表。
如果待输出省份种有包含“全国”则需要将把所有省份的统计结果累加,统计出相应结果。
最后输出前判断是否指定了输出患病人群的类别,无指定直接输出所有类别的患病情况;有指定需要按指定顺序输出。
四、设计实现过程
程序模块设计
类图设计
算法流程
五、代码说明
存储命令行参数的数据结构如下:
class CommandArgs { private List
commandArgs = new ArrayList<>(); private Map > 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
regexGroup2(String soap, String regex) { final Pattern pattern = Pattern.compile(regex, Pattern.DOTALL); final Matcher matcher = pattern.matcher(soap); if (matcher.find()) { List result = new ArrayList<>(); result.add(matcher.group(1)); result.add(matcher.group(2)); return result; } return null; } public static List regexGroup3(String soap, String regex) { final Pattern pattern = Pattern.compile(regex, Pattern.DOTALL); final Matcher matcher = pattern.matcher(soap); if (matcher.find()) { List 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
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 性能报告总览:
内存使用情况:
八、我的代码规范的链接
代码规范
九、心路历程与收获
这次寒假正好在家里好好沉下心完成了这次寒假作业,在还没有认真阅读完需求之前,我就尝试动手完成了这个作业的代码。
之后才看到要学习一下构建之法,于是我认真地把前三章都阅读了一遍,顺便把学习笔记录在一篇博客 构建之法学习笔记(第一章~第三章)里。
在重新编写这次作业的代码前,我先写好了 PSP 规划表,说实话,很多部分当时还是云里雾里的,只能凭自己的感觉就定下了一个时间。编码前先设计好所有类图花了我不少时间。
实践证明,软件工程需要清晰的代码逻辑思维,如何在编码前设计好各个模块之间的关联?编码时如何做好单测?单测应该什么时候做?这些问题我之前都没有考虑过,以至于我最后单独拿出一段时间来重新设计需要单测的模块。经历了这次惨痛的教训,我认识到事前做好规划的重要性,无论是生活还是学习,是从事编程还是其他行业,这一点我觉得都是需要事前考虑清楚的。
同时,软件工程不像 ACM 之类的需要打比赛,拼编码速度(打字员),它是一门工程学科,需要的是像科学家一样思考,做好系统分析和设计,不是 copy and paste 那样简单的事情。
麻雀虽小,五脏俱全,虽然这次作业仍然只是个人的一个小作业,但是它也用到了许多技术,如单元测试,版本控制等,这些是游离于语言之外的技能,也是需要掌握的。
将来可能我们很多人会从事程序员这个行业,但是,我们大部分人都不是天才,不会去做那种很高深的项目,尽管我们将来接触的只是一些简单的项目,但还是需要一些扩展技能的。
很多时候我们可能会停滞不前,认为自己到达了自己的天花板,但是程序员这个行业是需要不断学习的,永远有值得我们学习的东西,只有怀着学徒的心态,才能越走越远。
十、第一次作业中技术路线图相关的 5 个仓库
Spring Boot 学习示例
Spring Boot 使用的各种示例,以最简单、最实用为标准,此开源项目中的每个示例都以最小依赖,最简单为标准,帮助初学者快速掌握 Spring Boot 各组件的使用。
VBlog
V 部落是一个多用户博客管理平台,采用 Vue+SpringBoot 开发。项目演示地址: http://45.77.146.32:8081/index.html
Spring MVC Showcase
通过小而简单示例演示 Spring MVC Web 框架的功能。在回顾了这个展示之后,您应该对 Spring MVC 可以做什么有一个很好的了解,并了解它的易用性。
Spring Integration Samples
欢迎使用 Spring Integration Samples 存储库,该存储库提供了 50 多个示例来帮助您学习 Spring Integration。为了简化您的体验,Spring Integration 示例分为 4 个不同的类别:basic,intermediate, advanced, applications。
CS-Notes
技术面试必备基础知识、Leetcode、计算机操作系统、计算机网络、系统设计、Java、Python、C++
(Done)