软工实践寒假作业(2/2)

这个作业属于哪个课程 班级链接
这个作业要求在哪里 软工实践寒假作业(2/2)
这个作业的目标 GitHub 使用,制定代码规范,独立开发一个疫情统计程序,学会需求分析、单元测试、覆盖率分析、性能分析,学会PSP表格
作业正文 本文
其他参考文献 单元测试与回归测试、JProfiler使用、相关博文

一、Github 仓库地址

Github仓库地址

二、PSP 表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 45
Estimate 估计这个任务需要多少时间 40 30
Development 开发 40 45
Analysis 需求分析 (包括学习新技术) 120 60
Design Spec 生成设计文档 40 40
Design Review 设计复审 30 30
Coding Standard 代码规范 (为目前的开发制定合适的规范) 20 20
Design 具体设计 60 60
Coding 具体编码 600 360
Code Review 代码复审 30 30
Test 测试(自我测试,修改代码,提交修改) 120 240
Reporting 报告 120 90
Test Report 测试报告 100 120
Size Measurement 计算工作量 30 40
Postmortem & Process Improvement Plan 事后总结,并提出过程改进计划 30 30
合计 1290 1240

三、解题思路描述

软工实践寒假作业(2/2)_第1张图片

看完题目需求我将程序的功能大致分为三个模块:

1. 命令行参数解析

处理main函数传入的命令行参数数组,解析-log -out -type -date -province指令并作相应处理

2. 读取输入日志文件

根据-log 传入的目录读取日志文件,根据-date 传入的参数确定日期,然后就是解析文件的每行字符串,转换成数据,根据下标判断字符串就能区分所有情况

软工实践寒假作业(2/2)_第2张图片

3. 输出统计文件

根据-out 传入的输出文件目录,若需要输出全国数据,则统计全国数据,并写入文件的第一行。之后将其他需要输出的省份数据统计之后,按照省份拼音先后顺序排序后输出

四、设计实现过程

  • 程序模块设计

软工实践寒假作业(2/2)_第3张图片

  • 数据结构设计

    • 写了一个Province类来存储和处理省份数据。
    • 用HashMap 来存储日志中出现的省份数据,实现按名查找所需省份。
    • 用ArrayList 分别实现了-province传入的省份列表,以及所有有数据的省份列表,用于排序。
  • 关键函数流程

软工实践寒假作业(2/2)_第4张图片

