Zou-Wang
返回顶部

软工寒假实践作业(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

思路描述

  1. 先大概看一下所需要用到的大方向

    • 要处理日志文本,需要对文件进行读写。
    • 日志中的内容有多种匹配格式,需要使用正则表达式
    • 直接传入命令行参数,需要使用命令行参数
  2. 根据大方向利用发散思维从大架构思考到细节方面:

  3. 参考责任链模式命令模式优化:

    将命令、处理类、发出类抽象出来,降低耦合,增加代码的可扩展性

    将匹配正则表达式时多个if-else语句转化为多个处理函数并链接,优化代码。

设计实现过程

  1. 先处理最基本的功能:输入一个规定格式文本后,可以处理其中的数据。

    • 分别建立不同正则表达式的模式。将模式处理函数抽象为Handler,然后分别实例化为8种对应的处理函数。然后将8种对应处理函数串接起来成为责任链,届时只需传入对应的文本行进入责任链即可得到对应的结果。如果项目事后扩展时需要添加新的处理函数,直接接入责任链即可。
    • 对于数据的保存,就将每个省的患病情况封装成一个省情况类,再将省情况串成一个省列表方便整体的管理。

  2. 再进一步,从日志中获得规定格式文本传入责任链

    处理日志分为两步:1.读写文件 2.对日志内容进行处理。

    • 读写文件时,需要文件读写类,即一个文件处理类并写对应的操作函数即可,这部分设计比较简单。
    • 处理文件时,要考虑到会传入不同的命令和未来命令的增多,所以要使用命令模式,将处理日志的函数抽象,作为命令的接收者。然后用文件处理类来包括这个日志处理类
    • 在处理日志的时候还要关注到日志的时间命名,所以在文件处理类中需要对文件名进行处理。
  3. 最后,需要处理传入的命令行参数

    根据命令模式,将这一情况抽象出命令类命令的传递者命令的解析者,至于命令的接收者也就是处理者已经在第二步中封装完成。将这几者抽象出来变成松耦合的关系,方便之后命令增多时,直接新建一个命令类,并且在对应的类中写出处理和传递的函数即可。

最终完成流程图:

代码说明

从设计出发:

第一步,能够完成对某一行文本进行解析的函数:

首先将匹配模式设为全局变量,如果之后情况更加复杂则直接修改即可:

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个仓库

1.仿饿了吗的vue前端开发

参照饿了吗,开发的vue作为前端的项目。此项目大大小小共 45 个页面,涉及注册、登录、商品展示、购物车、下单等等,是一个完整的流程。

2.vue的学习教学渐进式代码仓库

在vue学习中的语法、操作、方法的小例子函数,结合doc写了教学代码。有着如生命周期、使用方法、示例等,在代码旁打好了注释方便阅读学习。

3.JavaScript(https://github.com/wchaowu/javascript)

包含了js的基础语法、面向对象的实现,JS中设计模式的实现、模式化开发。还带有jquery、Nodejs、html5等的教学以及开发细节。回答了一部分JS常见的疑问

4.JavaScript实现的植物大战僵尸

本项目是利用原生js实现的h5小游戏,在实现时使用了大量es6语法。完成了背景、阳光计分板、六种植物和一种僵尸以及相应的攻击判定和死亡判定。完成了游戏可以正常游玩的基本要素

5.高德地图API以及vue实现的地图

实现了移动端的智能地图。使用了高德地图的API,实时更新,实现了出行可视化。用户个人出行,不确定路程、目的地等信息,选择出行工具,点击开始,系统实时监听用户手机位置得到WGS84经纬度坐标(w3c HTML5 Geolocation地理定位标准),行程结束,记录本次出行信息,经纬度坐标转换GCJ-02坐标体系,通过高德地图提供三方API绘制出行轨迹。

posted @ 2020-02-17 23:58  Pcy潘晨宇  阅读(379)  评论(5编辑  收藏  举报