目录
- 寒假作业 疫情统计(2/2)
- 1、Github 连接
- 2、PSP 表格
- 3、解题思路描述
- 4、设计实现过程
- 5、代码说明
- 6、单元测试截图和描述
- 7、单元测试覆盖率优化和性能测试
- 8、代码规范的链接
- 9、心路历程与收获
- 10、5 个学习仓库
寒假作业 疫情统计(2/2)
这个作业属于哪个课程 | https://edu.cnblogs.com/campus/fzu/2020SpringW |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/fzu/2020SpringW/homework/10281 |
这个作业的目标 | 代码规范、GitHub使用、单元测试、编程 |
作业正文 | https://www.cnblogs.com/aahorse/p/12310680.html |
其他参考文献 | https://github.com/youlookwhat/DesignPattern |
1、Github 连接
https://github.com/aaHorse/InfectStatistic-main
2、PSP 表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 0.9*5*60 | 0.9*5*60 |
Estimate | 估计这个任务需要多少时间 | 0.1*5*60 | 0.1*5*60 |
Development | 开发 | 1*5*60 | - |
Analysis | 需求分析 (包括学习新技术) | 2*5*60 | 2*5*60 |
Design Spec | 生成设计文档 | 0.2*5*60 | - |
Design Review | 设计复审 | 0.2*5*60 | 0.5*5*60 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 0.5*5*60 | 0.5*5*60 |
Design | 具体设计 | 1*5*60 | 1*5*60 |
Coding | 具体编码 | 3*5*60 | 2*5*60 |
Code Review | 代码复审 | 0.4*5*60 | 0.5*5*60 |
Test | 测试(自我测试,修改代码,提交修改) | 1*5*60 | 3*5*60 |
Reporting | 报告 | 1*5*60 | 1*5*60 |
Test Report | 测试报告 | 0.5*5*60 | - |
Size Measurement | 计算工作量 | 0.2*5*60 | 0.5*5*60 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 1*5*60 | 2*5*60 |
合计 | 13*5*60 | 12*5*60 |
(备注:在家的计划都是按照一天学习 5 个小时来算的,每天其他的时间在社会实践)
3、解题思路描述
一开始看到作业要求的时候,有一种无从下手的感觉。在网上冲浪了几天后,甚至有了直接用 if-else 实现的念头。
几天后,助教发布了一篇关于作业引导和提示的博客,我对这篇博客进行了深入的学习和思考。非常感谢老师,感谢助教,通过这篇博客,我才有了一些想法。以下是解题思路:
在菜鸟教程,我找到了关于 Java 设计模式的的文档,看了一下提到的命令模式、状态模式、责任链模式,很遗憾的是,看不懂,不知道要用这些模式去实现什么功能;
转战 GitHub,我找到一个关于 Java 设计模式的 demo,是 Android 描述的,因为有学过 Android,看得懂,很快就上手了;
上手就好办了,用一个类封装命令的输入,用命令行模式实现 list 命令,用责任链模式实现日志行匹配,用一个类将省份的疫情封装起来;
一顿操作,就架构好了程序的框架,打通了输入输出,但是里面的具体实现还没开始写;
开始写各个命令参数的具体实现,学了正则表达式匹配日志行;
功能实现后,将项目改为 Maven 项目(之前只是一个简单的 Java 项目);
导 JUnit 包,写测试文件,标准输出文件,进行单元测试;
特别强调,期间各种懵逼,痛苦不堪!
4、设计实现过程
- 代码组织
- 关键函数的流程图
- 说明
代码主要分为以下几个部分:
① 命令行解析部分,通过上图可以看到,是通过 CmdArgs.java
实现的;
② 命令行参数执行部分,这部分通过命令模式实现 list,在MyList.java
中实现功能;
③ 日志行解析部分,这部分通过 public void out()
调用;
④ 测试部分,这部分在后面会有说明;
5、代码说明
- ① 命令行解析代码
/**
* 命令行参数数组解析
*
* @param args 命令行传来的参数数组
* @return 命令行参数数组解析后的键值对
*/
private HashMap parseArgs(String[] args) {
ArrayList argList = new ArrayList<>();
String argKey = null;
String[] valueMap = null;
//处理命令
valueMap = new String[1];
argList.add(args[0]);
argList.toArray(valueMap);
this.argMap.put(args[0], valueMap);
argList.clear();
//处理参数值
for (int i = 1; i < args.length; i++) {
String arg = args[i];
if (arg.startsWith("-")) {
if (argKey != null) {
valueMap = new String[argList.size()];
argList.toArray(valueMap);
this.argMap.put(argKey, valueMap);
argList.clear();
}
argKey = arg;
} else {
argList.add(arg);
}
}
if (argKey != null) {
valueMap = new String[argList.size()];
argList.toArray(valueMap);
this.argMap.put(argKey, valueMap);
argList.clear();
}
return this.argMap;
}
//------------------------代码说明----------------------------------
/*
根据命令行传进来的字符串数组,经过处理,转为通过
HashMap来存储。
① 对于传入的字符数组 String[] args,我们首先读取 args[0],既作为key也作 value,
存到map中;
② 然后通过一个 for 循环,以 '-' 开头的字符串作为键,两个 '-'之间的字符串作为值,
存到map中;
*/
- ② 命令行模式实现 list
public class Lib {
/**
* 执行命令
*
* @throws FileNotFoundException
*/
public void execute() throws FileNotFoundException {
if (this.args == null) {
return;
}
CmdArgs cmdArgs = new CmdArgs(this.args);
//比较是否为 list 命令
if (!cmdArgs.getCmd().equals(MyList.PARAMS[0])) {
throw new UnsupportedOperationException("命令输入错误");
}
/*
* list命令的参数,按照:list -log -date -province -type -out的顺序执行,
* 如果一个非必要参数缺省:-date -type -province,
* 仍然按照顺序执行缺省参数的默认值
* */
MyList list = new MyList(cmdArgs);
ControlCommand controlCommand = new ControlCommand();
//list -log
controlCommand.setCommands(0, new ListLogCommand(list));
//list -date
controlCommand.setCommands(1, new ListDateCommand(list));
//list -province
controlCommand.setCommands(2, new ListProvinceCommand(list));
//list -type
controlCommand.setCommands(3, new ListTypeCommand(list));
//list -out 输出在最后执行
controlCommand.setCommands(4, new ListOutCommand(list));
//执行全部命令
controlCommand.sureExecuteAll();
}
}
class MyList {
/**
* list命令操作的日志含有的省份,以及 -provice 参数含有的省份,包括 “全国”
* 通过键值对存放,比如:
* key="全国" value=ProvinceStatus对象
*/
public LinkedHashMap linkedHashMap;
public void log() {}
public void date() throws FileNotFoundException {}
public void province() {}
public void type() {}
public void out() {}
/**
* 形成责任链
*/
private static AbstractLogLineType getChainOfLogLine() {}
/**
* 获取需要读入的log日志路径
*
* @throws FileNotFoundException
*/
private void getLogPaths() throws FileNotFoundException {}
/**
* 读入日志信息
*/
private void readLog() {}
/**
* 获取 “全国” 的数据
*/
private void getProvinceStatusAll() {}
/**
* 删除不需要输出的省份
*/
private void deleteNotOutputProvicne() {}
/**
* 获取需要输出的信息
*/
private String getOutStr() {}
}
/**
* 命令的接口
*/
interface Command {
void execute() throws FileNotFoundException;
}
class ListLogCommand implements Command {}
class ListDateCommand implements Command {}
class ListProvinceCommand implements Command {}
class ListTypeCommand implements Command {}
class ListOutCommand implements Command {}
class ControlCommand {
/**
* list 总共有5种参数,分别是:-log -date -type -province -out
*/
private static final int CONTROL_SIZE = 5;
private Command[] commands;
public ControlCommand() {}
/**
* 添加一条待执行命令
*/
public void setCommands(int index, Command command) {}
/**
* 一条命令确认开始执行
*/
public void sureExecute(int index) throws FileNotFoundException {}
/**
* 全部命令确认开始执行
*/
public void sureExecuteAll() throws FileNotFoundException {}
}
//----------------------------代码说明--------------------------------
/*
以上是通过命令模式实现 list 的代码框架,说明几点:
① 为了适用于项目,命令模式并没有将 list 看成是一条命令,而是将它的每一个参数看作是命令。
换句话说,一条 list 在这里会被看成 5 条命令,并且通过 ControlCommand 中的
setCommands(int index, Command command)设定执行顺序,这 5 条命令会按照规定好的顺序执行,
它们的顺序是:list -log xxx -date xxx -province xxx xxx -type xxx xxx -out xxx
注意:即使某一条非必要的参数缺失,也会通过默认值补上并执行
② MyList 类是主要的执行类,其中,public void -out() 又是主要的执行函数
*/
- ③ 责任链模式匹配日志行
class MyList {
//.......
/**
* 形成责任链
*/
private static AbstractLogLineType getChainOfLogLine() {
NewInfectorLogLine newInfectorLogLine = new NewInfectorLogLine(AbstractLogLineType.LOGLINE_TYPE[0]);
NewSuspectLogLine newSuspectLogLine = new NewSuspectLogLine(AbstractLogLineType.LOGLINE_TYPE[1]);
InInfectorLogLine inInfectorLogLine = new InInfectorLogLine(AbstractLogLineType.LOGLINE_TYPE[2]);
InSuspectLogLine inSuspectLogLine = new InSuspectLogLine(AbstractLogLineType.LOGLINE_TYPE[3]);
DeathLogLine deathLogLine = new DeathLogLine(AbstractLogLineType.LOGLINE_TYPE[4]);
CureLogLine cureLogLine = new CureLogLine(AbstractLogLineType.LOGLINE_TYPE[5]);
DefiniteLogLine definiteLogLine = new DefiniteLogLine(AbstractLogLineType.LOGLINE_TYPE[6]);
ExcludeLogLine excludeLogLine = new ExcludeLogLine(AbstractLogLineType.LOGLINE_TYPE[7]);
MismatchingLogLine mismatchingLogLine = new MismatchingLogLine(AbstractLogLineType.LOGLINE_TYPE[8]);
newInfectorLogLine.setNextLogType(newSuspectLogLine);
newSuspectLogLine.setNextLogType(inInfectorLogLine);
inInfectorLogLine.setNextLogType(inSuspectLogLine);
inSuspectLogLine.setNextLogType(deathLogLine);
deathLogLine.setNextLogType(cureLogLine);
cureLogLine.setNextLogType(definiteLogLine);
definiteLogLine.setNextLogType(excludeLogLine);
excludeLogLine.setNextLogType(mismatchingLogLine);
return newInfectorLogLine;
}
/**
* 读入日志信息
*/
private void readLog() {
AbstractLogLineType abstractLogLineType = getChainOfLogLine();
for (int i = 0; i < this.logPath.size(); i++) {
String content = FileOperate.readFile(this.dir + this.logPath.get(i));
if (content != null) {
String[] logLine = content.split("#");
for (int j = 0; j < logLine.length; j++) {
//处理日志行
abstractLogLineType.matchLogLine(logLine[j], this.linkedHashMap);
}
}
}
}
}
/**
* 日志行
*/
abstract class AbstractLogLineType {
/**
* 正则表达式
*/
public final static String[] LOGLINE_TYPE = {
"(\\S+) 新增 感染患者 (\\d+)人",
"(\\S+) 新增 疑似患者 (\\d+)人",
"(\\S+) 感染患者 流入 (\\S+) (\\d+)人",
"(\\S+) 疑似患者 流入 (\\S+) (\\d+)人",
"(\\S+) 死亡 (\\d+)人",
"(\\S+) 治愈 (\\d+)人",
"(\\S+) 疑似患者 确诊感染 (\\d+)人",
"(\\S+) 排除 疑似患者 (\\d+)人",
"([\\s\\S]*)"
};
/**
* 日志行对应的正则表达式
*/
protected String logLineTypeRegExp;
/**
* 责任链中的下一个元素
*/
protected AbstractLogLineType nextLogLineType;
public void setNextLogType(AbstractLogLineType nextLogLineType) {
this.nextLogLineType = nextLogLineType;
}
/**
* 匹配责任链
*/
public void matchLogLine(String logLineStr, LinkedHashMap linkedHashMap) {
if (logLineStr.matches(logLineTypeRegExp)) {
executeLogLine(logLineStr, linkedHashMap);
return;
}
if (nextLogLineType != null) {
nextLogLineType.matchLogLine(logLineStr, linkedHashMap);
}
}
protected abstract void executeLogLine(String logLineStr, LinkedHashMap linkedHashMap);
}
/**
* <省> 新增 感染患者 n 人
*/
class NewInfectorLogLine extends AbstractLogLineType {
public NewInfectorLogLine(String logLineTypeRegExp) {
this.logLineTypeRegExp = logLineTypeRegExp;
}
@Override
protected void executeLogLine(String logLineStr, LinkedHashMap linkedHashMap) {
Pattern pattern = Pattern.compile(AbstractLogLineType.LOGLINE_TYPE[0]);
Matcher matcher = pattern.matcher(logLineStr);
if (matcher.find()) {
String province = matcher.group(1);
int num = Integer.parseInt(matcher.group(2));
if (!linkedHashMap.containsKey(province)) {
linkedHashMap.put(province, new ProvinceStatus(province));
}
ProvinceStatus provinceStatus = linkedHashMap.get(province);
provinceStatus.setInfect(provinceStatus.getInfect() + num);
linkedHashMap.put(province, provinceStatus);
}
}
}
/**
* <省> 新增 疑似患者 n 人
*/
class NewSuspectLogLine extends AbstractLogLineType {}
/**
* <省1> 感染患者 流入 <省2> n人
*/
class InInfectorLogLine extends AbstractLogLineType {}
/**
* <省1> 疑似患者 流入 <省2> n人
*/
class InSuspectLogLine extends AbstractLogLineType {}
/**
* <省> 死亡 n人
*/
class DeathLogLine extends AbstractLogLineType {}
/**
* <省> 治愈 n人
*/
class CureLogLine extends AbstractLogLineType {}
/**
* <省> 疑似患者 确诊感染 n人
*/
class DefiniteLogLine extends AbstractLogLineType {}
/**
* <省> 排除 疑似患者 n人
*/
class ExcludeLogLine extends AbstractLogLineType {}
/**
* 不匹配的情况
*/
class MismatchingLogLine extends AbstractLogLineType {}
//------------------------------代码说明---------------------------
/*
通过责任链模式来匹配日志行,避免使用过多的 if-else ,有利于项目的拓展和维护
说明几点:
① 8种日志行,每一种都有一条对应的正则表达式来匹配,如果不匹配,则在责任链的最后
面由通配类来接住,通配类的正则表达式为:"([\\s\\S]*)"
② 当一个日志行对应的类获得执行时,通过设计好的正则表达式抽取出需要的变量,用这些
变量来操作一个全局的 LinkedHashMap linkedHashMap
③ 所有日志行执行结束后,就会将日志信息转为:
LinkedHashMap linkedHashMap,再由这个map处理输出的问题
*/
- ④ 省份实体类
/**
* 省/全国类
*/
class ProvinceStatus {
/**
* 省份名称,包括“全国”
*/
private String name;
/**
* 感染患者数量
*/
private int infect;
/**
* 疑似患者数量
*/
private int suspect;
/**
* 治愈患者数量
*/
private int cure;
/**
* 死亡患者数量
*/
private int death;
public ProvinceStatus(String name) {
this.name = name;
// 默认等于 0
this.infect = 0;
this.suspect = 0;
this.cure = 0;
this.death = 0;
}
/**
* @param typeValue 命令行参数 -type 指定的一种输出类型
* @return 返回整理好的字符串
*/
public String getOutTypeStr(String typeValue) {
switch (typeValue) {
case "ip":
return " " + "感染患者" + this.infect + "人";
case "sp":
return " " + "疑似患者" + this.suspect + "人";
case "cure":
return " " + "治愈" + this.cure + "人";
case "dead":
return " " + "死亡" + this.death + "人";
default:
throw new IllegalStateException("Unexpected value: " + typeValue);
}
}
}
//-------------------------------代码说明--------------------------
/*
有必要展示出来,但是没什么需要说明的东西 -:)
*/
6、单元测试截图和描述
单元测试分为两种,一种是仅通过函数内的代码来实现,一种是将命令集写入文件,并且将命令对应的标准输出也写入文件,再通过处理文件操作来实现。以下是代码截图:
- ① 单元测试目录结构
- ② CmdArgs 类部分函数的测试
- ③ MyList 类部分函数测试
- ④ 通过文件来进行测试
存储命令集
存储命令对应的标准输出文件路径
测试函数
运行结果
7、单元测试覆盖率优化和性能测试
以 MyListTest 测试类为例,说明覆盖率优化问题
优化前:
(分析发现,因为测试中没有用命令模式添加命令,而是直接调用命令执行,所以命令模式板块全部没有被调用到。优化措施:改用命令模式调用命令执行。)
优化后:
(命令模块被调用到,单元测试覆盖率明显提升)
性能测试
(因为看不懂网上关于 JProfile 使用的文档,摸索了一下,但是对于如何利用这个工具进行性能优化还是没有入门,现在对于这个工具的认识仅停留在这是一个用于查找程序内存泄漏的工具并且和服务器开发密切相关。至于性能方面,因为这个本项目对于性能方面的要求并不高,并且我的程序是单线程的,运行结束就停止了,继续研究下去的意义不大。我猜测是因为我对这个工具的认识还没有入门,或许存在某一个按钮可以将程序循环执行。下面我演示的是通过 JProfile 查找内存泄漏的方法,当然,内存泄漏的地方是我故意用死循环添加上去的)
添加死循环
执行测试
(多次 Run GC 仍然存在锯齿图,体现内存泄漏)
(show Source 定位到该死循环)
大家都放了这张图
8、代码规范的链接
https://github.com/aaHorse/InfectStatistic-main/blob/master/221701414/codestyle.md
9、心路历程与收获
做完整个项目下来,和之前做项目的感觉一样:非常的痛苦。做项目的感觉和平时学习的感觉是不一样的。平时学习遇到问题可以跳过,甚至放弃,但是做项目的时候,就不得不迎难而上。
选课的时候,我无意间在博客园看到汪老师的博客,大概知道了这是一门真·实践课。做项目虽然痛苦,但是我当时还是用了一个很大的分值投了这门课,并且选上了。起码到目前,我没有后悔的念头,希望能坚持下来。
做项目我的原则是认真对待,尽早开始,尽力完成。
这次作业,最需要感谢的是助教发的作业引导。通过学习和实践,作业引导里面的东西也是我收获最大的地方。通过里面的博客,我才知道命令模式、责任链模式、单元测试这些东西,在作业中才懂得应用。希望以后每次作业,都能发一下关于作业引导的博客。
阅读了《构建之法》,我看到了优秀程序员应该具备的思想和能力,我也督促自己,朝着这个方向去增长自己的技能点,努力成为一个优秀的程序员。
10、5 个学习仓库
① mall 商城
链接:https://github.com/macrozheng/mall
介绍:mall 项目是一套电商系统,包括前台商城系统及后台管理系统,基于 SpringBoot+MyBatis 实现,采用 Docker 容器化部署。
备注:一直都想学这个项目,之前曾经研究过一会,是一个值得花时间研究的好项目。
② 算法与数据结构
链接:https://github.com/doubleview/data-structure
介绍:此项目是基于 java 语言的关于数据结构的代码实现,包含所有经典数据结构算法,并且注释完善,非常适合了解和学习数据结构。
备注:现在有点后悔当初学数据结构的时候用 C 语言来描述,C 语言确实可以学到算法的思想,但是 Java 才是偏应用的,所以一定要补上这个知识点。
③ 开发模式
链接:https://github.com/youlookwhat/DesignPattern
介绍:Java 设计模式归纳 (观察者、工厂、抽象工厂、单例、策略、适配器、命令、装饰者、外观、模板方法、状态、建造者、原型、享元、代理、桥接、组合、迭代器、中介者、备忘录、解释器、责任链)。
备注:这次作业后发现,用开发模式写出来的代码真的一目了然,多学习,多实践。
④Java 学习+面试
链接:https://github.com/Snailclimb/JavaGuide
介绍:一份涵盖大部分 Java 程序员所需要掌握的核心知识。
备注:这个项目 Start 比较多,值得学习。
⑤Java 的 List 框架
链接:https://github.com/CarpenterLee/JCFInternals
介绍:主要从数据结构和算法层面分析 JCF 中 List, Set, Map, Stack, Queue 等典型容器,结合生动图解和源代码,帮助读者对 Java 集合框架建立清晰而深入的理解。
备注:List 框架作为 Java 应用最广泛的框架之一,值得花时间进行深入学习。