五、代码说明

  • 存储省份数据的数据结构:

    提供修改数据和输出数据的API

    public class Province{
      	
            /** 省份名字 */
            private String name;
            /** 感染患者数量 */
            private long ip;
            /** 疑似患者数量 */
            private long sp;
            /** 治愈患者数量 */
            private long cure;
            /** 死亡患者数量 */
            private long dead;
    
            //提供修改数据的API
            ...
      	
      		/**
    		 * description:获取要输出的统计数据
    		 * @return 要输出的字符串
    		 */
    		public String getOuputResult() {
    			if (isShowAllData) {
    				return getAllOuputResult();
    			}
    			return getOuputResultByTypes(typeList);
    		}
    		
    		/** 
    		 * description:获取要输出的全部统计数据
    		 * @return 要输出的字符串
    		 */
    		private String getAllOuputResult() {
    			String res = name + ' ' + "感染患者" + ip + "人" + ' ' + "疑似患者" + sp + "人" + ' ' + "治愈" + cure
                        + "人" + ' ' + "死亡" + dead + "人";
    			return res;
    		}		
    		
    		/**
    		 * description:通过命令行指令参数获取要输出的统计结果
    		 * @param types  命令行参数类型数组
    		 * @return 要输出的字符串
    		 */
    		private String getOuputResultByTypes(ArrayList types) {
    			String res = name + " ";
    			for (int i = 0; i < types.size(); i++) {
    				switch (types.get(i)) {
    				case Constants.TYPE_IP:
    					res += "感染患者" + ip + "人 ";
    					break;
    				case Constants.TYPE_SP:
    					res += "疑似患者" + sp + "人 ";
    					break;
    				case Constants.TYPE_CURE:
    					res += "治愈" + cure + "人 ";
    					break;
    				case Constants.TYPE_DEAD:
    					res += "死亡" + dead + "人 ";
    					break;
    				default:
    					break;
    				}
    			}
    			return res;
    		}
    }
    
  • 用一个常数类来存储常数:

    防止打错,不好debug和修改

    public static class Constants{
        	 
        	 private static final int NUM_PROVINCE = 34;
        	 private static final int NUM_TYPE = 4;
        	 
        	 private static final String TYPE_IP = "ip";
        	 private static final String TYPE_SP = "sp";
        	 private static final String TYPE_CURE = "cure";
        	 private static final String TYPE_DEAD = "dead";
        	 
             private static final String CMD_LOG = "-log";
             private static final String CMD_OUT = "-out";
             private static final String CMD_DATE = "-date";
             private static final String CMD_TYPE = "-type";
             private static final String CMD_PROVINCE = "-province";
        }
    
  • 循环解析命令行参数数组:

    若传入-Date 则将isReadALL标志设为false,表示不需要读取所有日志文件,若没有传入则默认传入的日期为当前日志文件的最新日期

    -type和-province的参数分别存入相应的数组中,等待后续的调用,将相应标志设为false,表示不需要全部输出,而是从列表中选择性输出

    private void parse() {
    		 for (int i = 1; i < args.length; i++) {
    			switch (args[i]) {
    			case Constants.CMD_DATE:
    				isReadAll = false;
    				date = args[++i];
    				break;
    			case Constants.CMD_LOG:
    				logPath = args[++i];
    				break;
    			case Constants.CMD_OUT:
    				outputPath = args[++i];
    				break;
    			case Constants.CMD_TYPE:
    				isShowAllData = false;
    				i = executeTypeCmd(i + 1) - 1;
    				break;
    			case Constants.CMD_PROVINCE:
    				isShowAllProvince = false;
    				i = executeProvinceCmd(i + 1) - 1;
    				break;
    			default:
    				break;
    			}
    		}		 
    	 }
    
  • 读取并解析文件:

    如果日志文件夹错误或者日期参数超出范围,都会报错提前结束程序,而不会写入文件操作

    日期没有问题则比较传入日期与获取的文件名的日期,小于传入日期就循环读取解析文件每行的信息

    private void readFiles() {
    		 
    		 File file = new File(logPath);
    		 File[] logFiles = file.listFiles();
    		 		 		 		 
    		 if (logFiles.length == 0) {
    			 System.out.println("当前文件夹下没有日志文件!路径:" + logPath);
    			 isEnd = true;
    			 return;
    		 }		 
    		 
    		 if (isReadAll == false) {
    			//比较输入日期与最新日期
    			 String lastestDate = getLastestData(logFiles);
    			 if (date.compareTo(lastestDate) > 0) {
    				 System.out.println("日期超出范围,当前最新日期为:" + lastestDate);
    				 isEnd = true;
    				 return;
    			 }
    		 }
    		 
    		 //读取日志文件
    		 for (int i = 0; i < logFiles.length; i++) {
    			String logDate = logFiles[i].getName().split("\\.")[0];
    			if (isReadAll || date.compareTo(logDate) >= 0) {
    				try {
    					BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(logFiles[i]), "UTF-8"));;               
    		            String line = new String();          
    		                
    		            while ((line = br.readLine()) != null){
    		                String[] datas = line.split(" ");
    		                //读取并处理单行数据
    		                executeOneLine(datas);
    		            } //单个文件读取完毕         
    		            br.close();
    				} catch (Exception e) {
    					e.printStackTrace();
    				}				
    			}
    		} //文件全部读取完毕				 
    	 }
    
  • 处理文件每行的信息:

    根据需求分析所述,将文件单行数据用空格分隔,解析字符串数组,对不同情况进行分类,调用该省份对象的方法来处理变动的数据

    如果省份为第一次出现,则创建新的对象,并加入哈希表中存储,否则直接按名从哈希表中获取对象

    private void executeOneLine(String[] datas) {
    		 //忽略注释行
    		 if(datas[0].equals("//")) {
    			 return;
    		 }
    
    		String provinceName = datas[0];
    		Province prov = getProvinceByKey(provinceName); 
    		
    		//根据不同情况进行处理
    		switch (datas[1]) {
    			case "死亡":
    				prov.increaseDead(datas[2]);
    				break;
    			case "治愈":
    				prov.increaseCure(datas[2]);
    				break;
    			case "新增":
    				if (datas[2].equals("感染患者")) {
    					prov.increaseIp(datas[3]);
    				}
    				else { //新增疑似患者
    					prov.increaseSp(datas[3]);
    				}
    				break;
    			case "排除": //排除疑似患者
    				prov.decreaseSp(datas[3]);
    				break;
    			case "疑似患者": 
    				if (datas[2].equals("确诊感染")) { //疑似患者确诊
    					prov.increaseIpBySpConfirmed(datas[3]);
    				}
    				else { //疑似患者流入他省
    					Province prov2 = getProvinceByKey(datas[3]);
    					prov.decreaseSp(datas[4]);
    					prov2.increaseSp(datas[4]);
    				}
    				break;
    			case "感染患者": //感染患者流入他省
    				Province prov2 = getProvinceByKey(datas[3]);				
    				prov.decreaseIp(datas[4]);
    				prov2.increaseIp(datas[4]);
    				break;
    			default:
    				System.out.println("日志格式可能出现错误!解析失败!");
    				break;
    		}		
    	}
    
  • 写入文件:

    优先统计并写入全国的数据,若不需要输出全国则跳过

    根据type参数来判定输出哪些省份,默认为全部省份,对省份列表进行排序之后依次写入文件

    private void writeFile() {
    		 //若中途出错则直接结束程序
    		 if (isEnd) {
    			 return;
    		 }
    		 
    		 try {
    			 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputPath), "UTF-8"));
    			 
    			//判断输出列表是否包含全国
    			 if (isShowAllProvince || isOuputNationwide()) {
    				//打印全国数据
    				 Province nation = getNationStatResult();
    				 bw.write(nation.getOuputResult());
    				 bw.newLine();				 
    			 }
    			 			 
    			 if (isShowAllProvince == false) { //输出参数传入的省份				 
    				 //provinceArgsList.sort(new ProvinceCompartor());
    				 Collections.sort(provinceArgsList, Collator.getInstance(java.util.Locale.CHINA));
    				 for (String name : provinceArgsList) {
    					 if (!name.equals("全国")) {
    						 bw.write(provinceHashtable.get(name).getOuputResult());
    						 bw.newLine();
    					 }					 					
    				}
    			 }
    			 else {	//输出所有省份			 
    				 //allProvinceList.sort(new ProvinceCompartor());
    				 Collections.sort(allProvinceList, Collator.getInstance(java.util.Locale.CHINA));
    				 for (String name : allProvinceList) {					 
    					 bw.write(provinceHashtable.get(name).getOuputResult());
    					 bw.newLine();
    				}					 			 					 
    			 }
    			 
    			 bw.close();
    			 System.out.println("文件写入完毕.");
    		} 
    		 catch (Exception e) {
    			e.printStackTrace();
    		}
    	 }
    

