这个作业属于哪个课程 | 2020春 软件工程实践 |
---|---|
这个作业要求在哪里 | 2020春软工实践寒假作业(2/2) |
这个作业的目标 | 实现一个疫情统计程序,并在开发过程中练习需求分析、版本控制、代码测试等 |
作业正文 | 寒假作业(2/2)——疫情统计 |
其他参考文献 | ... |
1.GitHub仓库地址
作业的主仓库:https://github.com/numb-men/InfectStatistic-main
我的作业仓库:https://github.com/YBN-JUAN/InfectStatistic-main
2.PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
Estimate | 估计这个任务需要多少时间 | 60 | 60 |
Development | 开发 | ||
Analysis | 需求分析 (包括学习新技术) | 120 | 300 |
Design Spec | 生成设计文档 | 60 | 60 |
Design Review | 设计复审 | 60 | 70 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 60 | 150 |
Design | 具体设计 | 60 | 100 |
Coding | 具体编码 | 1800 | 1600 |
Code Review | 代码复审 | 180 | 240 |
Test | 测试(自我测试,修改代码,提交修改) | 180 | 260 |
Reporting | 报告 | ||
Test Report | 测试报告 | 60 | 150 |
Size Measurement | 计算工作量 | 30 | 70 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 60 | 50 |
合计 | 2730 | 3110 |
可能不太精确,统计完之后自己还是有点吃惊,一个小小的命令行处理程序花了这么长时间,自己的编程能力还是远远不够。
3.解题思路
整体思路
- 命令行参数用ArgumentHandler类解析,检测命令中的异常并向main抛出异常,若无异常则生成ArgumentContainer对象。
- 定义RecordContainer对象并调用init方法初始化,创建含有所有省份的空记录。
- 定义FileTools对象,通过构造方法从ArgumentContainer中获得log路径和输出路径,然后调用readFile方法,传入RecordContainer对象进行读取。
- RecordContainer调用getOutputDataSet方法获得待输出的数组,用FileTools输出。
类图
流程图
4.代码说明
初始化待读取log列表,指定-date的情况下只会读取指定的当天及以前的log。
private void initFileListWithDateLimit(String newestFileName) throws ArgumentException {
try (DirectoryStream stream = Files.newDirectoryStream(Paths.get(logPath))) {
fileList = new ArrayList<>() {{
boolean dateOutOfBound = true;
for (Path path : stream) {
String name = path.getFileName().toString();
if (name.matches(FILE_NAME_FILTER) && name.compareTo(newestFileName) <= 0) {
if (name.equals(newestFileName)) {
dateOutOfBound = false;
}
add(path.getFileName().toString());
}
}
if (dateOutOfBound) {
throw new ArgumentException("-date:指定的日期不存在。");
}
};
} catch (IOException e) {
e.printStackTrace();
}
}
将一行记录用空格分割成数组,由于数组长度只有3、4、5这三种情况,故使用命名相同而参数列表不同的3个updateRecord方法进行处理。
//<省> 治愈/死亡/确诊感染 n人
private void updateRecord(String province, String patientType, int number)
//<省> 新增/排除 感染患者/疑似患者 n人
private void updateRecord(String province, String operation, String patientType, int number)
//<省1> 感染患者/疑似患者 流入 <省2> n人
private void updateRecord(String provinceOut, String patientType, int number, String provinceIn)
void parseSingleLine(String line) throws LogFormatException {
//将一行log用空格分隔成字符串数组
String[] log = line.split(" ");
switch (log.length) {
case 3:
updateRecord(log[0], log[1], Lib.extractNumberFromString(log[2]));
break;
case 4:
//确诊感染
if (Lib.CONFIRMED.equals(log[2].trim())) {
updateRecord(log[0], log[2].trim(), Lib.extractNumberFromString(log[3]));
} else {
updateRecord(log[0], log[1], log[2], Lib.extractNumberFromString(log[3]));
}
break;
case 5:
updateRecord(log[0], log[1], Lib.extractNumberFromString(log[4]), log[3]);
break;
default:
throw new LogFormatException();
}
}
提取出命令行参数指定的-type
String getStringWithPatientTypeFilter(ArrayList filter) {
StringBuilder builder = new StringBuilder();
for (String type : filter) {
switch (type) {
case Lib.INFECTED:
builder.append(getStringOfInfected());
break;
case Lib.SUSPECTED:
builder.append(getStringOfSuspected());
break;
case Lib.CURED:
builder.append(getStringOfCured());
break;
case Lib.DEAD:
builder.append(getStringOfDead());
break;
default:
break;
}
}
return builder.toString();
}
实现记录按省份拼音顺序输出
//provinceList为HashSet,查找性能比数组好
for (String province : Lib.PROVINCE_LIST) {
if (provinceList.contains(province)) {
add(province + mapContainer.get(province).getStringWithPatientTypeFilter(patientTypes));
}
}
直接在工具类中定义已经排好序的省份名数组常量PROVINCE_LIST,输出log时只需要遍历这个数组,如果遍历到某个省份时,从命令行参数中解析出来的省份集合也含有这个省份,就从记录容器中取出相应的记录放入待输出数组。
5.单元测试
对于不同命令引起的异常,甚至log文件中的格式异常,都能正确报错。
覆盖率
6.代码优化
使用的命令行参数为:
list -log /Users/ybn/IdeaProjects/InfectStatistic-main/221701223/log/ -out /Users/ybn/Downloads/out.txt -province 全国 湖北 福建 -type ip dead -date 2020-01-27
public static boolean validateFormatOfDateString(String dateString) {
// SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
// try {
// format.setLenient(false);
// format.parse(dateString);
// } catch (ParseException parseException) {
// return false;
// }
// return true;
return dateString.matches(DATE_REGEX);
}
原来使用java.text.SimpleDateFormat
中的SimpleDateFormat
验证日期格式,较为严格但效率低,改成了较为宽松但效率大大提升的正则表达式。
优化后:
可以看到效率得到了很大提升(多次运行后耗时大约在200ms以内)。
7.代码规范
8.心路历程
- 刚开始着手做这次作业时其实对测试相关的要求没有太上心,直到代码差不多都编写完了才发现原来测试也有那么大工作量。在使用JUnit做单元测试及使用JProfile做性能测试时也都遇到了一些阻碍。
- 通过练习使用IDEA集成的Git以及配合GitHub进行版本控制,发现使用版本控制会显著提高软件开发的效率,在后续的课内实践以及工作中还要继续多多练习。
- 因为给自己定了学习iOS开发的目标路线,所以这次的作业原本是打算使用Objective-C语言来编写,OC标准库中的工具也非常契合此次作业的需求,但因为作业要求使用gcc编译,而OC需要使用clang,故只能转为用Java编写。又由于自己对Java的掌握并不好(甚至不如更早学的C++),相当于边学边做,特别是在选择存放Record对象以及其他参数的数据结构时,犹豫了很长时间,经过反复修改才达到基本让自己满意。
- 最后,使用macOS或Linux等默认字符集为UTF-8的操作系统进行开发,可以极大地节省使用Windows时用来解决丑陋的编码问题的无意义时间。
9.iOS开发相关第三方库
- CocoaPods:是iOS开发中最常用的第三方类库管理工具,开发者可以轻松地使用它向工程中添加第三方库和依赖、以及通过命令对库进行更新。
- SwiftyJSON:是个使用Swift语言编写的开源库,可以让开发者很方便地处理JSON数据(解析数据、生成数据)。
- realm-cocoa :是一个跨平台移动数据库引擎,支持iOS、macOS(Objective-C和Swift)以及Android,核心数据引擎C++打造,并不是建立在SQLite之上的ORM, 是拥有独立的数据库存储引擎。
- Valet:是一个使用OC语言编写的开源库,可以让开发者安全地使用iCloud Keychain存储数据,而无需知道其原理。
- RandomKit:是一个使用Swift语言编写的开源库,可以方便地生成各种随机数据(Integer、Float、Bool甚至是数组)。