寒假作业(2/2)


这个作业属于哪个课程 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.解题思路

整体思路

  1. 命令行参数用ArgumentHandler类解析,检测命令中的异常并向main抛出异常,若无异常则生成ArgumentContainer对象。
  2. 定义RecordContainer对象并调用init方法初始化,创建含有所有省份的空记录。
  3. 定义FileTools对象,通过构造方法从ArgumentContainer中获得log路径和输出路径,然后调用readFile方法,传入RecordContainer对象进行读取。
  4. RecordContainer调用getOutputDataSet方法获得待输出的数组,用FileTools输出。

类图

寒假作业(2/2)_第1张图片

流程图

寒假作业(2/2)_第2张图片

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.单元测试

寒假作业(2/2)_第3张图片
寒假作业(2/2)_第4张图片

对于不同命令引起的异常,甚至log文件中的格式异常,都能正确报错。

覆盖率

寒假作业(2/2)_第5张图片

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验证日期格式,较为严格但效率低,改成了较为宽松但效率大大提升的正则表达式。

优化后:

寒假作业(2/2)_第6张图片

可以看到效率得到了很大提升(多次运行后耗时大约在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开发相关第三方库

  1. CocoaPods:是iOS开发中最常用的第三方类库管理工具,开发者可以轻松地使用它向工程中添加第三方库和依赖、以及通过命令对库进行更新。
  2. SwiftyJSON:是个使用Swift语言编写的开源库,可以让开发者很方便地处理JSON数据(解析数据、生成数据)。
  3. realm-cocoa :是一个跨平台移动数据库引擎,支持iOS、macOS(Objective-C和Swift)以及Android,核心数据引擎C++打造,并不是建立在SQLite之上的ORM, 是拥有独立的数据库存储引擎。
  4. Valet:是一个使用OC语言编写的开源库,可以让开发者安全地使用iCloud Keychain存储数据,而无需知道其原理。
  5. RandomKit:是一个使用Swift语言编写的开源库,可以方便地生成各种随机数据(Integer、Float、Bool甚至是数组)。

你可能感兴趣的:(寒假作业(2/2))