六、单元测试截图和描述(总共写了十个测试例子 都运行成功)

软工实践寒假作业(2/2)_第5张图片

1. Test1 测试基础命令 只有 -log 和 -out 输出结果正确

图片

软工实践寒假作业(2/2)_第6张图片

2. Test2 测试 -date 传入日期参数 读取单个文件数据 输出结果正确

软工实践寒假作业(2/2)_第7张图片

软工实践寒假作业(2/2)_第8张图片

3. Test3 测试读取多个文件,并加入-type参数 输出结果正确

软工实践寒假作业(2/2)_第9张图片

软工实践寒假作业(2/2)_第10张图片

4. Test4 测试加入-province参数 日志中未出现香港,则数据为0 输出结果正确

软工实践寒假作业(2/2)_第11张图片

软工实践寒假作业(2/2)_第12张图片

5. Test5 测试-province参数 加入全国 排序正确 输出结果正确

软工实践寒假作业(2/2)_第13张图片

软工实践寒假作业(2/2)_第14张图片

6. Test6 测试-type 和-province复合参数,输出结果正确

软工实践寒假作业(2/2)_第15张图片

软工实践寒假作业(2/2)_第16张图片

7. Test7 测试-date 传入日志文件没有的日期,则当天数据为0 输出结果正确

