软工寒假实践作业(2/2)----疫情统计
作业描述
这个作业属于哪个课程 | 2020春|W班(福州大学) |
---|---|
这个作业要求在哪里 | 寒假作业(2/2)——疫情统计 |
这个作业的目标 | 本次作业主要考察Git、GitHub使用 代码规范意识,一定的程序设计能力(基于命令行) PSP,以及单元测试和性能分析改进。 |
作业正文 | 作业正文 |
其他参考文献 | 《码出高效_阿里巴巴Java开发手册》 工程师的能力评估和发展 寒假第二次作业引导和提示 |
我的Github仓库地址
本次作业的PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 15 | 40 |
Estimate | 估计这个任务需要多少时间 | 600 | 680 |
Development | 开发 | 550 | 620 |
Analysis | 需求分析 (包括学习新技术) | 50 | 20 |
Design Spec | 生成设计文档 | 30 | 40 |
Design Review | 设计复审 | 5 | 5 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 40 | 25 |
Design | 具体设计 | 20 | 30 |
Coding | 具体编码 | 400 | 470 |
Code Review | 代码复审 | 20 | 50 |
Test | 测试(自我测试,修改代码,提交修改) | 30 | 70 |
Reporting | 报告 | 80 | 90 |
Test Report | 测试报告 | 20 | 30 |
Size Measurement | 计算工作量 | 5 | 15 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 10 | 10 |
合计 | 1875 | 2195 |
思路描述
-
先大概看一下所需要用到的大方向:
- 要处理日志文本,需要对文件进行读写。
- 日志中的内容有多种匹配格式,需要使用正则表达式
- 直接传入命令行参数,需要使用命令行参数
-
根据大方向利用发散思维从大架构思考到细节方面:
-
参考责任链模式和命令模式优化:
将命令、处理类、发出类抽象出来,降低耦合,增加代码的可扩展性
将匹配正则表达式时多个if-else语句转化为多个处理函数并链接,优化代码。
设计实现过程
-
先处理最基本的功能:输入一个规定格式文本后,可以处理其中的数据。
- 分别建立不同正则表达式的模式。将模式处理函数抽象为Handler,然后分别实例化为8种对应的处理函数。然后将8种对应处理函数串接起来成为责任链,届时只需传入对应的文本行进入责任链即可得到对应的结果。如果项目事后扩展时需要添加新的处理函数,直接接入责任链即可。
- 对于数据的保存,就将每个省的患病情况封装成一个省情况类,再将省情况串成一个省列表方便整体的管理。
-
再进一步,从日志中获得规定格式文本传入责任链
处理日志分为两步:1.读写文件 2.对日志内容进行处理。
- 读写文件时,需要文件读写类,即一个文件处理类并写对应的操作函数即可,这部分设计比较简单。
- 处理文件时,要考虑到会传入不同的命令和未来命令的增多,所以要使用命令模式,将处理日志的函数抽象,作为命令的接收者。然后用文件处理类来包括这个日志处理类。
- 在处理日志的时候还要关注到日志的时间命名,所以在文件处理类中需要对文件名进行处理。
-
最后,需要处理传入的命令行参数
根据命令模式,将这一情况抽象出命令类、命令的传递者和命令的解析者,至于命令的接收者也就是处理者已经在第二步中封装完成。将这几者抽象出来变成松耦合的关系,方便之后命令增多时,直接新建一个命令类,并且在对应的类中写出处理和传递的函数即可。
最终完成流程图:
代码说明
从设计出发:
第一步,能够完成对某一行文本进行解析的函数:
首先将匹配模式设为全局变量,如果之后情况更加复杂则直接修改即可:
static Pattern ADD_EXACT_PATIENT = Pattern.compile("(\\S+) 新增 感染患者 (\\d+)人");
static Pattern ADD_DOUBTED_PATIENT = Pattern.compile("(\\S+) 新增 疑似患者 (\\d+)人");
static Pattern MOVE_EXACT_PATIENT = Pattern.compile("(\\S+) 感染患者 流入 (\\S+) (\\d+)人");
static Pattern MOVE_DOUBTED_PATIENT = Pattern.compile("(\\S+) 疑似患者 流入 (\\S+) (\\d+)人");
static Pattern CURED_PATIENT = Pattern.compile("(\\S+) 治愈 (\\d+)人");
static Pattern DEAD_PATIENT = Pattern.compile("(\\S+) 死亡 (\\d+)人");
static Pattern DOUBTED_TO_EXACT = Pattern.compile("(\\S+) 疑似患者 确诊感染 (\\d+)人");
static Pattern DOUBTED_TO_NONE = Pattern.compile("(\\S+) 排除 疑似患者 (\\d+)人");
然后抽象出处理日志行的类
/*
定义处理文档的抽象类
*/
public abstract class LogHandler {
protected LogHandler successor;
public LogHandler getSuccessor() {
return successor;
}
public void setSuccessor(LogHandler successor) {
this.successor = successor
}
/*
抽象处理方法
@param line:文本行
*/
public abstract void handleRequest(String line);
}
通过successor
可以实现各个类的链接
随后实体化为具体的处理类,此处以处理某省新出现感染人群的情况为例
/*
处理感染病人类
*/
public class AddExactHandler extends LogHandler {
@Override
public void handleRequest(String line) {
Matcher m1 = ADD_EXACT_PATIENT.matcher(line);
if (m1.matches()) {
dealAddExact(line);
} else {
getSuccessor().handleRequest(line);
}
}
}
当匹配完成时,则执行处理增加感染人数的函数dealAddExact(line)
/*
对符合增加感染人数的行作数据处理
@param 日志行
*/
public void dealAddExact(String line) {
//将字符串以空格分割为多个字符串
String[] strArray = line.split(" ");
int n = Integer.parseInt(strArray[3].replace("人", ""));
for (int i = 0; i < PROVINCE_NUM; i++)
{
if (strArray[0].equals(PROVINCE[i]))
{
//全国感染人数增加
PROVINCE_LIST.provinceList[0].addInfected(n);
//全国可被展示
PROVINCE_LIST.provinceList[0].canBeShown();
//当地区感染人数增加
PROVINCE_LIST.provinceList[i].addInfected(n);
//当地区可被展示
PROVINCE_LIST.provinceList[i].canBeShown();
break;
}
}
}
剩余的dealAddDoubted(line)
、dealMoveExact(line)
等函数都按照相同的模式:先检查是不是符合自己对应处理的模式,符合则直接处理,不符合则将其传给自己的下一个处理类。
最后将各个类串联起来组成责任链并封装即可。
/*
处理文档类,将责任链包括其中。
*/
public class DealLog {
public void execute(String line) {
LogHandler addExactPatient = new AddExactHandler();
LogHandler addDoubtedPatient = new AddDoubtedHandler();
LogHandler moveExactPatient = new MoveExactHandler();
LogHandler moveDoubtedPatient = new MoveDoubtedHandler();
LogHandler curePatient = new CureHandler();
LogHandler deadPatient = new DeadHandler();
LogHandler doubted2Exact = new Doubted2ExactHandler();
LogHandler doubted2None = new Doubted2NoneHandler();
//串联责任链
addExactPatient.setSuccessor(addDoubtedPatient);
addDoubtedPatient.setSuccessor(moveExactPatient);
moveExactPatient.setSuccessor(moveDoubtedPatient);
moveDoubtedPatient.setSuccessor(curePatient);
curePatient.setSuccessor(deadPatient);
deadPatient.setSuccessor(doubted2Exact);
doubted2Exact.setSuccessor(doubted2None);
//自动调用责任链
addExactPatient.handleRequest(line);
}
}
之后就可以直接使用dealLog
类来处理日志行文本了。
在处理文本时需要对数据进行处理,于是将数据封装成一个各省状态类,下面以单功能代码作示例
/*
定义各个省的情况类
*/
class ProvinceStat {
public String name;
private boolean showStat;
private int infected;
private int doubted;
private int cured;
private int dead;
ProvinceStat(String name)
{
this.name = name;
//0表示显示全部,其余下标分别对应感染、疑似、治愈、死亡
infected = 0;
doubted = 0;
cured = 0;
dead = 0;
showStat = false;
}
/*
改变是否显示
*/
public void canBeShown() {
showStat = true;
}
/*
用以获取受感染人数
*/
public int getInfected() {
return infected;
}
//其他类型的get函数省去
/*
用以增加感染人数
*/
public void addInfected(int n) {
infected += n;
}
//改变其他类型的函数省去
}
将各省状态类创建一个List
即可得到全国状态,并声明为全局变量,储存各省的状态,顺便将各省的名字也设为全局变量方便事后项目的改变。
static int PROVINCE_NUM = 35;
static String[] PROVINCE = {"全国", "安徽", "澳门" ,"北京", "重庆", "福建","甘肃",
"广东", "广西", "贵州", "海南", "河北", "河南", "黑龙江", "湖北", "湖南", "吉林",
"江苏", "江西", "辽宁", "内蒙古", "宁夏", "青海", "山东", "山西", "陕西", "上海",
"四川", "台湾", "天津", "西藏", "香港", "新疆", "云南", "浙江"};
ProvinceList PROVINCE_LIST = new ProvinceList();
到此设计部分的第一部分结束。
第二步,处理日志文本行
首先新建一个文件管理类将上一步新建的日志处理类dealLog
类包括进去,并且设置文件名、路径名等属性方便接下来的调用。
class FileHandler {
File[] fileList;
String path;
String newestLogName;
//新建文件内日志处理类
DealLog dealLog;
List<String> logNames, logPaths;
}
随后设置构造函数,顺便初始化所有的内容
/*
初始化构造函数
@param path 文件路径
@param path 日志处理方法
*/
FileHandler (String path, DealLog dealLog) {
File f = new File(path);
this.path = path;
this.dealLog = dealLog;
logNames = new ArrayList<String>();
logPaths = new ArrayList<String>();
fileList = f.listFiles();
initLogName();
initLogPath();
}
在初始化储存日志名时,记录最新的日志名
/*
获取目录下的日志名
*/
public void initLogName() {
String tmp = "2000-01-01.log.txt";
for (int i = 0;i < fileList.length; i++) {
logNames.add(fileList[i].getName());
if (fileList[i].getName().compareTo(tmp) >= 0) {
tmp = fileList[i].getName();
}
}
newestLogName = tmp;
}
然后创建流读写文档的方法
/*
读取日志并且进行处理
*/
public void readLog(String logPath) {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream
(new File(logPath)), "UTF-8"));
String line;
while ((line = br.readLine()) != null && !line.startsWith("//")) {
dealLog.execute(line);
}
br.close();
} catch (Exception e) {
e.printStackTrace();
}
}
写文件时,为了满足-type按照特定类型顺序输出的要求,另写了一个多态函数完成。
/*
将PROVINCE_LIST内的结果写出到指定文件
*/
public void writeResultLog(String resultPath) {
try {
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(resultPath)),"UTF-8"));
for (int i = 0; i < PROVINCE_NUM; i++) {
if (PROVINCE_LIST.provinceList[i].showStat) {
bw.write(PROVINCE_LIST.provinceList[i].name
+ " 感染患者" + PROVINCE_LIST.provinceList[i].getInfected() + "人"
+ " 疑似患者" + PROVINCE_LIST.provinceList[i].getDoubted() + "人"
+ " 治愈" + PROVINCE_LIST.provinceList[i].getCured() + "人"
+ " 死亡" + PROVINCE_LIST.provinceList[i].getDead() + "人");
bw.write('\n');
}
}
bw.write("// 该文档并非真实数据,仅供测试使用");
bw.close();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/*
将PROVINCE_LIST内特定类型的结果写出到指定文件
*/
public void writeResultLog(String resultPath, List<String> type) {
try {
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(resultPath)),"UTF-8"));
for (int i = 0; i < PROVINCE_NUM; i++) {
if (PROVINCE_LIST.provinceList[i].showStat) {
bw.write(PROVINCE_LIST.provinceList[i].name + " ");
for (String t : type) {
switch (t) {
case "ip" :
bw.write(" 感染患者" + PROVINCE_LIST.provinceList[i].getInfected() + "人");
break;
case "sp" :
bw.write(" 疑似患者"
+ PROVINCE_LIST.provinceList[i].getDoubted() + "人");
break;
case "cure" :
bw.write(" 治愈"
+ PROVINCE_LIST.provinceList[i].getCured() + "人");
break;
case "dead" :
bw.write(" 死亡"
+ PROVINCE_LIST.provinceList[i].getDead() + "人");
break;
}
}
bw.write('\n');
}
}
bw.write("// 该文档并非真实数据,仅供测试使用");
bw.close();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
为了满足-date属性的过滤显示,写出了过滤函数
/*
过滤date
@param String:日期
*/
public void getBeforeDate(String date) {
logPaths.clear();
for (String logName : logNames) {
if (logName.compareTo(date + ".log.txt") <= 0) {
logPaths.add(path + logName);
}
}
}
至此,文件处理已经完成。
第三步,命令的处理
先将创建一个命令类,创建一个抽象的命令类
/*
命令抽象类
*/
abstract class Command {
public abstract void execute();
}
本次项目中只有-list
一个命令,所以只写了ListCommand
命令类,若之后项目需要拓展,则新建其他的命令类并继承命令抽象类
/*
实际命令类
*/
class ListCommand extends Command {
CmdHandler cmdHandler;
CmdArgs cmdArgs;
ListCommand(CmdHandler cmdHandler, CmdArgs cmdArgs) {
this.cmdHandler = cmdHandler;
this.cmdArgs = cmdArgs;
}
@Override
public void execute() {
cmdHandler.listHandler(cmdArgs);
}
}
此处的构造函数要求传入一个命令解析类CmdArgs
方便接受命令中的参数。
/*
解析命令行类
*/
class CmdArgs {
String[] args;
CmdArgs(String[] args) {
this.args = args;
}
/*
@return 取得的命令
*/
public String getCmd() {
return args[0];
}
/*
判断是否有特定的参数
@param 特定参数名如-log -province
@return 返回特定参数的索引值,没有则返回0
*/
public int getParam(String param) {
for (int i = 0; i < args.length; i++) {
if (args[i].equals(param)) {
return i;
}
}
return 0;
}
/*
获取参数的单个参数值
@param index:参数下标值
@return 参数值
*/
public String getVal(int index) {
String tmp = "";
try {
tmp = args[index + 1];
}
catch (Exception e) {
System.err.println("数组要求越界");
}
return tmp;
}
/*
获取参数的所有参数值
@param i:参数下标值
@return 参数值列表
*/
public List<String> getVals(int index) {
List<String> paramVals = new ArrayList<String>();
for (int i = index + 1; i < args.length; i++) {
if (!args[i].startsWith("-")) {
paramVals.add(args[i]);
} else {
break;
}
}
return paramVals;
}
}
完成命令类后就需要建立命令接受类来处理命令(此处只有-list命令所以直接使用了CmdHandler,如果需要扩展则新建其他的命令处理类)
/*
命令处理类
*/
class CmdHandler {
String logPath, outPath, date;
List<String> type, province;
DealLog dealLog;
/*
初始化所有值构造函数
*/
CmdHandler(CmdArgs cmdArgs, DealLog dealLog) {
this.dealLog = dealLog;
//获取文件地址
logPath = cmdArgs.getVal(cmdArgs.getParam("-log"));
//获取输出文件地址
outPath = cmdArgs.getVal(cmdArgs.getParam("-out"));
//获取日期
//指定日期
if (cmdArgs.getParam("-date") > 0) {
date = cmdArgs.getVal(cmdArgs.getParam("-date"));
} else {
//未指定日期
}
//获取所需类型
if (cmdArgs.getParam("-type") > 0) {
type = cmdArgs.getVals(cmdArgs.getParam("-type"));
checkType();
}
//获取省份
if (cmdArgs.getParam("-province") > 0) {
province = cmdArgs.getVals((cmdArgs.getParam("-province")));
}
}
public void listHandler(CmdArgs cmdArgs) {
//读取并处理文件
FileHandler fh = new FileHandler(logPath, dealLog);
//如果没有date参数
if (cmdArgs.getParam("-date") == 0) {
for (String lp : fh.logPaths) {
fh.readLog(lp);
}
} else {
//如果有date参数
if (date.compareTo(fh.newestLogName) >= 0) {
System.err.println("日期超出范围。");
return;
}
fh.getBeforeDate(date);
for (String lp : fh.logPaths) {
fh.readLog(lp);
}
}
//如果有-province参数
if (cmdArgs.getParam("-province") > 0) {
//初始化所有可见参数
PROVINCE_LIST.initShowStat();
PROVINCE_LIST.setBeShown(province);
}
//如果有-type参数
if (cmdArgs.getParam("-type") > 0) {
fh.writeResultLog(outPath, type);
return ;
}
//将内容写到指定文件内
fh.writeResultLog(outPath);
}
/*
检查type参数值是否有误
@return 正确true 错误false
*/
public boolean checkType() {
for (String t : type) {
if (!(t.equals("ip") || t.equals("sp") || t.equals("cure") || t.equals("dead"))) {
System.err.println("不存在对应的-type参数值");
return false;
}
}
return true;
}
}
随后需要一个能够传递命令的类,也即发出命令的类
/*
传递命令类
*/
class CmdPass {
CmdArgs cmdArgs;
DealLog dealLog;
CmdHandler cmdHandler;
CmdPass(CmdArgs cmdArgs) {
this.cmdArgs = cmdArgs;
dealLog = new DealLog();
cmdHandler = new CmdHandler(cmdArgs, dealLog);
}
public void passCmd() {
ListCommand listCommand = new ListCommand(cmdHandler,cmdArgs);
if (cmdArgs.getCmd().equals("list")) {
listCommand.execute();
} else {
System.err.println("命令输入错误。");
}
}
}
至此第三步设计完成
完成串接
将命令解析 -> 将命令传出 -> 命令接收并进行接下来的处理
public static void main(String[] args) {
InfectStatistic infectStatistic = new InfectStatistic();
InfectStatistic.CmdArgs cmdArgs = infectStatistic.new CmdArgs(args);
InfectStatistic.CmdPass cmdPass = infectStatistic.new CmdPass(cmdArgs);
cmdPass.passCmd();
}
单元测试截图和描述
单元测试使用log文件皆是example中提供的测试日志文件。
1.main 参数测试
1.1 对list
命令进行测试,包括了-type
,-province
,-log
和-out
参数
1.2 在上述内容情况下加入-date
参数
结果发生变化
1.3 如果-date
数据超过了最新时间
1.4 如果-type
中的参数值不符合规定
1.5 缺省参数,只写-log与-out,按照默认方式输出文件
1.6 -porvince
参数的值若不存在任何病例
1.7 输入不存在的命令时,该程序不予处理
2.命令传递类的测试
2.1 错误命令的传入(正确命令传入如上1中所示)
2.2 命令非第一参数
3. 测试命令解析类
单元测试覆盖率优化和性能测试,性能优化截图和描述
单元测试覆盖率
性能优化
最开始使用if语句一条一条匹配
后来用责任链改进了大段的if-else语句
git仓库链接、代码规范链接
心路历程和收获
在一开始的时候觉得这次作业有点难,看到那么长的一条作业文档有点慌。在最开始的设想里,打算使用if-else的语句来控制命令判断、控制正则表达式的判断。但在不断地学习中学到了责任链模式、状态模式、命令模式,很好的解决了我觉得”这个代码好丑“的想法,并且拓展了自己的知识面,让整个代码的扩展性也更好。
写完之后回头一看,发现一个清晰的设计思路真的很重要,如果只是盲目的,一边做一边想,代码就会越写越乱,到最后都不知道自己在写什么了。
这次的收获:
1.自学能力真的很重要,在这次代码过程中,有很多JAVA的语法、方法、开发模式都是现在尚未学到的,也不是过去能在书本上学到的,只有通过自己的学习,看视频,看博客,不断地写代码,试错,改错,重做才能真正的掌握。在这程序这条路上书面上的知识远远不如实践更让人印象深刻。
2.一个程序的开发不能只着眼于眼前,之前的开发都处于”现在有什么功能我就开发什么功能“的境界上,在这次开发中才发现了一个程序的”拓展性“有多么重要。看教学视频的时候里面说到”写程序和搭建筑是很相似的,但是建筑不会让你搭完一个摩天大楼的时候,再在最下层建一个地下室“,编程出来的程序可能会在将来的一段时间内需求不断变化,这个时候就要求一开始的变成就要具有良好的拓展性,而非为了眼前需求而做的临时程序,到时候还得全部推翻重来。
3.工具的重要性,在开发过程中,编程能力固然重要,但是一个良好的工具能够给人更好的编程体验。用JUnit作单元测试和覆盖率检查是我第一次接触,尽管在使用的过程中遇到了很多问题,但是这也是自我学习路上的一个重要功课,在未来的开发中自己还有可能接触更多的开发工具与插件,不去熟悉他们固然可以正常进行开发,但是如果能够掌握他们,提高自己的效率,岂不更加美哉?
技术路线图相关的5个仓库
参照饿了吗,开发的vue作为前端的项目。此项目大大小小共 45 个页面,涉及注册、登录、商品展示、购物车、下单等等,是一个完整的流程。
在vue学习中的语法、操作、方法的小例子函数,结合doc写了教学代码。有着如生命周期、使用方法、示例等,在代码旁打好了注释方便阅读学习。
3.JavaScript(https://github.com/wchaowu/javascript)
包含了js的基础语法、面向对象的实现,JS中设计模式的实现、模式化开发。还带有jquery、Nodejs、html5等的教学以及开发细节。回答了一部分JS常见的疑问
本项目是利用原生js实现的h5小游戏,在实现时使用了大量es6语法。完成了背景、阳光计分板、六种植物和一种僵尸以及相应的攻击判定和死亡判定。完成了游戏可以正常游玩的基本要素
实现了移动端的智能地图。使用了高德地图的API,实时更新,实现了出行可视化。用户个人出行,不确定路程、目的地等信息,选择出行工具,点击开始,系统实时监听用户手机位置得到WGS84经纬度坐标(w3c HTML5 Geolocation地理定位标准),行程结束,记录本次出行信息,经纬度坐标转换GCJ-02坐标体系,通过高德地图提供三方API绘制出行轨迹。