这个作业属于哪个课程 | 2020春S班 (福州大学) |
---|---|
这个作业要求在哪里 | 软工实践寒假作业(2/2) |
这个作业的目标 | 1.学习github使用 2.制定代码规范 3.学习PSP表格规划 4.对简单项目进行项目需求分析并编程实现 5.学习使用单元测试和性能测试工具 |
作业正文 | hujh的软工实践寒假作业(2/2) |
其他参考文献 | 《码出高效_阿里巴巴Java开发手册》 工程师的能力评估和发展 |
1. Github仓库地址
https://github.com/hujh4779/InfectStatistic-main
2. 阅读《构建之法》及PSP表格
- 本次作业PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 60 |
Estimate | 估计这个任务需要多少时间 | 60 | 60 |
Development | 开发 | 720 | 920 |
Analysis | 需求分析 (包括学习新技术) | 90 | 150 |
Design Spec | 生成设计文档 | 60 | 90 |
Design Review | 设计复审 | 30 | 30 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 30 | 60 |
Design | 具体设计 | 60 | 90 |
Coding | 具体编码 | 300 | 360 |
Code Review | 代码复审 | 30 | 20 |
Test | 测试(自我测试,修改代码,提交修改) | 120 | 120 |
Reporting | 报告 | 180 | 160 |
Test Repor | 测试报告(发现了多少bug,修复了多少) | 30 | 20 |
Size Measurement | 计算工作量 (多少行代码,多少次签入, 多少测试用例,其他工作量) |
30 | 20 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 (包括写文档、博客的时间) |
120 | 120 |
合计 | 960 | 1140 |
3. 解题思路描述
3.1 需求分析
本次作业的需求概括为:程序能够读取指定目录下的log文件,统计后在指定目录按照指定格式输出,指定参数通过命令行参数给出。代码的功能主要有以下四项:
- 分析命令行参数
- 按要求读取日志
- 处理日志中读取的数据
- 按要求输出
3.2 数据结构选择
在读懂题目需求后,题目中最关键的数据结构为对各省份四种类型人员的数据存储,我选择了二维数据int[][] patient
对各省份人员进行存储。采用二维数组进行存储,优点是数据结构简单高效,缺点是较难通过二维数组的下标看出省份和人员类型。
3.3 命令行解析
最早接触命令行参数是在大一的C语言课程上,但后来几乎没有再使用过。而在linux课程中我也接触了一些关于命令与参数的操作。在这次的作业中要求用命令行参数来设置日志读取目录、输出目录等,在百度搜索了相关知识后,决定写一个命令行处理的类CommandHandle
来专门处理命令行参数。
3.4 日志文本匹配
本次作业中要对日志中的八种文本类型进行匹配和提取:
1、<省> 新增 感染患者 n人
2、<省> 新增 疑似患者 n人
3、<省1> 感染患者 流入 <省2> n人
4、<省1> 疑似患者 流入 <省2> n人
5、<省> 死亡 n人
6、<省> 治愈 n人
7、<省> 疑似患者 确诊感染 n人
8、<省> 排除 疑似患者 n人
在百度相关知识后决定采用正则匹配,使用java库中的Pattern
类和Matcher
类进行处理。
4. 设计实现过程
4.1 总体设计
程序主要功能有四项:
- 分析命令行参数
- 按要求读取日志
- 处理日志中读取的数据
- 按要求输出
代码设计也按照程序的功能封装成四个内部类:
- 命令处理类
CommandHandle
- 日志处理类
LogHandle
- 数据处理类
DataHandle
- 输出控制类
OutputHandle
在后期实际代码编写时,发现数据处理和输出控制合并后更佳,因此最终分为了CommandHandle
, LogHandle
和DataHandle
三个类。
4.2 模块结构图
4.3 总体流程图
4.4 readLogs函数流程图
5. 代码说明
5.1 InfectStatistic类
1.变量
String[] provinceString:用以存放省份信息的字符串数组
String[] patientType:用以存放人员类型的字符串数组
private String[] provinceString = {"全国", "安徽", "北京", "重庆", "福建", "甘肃", "广东", "广西", "贵州", "海南",
"河北", "河南", "黑龙江", "湖北", "湖南", "吉林", "江苏", "江西", "辽宁", "内蒙古", "宁夏", "青海", "山东",
"山西", "陕西", "上海", "四川", "天津", "西藏", "新疆", "云南", "浙江"};
private String[] patientType = {"感染患者", "疑似患者", "治愈", "死亡"};
5.2 命令处理类CommandHandle
1.变量
- String logPath:用以存放命令行参数中的日志地址
- String outputPath:用以存放命令行参数中的输出地址
- String endDate:用以存放命令行参数中的截止日期
- boolean typeSign:用以判断命令行参数中是否有
-type
参数 - ArrayList typeList:用以存放命令行参数
-type
的参数值 - boolean provinceSign:用以判断命令行参数中是否有
-province
参数 - int[] provinceList:下标分别代表
InfectStatistic
类中provinceString
的省份。初始为0,根据-province
所带的参数值将对应的位置1。
private String logPath;
private String outputPath;
private String endDate;
private boolean typeSign;
private ArrayList typeList;
private boolean provinceSign;
private int[] provinceList;
public CommandHandle() {
this.logPath = "";
this.outputPath = "";
this.endDate = "";
typeSign = false;
this.typeList = new ArrayList<>();
provinceSign = false;
this.provinceList = new int[provinceString.length];
}
2.主要方法:commandProcess
/**
* commandProcess
* @description 根据传入的命令行参数,设置类变量
* @param args 命令行参数
*/
public void commandProcess(String[] args) {
if (args[0].equals("list")) { //命令匹配
for (int i = 1; i < args.length; i++) {
switch (args[i]) {
case "-log": //设置logPath
i++;
this.setLogPath(args[i]);
break;
case "-out": //设置outputPath,思路同上
...
case "-date": //设置endDate,思路同上
...
case "-type": //设置typeSign和patientTypeSign
typeSign = true;
int j = 0;
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
i++;
j++;
switch (args[i]) {
case "ip":
typeList.add(patientType[0]);
break;
...
}
}
break;
case "-province":
provinceSign = true;
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
i++;
for (int k = 0; k < provinceString.length; k++) {
if (args[i].equals(provinceString[k])) {
provinceList[k] = 1;
}
}
}
break;
...
5.3 日志处理类LogHandle
1.变量
- ArrayList
stringList:用以存放日志中读取到的每行字符串
private ArrayList stringList;
public LogHandle() {
this.stringList = new ArrayList<>();
}
2.主要方法:readLogs
/**
* readLogs
* @description 根据传入的最迟日期和日志目录逐行读取日志,并添加至stringList变量中
* @param endDate 读取日志最迟日期
* @param logPath 读取日志目录
*/
public boolean readLogs(String endDate, String logPath) {
File file = new File(logPath);
File[] fileList = file.listFiles();
assert fileList != null;
String biggestDate = "";
for (File value : fileList) {
String fileName = value.getName();
String fileNameWithout = fileName.substring(0, 10);
if (biggestDate.compareTo(fileNameWithout) < 0) {
biggestDate = fileNameWithout;
}
}
if (endDate.compareTo(biggestDate) > 0) { //若命令行传入的截止日期大于日志文件的最大日期
return false;
}
else {
for (File value : fileList) {
String fileName = value.getName();
String fileNameWithout = fileName.substring(0, 10); //截取日志文件的形如"2020-01-22"格式字符串,方便比较
if (endDate.compareTo(fileNameWithout) >= 0) { //若截止日期大于等于日志日期,就读入日志
try {
BufferedReader bf = new BufferedReader(new InputStreamReader(new FileInputStream(
new File(logPath + fileName)), "UTF-8")); //读入文件
String str;
while ((str = bf.readLine()) != null) {
if (!str.startsWith("//")) { //不读入以//开头的行
this.stringList.add(str); //将符合要求的每一行添加至stringList中
}
}
bf.close();
}
...
5.4 数据处理类DataHandle
1.属性
- int[][] patient:二维数组存放每个省份的每种类型的人员数量。一维下标根据
InfectStatistic
类中的provinceString
,二维下标根据InfectStatistic
类中的patientType
。 - int[] influencedProvince:表示日志中受影响的省份,初始均为0,根据日志中的信息将影响到的省份的值置1。
private int[][] patient = new int[provinceString.length][patientType.length];
private int[] influencedProvince = new int[provinceString.length];
2.主要方法:dataProcess
/**
* dataProcess
* @description 将传入的字符串数组匹配正则表达式模式,并调用其他方法进行处理
* @param stringList 传入日志中的每一行字符串
*/
public void dataProcess(ArrayList stringList) {
//正则表达式八种匹配模式
String pattern1 = "\\W+ 新增 感染患者 \\d+人";
String pattern2 = "\\W+ 新增 疑似患者 \\d+人";
String pattern3 = "\\W+ 感染患者 流入 \\W+ \\d+人";
String pattern4 = "\\W+ 疑似患者 流入 \\W+ \\d+人";
String pattern5 = "\\W+ 死亡 \\d+人";
String pattern6 = "\\W+ 治愈 \\d+人";
String pattern7 = "\\W+ 疑似患者 确诊感染 \\d+人";
String pattern8 = "\\W+ 排除 疑似患者 \\d+人";
influencedProvince[0] = 1; //全国必定受到影响,因此置1
//将日志中每一条字符串匹配正则表达式模式,并调用相应的方法
for (String str : stringList) {
if (Pattern.matches(pattern1, str)) {
ipAdd(str);
} else if (Pattern.matches(pattern2, str)) {
spAdd(str);
} else if (Pattern.matches(pattern3, str)) {
ipFlow(str);
} else if (Pattern.matches(pattern4, str)) {
spFlow(str);
} else if (Pattern.matches(pattern5, str)) {
deadAdd(str);
} else if (Pattern.matches(pattern6, str)) {
cureAdd(str);
} else if (Pattern.matches(pattern7, str)) {
spToIp(str);
} else if (Pattern.matches(pattern8, str)) {
spSub(str);
}
}
}
3.主要方法:ipAdd(其他数据处理方法如spAdd、ipFlow等类似)
/**
* ipAdd
* @description 根据单条日志记录修改patient和influencedProvince
* @param str 传入单条日志记录
*/
public void ipAdd(String str) {
//使用正则表达式提取字符串中的省份和人数
String pattern = "(\\W+) 新增 感染患者 (\\d+)人";
Pattern r = Pattern.compile(pattern);
Matcher m = r.matcher(str);
String province = "";
int ip = 0;
if (m.find( )) {
province = m.group(1);
ip = Integer.parseInt(m.group(2));
} else {
System.out.println("NO MATCH");
}
//对patient和influencedProvince进行处理
for (int i = 0; i < provinceString.length; i++) {
if (provinceString[i].equals(province)) {
patient[i][0] += ip;
patient[0][0] += ip;
influencedProvince[i] = 1;
}
}
}
4.主要方法:output
/**
* output
* @description 根据传入的参数,将类变量格式化输出
* @param outputPath 输出的日志目录
* @param patientTypeSign 命令行参数是否有-type
* @param provinceList 省份输出标志
* @param provinceSign 命令行参数是否有-province
* @param typeSign 类型输出标志
*/
public void output(String outputPath, int[] patientTypeSign, int[] provinceList, boolean provinceSign, boolean typeSign) {
try {
File file = new File(outputPath);
String dir = file.getParent();
File dirFile = new File(dir);
if (!dirFile.exists()) { //如果生成日志的目录不存在,则先创建目录,防止后续创建文件失败
dirFile.mkdir();
}
BufferedWriter fileWriter = new BufferedWriter (new OutputStreamWriter (new FileOutputStream (outputPath, false),"UTF-8")); //设置输出格式为UTF-8
if (provinceSign) { //如果有命令行参数中带有-province
for (int i = 0; i < provinceString.length; i++) {
if (provinceList[i] == 1){
if (typeSign) { //如果命令行参数中带有-type
fileWriter.write(provinceString[i]);
for (String s : typeList) {
for (int k = 0; k < patientType.length; k++) {
if (s.equals(patientType[k])) {
fileWriter.write(" " + patientType[k] + patient[i][k] + "人");
}
}
}
fileWriter.write("\n");
}
else {
fileWriter.write(provinceString[i] + " 感染患者" + patient[i][0] + "人 疑似患者"
+ patient[i][1] + "人 治愈" + patient[i][2] + "人 死亡" + patient[i][3] + "人\n");
}
}
}
}
else {
...
6. 单元测试截图和描述
6.1 testCommandProcess
6.2 testReadLogs
6.3 testIpAdd
6.4 testDataProcess
6.5 testOutput
6.6 testMain
6.7 测试结果
7. 单元测试覆盖率优化和性能优化
7.1 单元测试覆盖率及优化
除了未使用的Lib类,测试代码的类覆盖率和方法覆盖率均为100%,而代码的覆盖率为95%,查看后发现是错误处理的代码,因此未做修改。
7.2 性能测试及优化
-
性能优化分析
在百度过一些资料后,我发现应避免在循环中调用try...catch...,而应该放在最外层;也应避免在循环中创建对象,应提到外层。因此我用这两种方法优化了我的代码。 -
优化前后分析
对比可以看出,优化前后并没有什么差异,初步分析是因为代码中本来循环中调用就不多,并且优化方法欠佳,因此优化前后收效甚微。今后应继续学习代码性能优化的方法。
8. 代码规范链接
https://github.com/hujh4779/InfectStatistic-main/blob/master/221701410/codestyle.md
9. 本次项目的心路历程与收获
在阅读《构建之法》一至三章后,我收获良多,有了软件工程的初步概念,也深感自己距离软件工程师的差距。在这次项目中,我学习了gitbub的使用,并理解了为什么软件需要项目管理、如何进行团队项目协作。而代码规范的制定也让我更深刻地理解了代码结构的可读性对编程的重要性。上一次编写代码规范还是在大二时,那时制定了c++语言的代码规范。在经过近一年时间后的现在重新制定java语言规范,我又学习了更多良好的结构,并进一步提高了自己的代码规范性。这一次的项目我在需求分析和设计方面花的时间很久,因为在读了工程师的能力评估和发展后,我了解了优秀的工程师在需求分析方面花费的时间会比大学生更久,因此在这次项目中我也尽量让自己能够更多地分析需求和设计代码。在更好地分析需求和设计之后,编写代码的目标就更明确了,写起来也更有目的性。在编写完代码之后,我也是第一次学习并使用了单元测试,这种测试方法非常高效,不仅能够测试每个函数的输出正确性,而且易于运行和重现,我在对代码进行改变之后运行一次单元测试就可以清晰地知道是否有错误。在之后的编码过程中,我也都会使用单元测试。在这一次的项目中,我学会了很多东西,并且这些对我之后的路也非常有帮助。在之后的日子里,我会继续努力学习软件工程,提高自己的水平。
10. 技术路线图相关的仓库
1.android-architecture
- 链接:android-architecture
- 简介:google 提供的 android 架构蓝图,内部包含多个示例。
2.JiaoZiVideoPlayer
- 链接:JiaoZiVideoPlayer
- 简介:Android VideoPlayer MediaPlayer VideoView MediaView Float View And Fullscreen.一款可以快速继承的视频播放库。
3.MVVMHabit
- 链接:MVVMHabit
- 简介:基于DataBinding框架,MVVM设计模式的一套快速开发库,整合Okhttp+RxJava+Retrofit+Glide等主流库,满足日常开发需求。
4.iosched
- 链接:iosched
- 简介:Google material design based on official app source code examples. Google官方的基于material design的实例app源码。
5.KotlinMvp
- 链接:KotlinMvp
- 简介:基于Kotlin+MVP+Retrofit+RxJava+Glide 等架构实现的短视频类的APP练手项目,仿“开眼Eyepetizer”。