软工实践寒假作业(2/2)_第17张图片

软工实践寒假作业(2/2)_第18张图片

8. Test8 测试-date 传入日期超过日志文件的最新日期,命令行给出输入错误提示

软工实践寒假作业(2/2)_第19张图片

图片

9. Test9 测试命令行指令错误检测是否成功,输入asd指令,命令行给出命令不存在提示

软工实践寒假作业(2/2)_第20张图片

图片

10. Test10 综合测试所有指令,并加入多个省份包括全国,查看排序是否正确,最终输出结果正确

软工实践寒假作业(2/2)_第21张图片

软工实践寒假作业(2/2)_第22张图片

七、单元测试覆盖率优化和性能测试

单元测试覆盖率

删除了一些没有用到的方法之后,主类覆盖率为95.7%,其余没有覆盖到的都是异常处理部分的代码

软工实践寒假作业(2/2)_第23张图片

JProfiler 性能报告总览:

软工实践寒假作业(2/2)_第24张图片

内存使用情况:

软工实践寒假作业(2/2)_第25张图片

八、代码规范链接

代码规范 codestyle.md

九、心路历程与收获

心路历程:第一次看到题目时,因为没有相关开发经验,我看不懂那些命令行指令是什么意思,不知道怎么实现,导致逃避了很久,先去学Unity相关的东西了,拖到后面才意识到已经快到截止日期,于是上网搜索了相关资料,才发现原来是这么简单的东西,好在文件处理的需求也很简单,最后花了两三天赶完了,但程序的架构却是非常草率,运行效率也不高,然后代码写完才知道要做单元测试,没有事先设计好测试代码全程测试,导致单元测试没有起到什么作用,代码设计时也没有考虑如何测试,导致测试代码只能跑完全程,都怪我当初太懒,如果第一时间我就去想办法解决需求的话,就有更多的时间思考代码的设计,程序的质量应该会得到显著的提升,也就能收获更多东西。

收获:最大的收获就是学会了单元测试和回归测试的概念,还有PSP表格,也懂得了开发一个程序最重要的不是编写代码,而是需求分析,在具体编码前就要做足一系列的设计工作,对开发过程各个环节做出时间的合理预估,才能更充分的安排时间,让开发过程更有效率,开发软件是一项工程,就像建筑一样,同样需要有详细的设计图和工程管理方案,才不会导致建到一半才发现存在各式各样的问题,那样付出的代价是非常大的,所以程序设计和项目管理也是非常重要的一环。

十、技术路线图相关的 5 个仓库

  1. 像素宝可梦风格引擎

    这个项目附带了从宝可梦系列视频游戏中提取的数据和图形。这个项目的目标不是创建和/或发布一个游戏,而是学习如何在Unity中创建类似的游戏。因此,这个项目除了可能成为未来类似项目的基础引擎之外,与宝可梦专营权无关,主要具有教育目的。

  2. Unity-Design-Pattern

    各种设计模式的Unity3D C#版本实现。

  3. Entitas框架

    Entitas是一个专门为C#和Unity制作的超快速实体组件系统框架(ECS)。

  4. QFramework

    是一套渐进式的快速开发框架。

  5. ML-Agents ToolKit

    ML-Agents ToolKit是一个开源的Unity3D机器学习插件,它使游戏和模拟能够作为智能代理培训的环境。

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