寒假作業 疫情統計(2/2)
這個作業屬於哪個課程 | https://edu.cnblogs.com/campus/fzu/2020SpringW |
---|---|
這個作業要求在哪里 | https://edu.cnblogs.com/campus/fzu/2020SpringW/homework/10281 |
這個作業的目標 | 代碼規范、GitHub使用、單元測試、編程 |
作業正文 | https://www.cnblogs.com/aahorse/p/12310680.html |
其他參考文獻 | https://github.com/youlookwhat/DesignPattern |
1、Github 連接
https://github.com/aaHorse/InfectStatistic-main
2、PSP 表格
PSP2.1 | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
---|---|---|---|
Planning | 計划 | 0.9*5*60 | 0.9*5*60 |
Estimate | 估計這個任務需要多少時間 | 0.1*5*60 | 0.1*5*60 |
Development | 開發 | 1*5*60 | - |
Analysis | 需求分析 (包括學習新技術) | 2*5*60 | 2*5*60 |
Design Spec | 生成設計文檔 | 0.2*5*60 | - |
Design Review | 設計復審 | 0.2*5*60 | 0.5*5*60 |
Coding Standard | 代碼規范 (為目前的開發制定合適的規范) | 0.5*5*60 | 0.5*5*60 |
Design | 具體設計 | 1*5*60 | 1*5*60 |
Coding | 具體編碼 | 3*5*60 | 2*5*60 |
Code Review | 代碼復審 | 0.4*5*60 | 0.5*5*60 |
Test | 測試(自我測試,修改代碼,提交修改) | 1*5*60 | 3*5*60 |
Reporting | 報告 | 1*5*60 | 1*5*60 |
Test Report | 測試報告 | 0.5*5*60 | - |
Size Measurement | 計算工作量 | 0.2*5*60 | 0.5*5*60 |
Postmortem & Process Improvement Plan | 事后總結, 並提出過程改進計划 | 1*5*60 | 2*5*60 |
合計 | 13*5*60 | 12*5*60 |
(備注:在家的計划都是按照一天學習 5 個小時來算的,每天其他的時間在社會實踐)
3、解題思路描述
-
一開始看到作業要求的時候,有一種無從下手的感覺。在網上沖浪了幾天后,甚至有了直接用 if-else 實現的念頭。
-
幾天后,助教發布了一篇關於作業引導和提示的博客,我對這篇博客進行了深入的學習和思考。非常感謝老師,感謝助教,通過這篇博客,我才有了一些想法。以下是解題思路:
-
在菜鳥教程,我找到了關於 Java 設計模式的的文檔,看了一下提到的命令模式、狀態模式、責任鏈模式,很遺憾的是,看不懂,不知道要用這些模式去實現什么功能;
-
轉戰 GitHub,我找到一個關於 Java 設計模式的 demo,是 Android 描述的,因為有學過 Android,看得懂,很快就上手了;
-
上手就好辦了,用一個類封裝命令的輸入,用命令行模式實現 list 命令,用責任鏈模式實現日志行匹配,用一個類將省份的疫情封裝起來;
-
一頓操作,就架構好了程序的框架,打通了輸入輸出,但是里面的具體實現還沒開始寫;
-
開始寫各個命令參數的具體實現,學了正則表達式匹配日志行;
-
功能實現后,將項目改為 Maven 項目(之前只是一個簡單的 Java 項目);
-
導 JUnit 包,寫測試文件,標准輸出文件,進行單元測試;
-
特別強調,期間各種懵逼,痛苦不堪!
4、設計實現過程
- 代碼組織

- 關鍵函數的流程圖

- 說明
代碼主要分為以下幾個部分:
① 命令行解析部分,通過上圖可以看到,是通過 CmdArgs.java
實現的;
② 命令行參數執行部分,這部分通過命令模式實現 list,在MyList.java
中實現功能;
③ 日志行解析部分,這部分通過 public void out()
調用;
④ 測試部分,這部分在后面會有說明;
5、代碼說明
- ① 命令行解析代碼
/**
* 命令行參數數組解析
*
* @param args 命令行傳來的參數數組
* @return 命令行參數數組解析后的鍵值對
*/
private HashMap<String, String[]> parseArgs(String[] args) {
ArrayList<String> argList = new ArrayList<>();
String argKey = null;
String[] valueMap = null;
//處理命令
valueMap = new String[1];
argList.add(args[0]);
argList.toArray(valueMap);
this.argMap.put(args[0], valueMap);
argList.clear();
//處理參數值
for (int i = 1; i < args.length; i++) {
String arg = args[i];
if (arg.startsWith("-")) {
if (argKey != null) {
valueMap = new String[argList.size()];
argList.toArray(valueMap);
this.argMap.put(argKey, valueMap);
argList.clear();
}
argKey = arg;
} else {
argList.add(arg);
}
}
if (argKey != null) {
valueMap = new String[argList.size()];
argList.toArray(valueMap);
this.argMap.put(argKey, valueMap);
argList.clear();
}
return this.argMap;
}
//------------------------代碼說明----------------------------------
/*
根據命令行傳進來的字符串數組,經過處理,轉為通過
HashMap<String key, String[] value>來存儲。
① 對於傳入的字符數組 String[] args,我們首先讀取 args[0],既作為key也作 value,
存到map中;
② 然后通過一個 for 循環,以 '-' 開頭的字符串作為鍵,兩個 '-'之間的字符串作為值,
存到map中;
*/
- ② 命令行模式實現 list
public class Lib {
/**
* 執行命令
*
* @throws FileNotFoundException
*/
public void execute() throws FileNotFoundException {
if (this.args == null) {
return;
}
CmdArgs cmdArgs = new CmdArgs(this.args);
//比較是否為 list 命令
if (!cmdArgs.getCmd().equals(MyList.PARAMS[0])) {
throw new UnsupportedOperationException("命令輸入錯誤");
}
/*
* list命令的參數,按照:list -log -date -province -type -out的順序執行,
* 如果一個非必要參數缺省:-date -type -province,
* 仍然按照順序執行缺省參數的默認值
* */
MyList list = new MyList(cmdArgs);
ControlCommand controlCommand = new ControlCommand();
//list -log
controlCommand.setCommands(0, new ListLogCommand(list));
//list -date
controlCommand.setCommands(1, new ListDateCommand(list));
//list -province
controlCommand.setCommands(2, new ListProvinceCommand(list));
//list -type
controlCommand.setCommands(3, new ListTypeCommand(list));
//list -out 輸出在最后執行
controlCommand.setCommands(4, new ListOutCommand(list));
//執行全部命令
controlCommand.sureExecuteAll();
}
}
class MyList {
/**
* list命令操作的日志含有的省份,以及 -provice 參數含有的省份,包括 “全國”
* 通過鍵值對存放,比如:
* key="全國" value=ProvinceStatus對象
*/
public LinkedHashMap<String, ProvinceStatus> linkedHashMap;
public void log() {}
public void date() throws FileNotFoundException {}
public void province() {}
public void type() {}
public void out() {}
/**
* 形成責任鏈
*/
private static AbstractLogLineType getChainOfLogLine() {}
/**
* 獲取需要讀入的log日志路徑
*
* @throws FileNotFoundException
*/
private void getLogPaths() throws FileNotFoundException {}
/**
* 讀入日志信息
*/
private void readLog() {}
/**
* 獲取 “全國” 的數據
*/
private void getProvinceStatusAll() {}
/**
* 刪除不需要輸出的省份
*/
private void deleteNotOutputProvicne() {}
/**
* 獲取需要輸出的信息
*/
private String getOutStr() {}
}
/**
* 命令的接口
*/
interface Command {
void execute() throws FileNotFoundException;
}
class ListLogCommand implements Command {}
class ListDateCommand implements Command {}
class ListProvinceCommand implements Command {}
class ListTypeCommand implements Command {}
class ListOutCommand implements Command {}
class ControlCommand {
/**
* list 總共有5種參數,分別是:-log -date -type -province -out
*/
private static final int CONTROL_SIZE = 5;
private Command[] commands;
public ControlCommand() {}
/**
* 添加一條待執行命令
*/
public void setCommands(int index, Command command) {}
/**
* 一條命令確認開始執行
*/
public void sureExecute(int index) throws FileNotFoundException {}
/**
* 全部命令確認開始執行
*/
public void sureExecuteAll() throws FileNotFoundException {}
}
//----------------------------代碼說明--------------------------------
/*
以上是通過命令模式實現 list 的代碼框架,說明幾點:
① 為了適用於項目,命令模式並沒有將 list 看成是一條命令,而是將它的每一個參數看作是命令。
換句話說,一條 list 在這里會被看成 5 條命令,並且通過 ControlCommand 中的
setCommands(int index, Command command)設定執行順序,這 5 條命令會按照規定好的順序執行,
它們的順序是:list -log xxx -date xxx -province xxx xxx -type xxx xxx -out xxx
注意:即使某一條非必要的參數缺失,也會通過默認值補上並執行
② MyList 類是主要的執行類,其中,public void -out() 又是主要的執行函數
*/
- ③ 責任鏈模式匹配日志行
class MyList {
//.......
/**
* 形成責任鏈
*/
private static AbstractLogLineType getChainOfLogLine() {
NewInfectorLogLine newInfectorLogLine = new NewInfectorLogLine(AbstractLogLineType.LOGLINE_TYPE[0]);
NewSuspectLogLine newSuspectLogLine = new NewSuspectLogLine(AbstractLogLineType.LOGLINE_TYPE[1]);
InInfectorLogLine inInfectorLogLine = new InInfectorLogLine(AbstractLogLineType.LOGLINE_TYPE[2]);
InSuspectLogLine inSuspectLogLine = new InSuspectLogLine(AbstractLogLineType.LOGLINE_TYPE[3]);
DeathLogLine deathLogLine = new DeathLogLine(AbstractLogLineType.LOGLINE_TYPE[4]);
CureLogLine cureLogLine = new CureLogLine(AbstractLogLineType.LOGLINE_TYPE[5]);
DefiniteLogLine definiteLogLine = new DefiniteLogLine(AbstractLogLineType.LOGLINE_TYPE[6]);
ExcludeLogLine excludeLogLine = new ExcludeLogLine(AbstractLogLineType.LOGLINE_TYPE[7]);
MismatchingLogLine mismatchingLogLine = new MismatchingLogLine(AbstractLogLineType.LOGLINE_TYPE[8]);
newInfectorLogLine.setNextLogType(newSuspectLogLine);
newSuspectLogLine.setNextLogType(inInfectorLogLine);
inInfectorLogLine.setNextLogType(inSuspectLogLine);
inSuspectLogLine.setNextLogType(deathLogLine);
deathLogLine.setNextLogType(cureLogLine);
cureLogLine.setNextLogType(definiteLogLine);
definiteLogLine.setNextLogType(excludeLogLine);
excludeLogLine.setNextLogType(mismatchingLogLine);
return newInfectorLogLine;
}
/**
* 讀入日志信息
*/
private void readLog() {
AbstractLogLineType abstractLogLineType = getChainOfLogLine();
for (int i = 0; i < this.logPath.size(); i++) {
String content = FileOperate.readFile(this.dir + this.logPath.get(i));
if (content != null) {
String[] logLine = content.split("#");
for (int j = 0; j < logLine.length; j++) {
//處理日志行
abstractLogLineType.matchLogLine(logLine[j], this.linkedHashMap);
}
}
}
}
}
/**
* 日志行
*/
abstract class AbstractLogLineType {
/**
* 正則表達式
*/
public final static String[] LOGLINE_TYPE = {
"(\\S+) 新增 感染患者 (\\d+)人",
"(\\S+) 新增 疑似患者 (\\d+)人",
"(\\S+) 感染患者 流入 (\\S+) (\\d+)人",
"(\\S+) 疑似患者 流入 (\\S+) (\\d+)人",
"(\\S+) 死亡 (\\d+)人",
"(\\S+) 治愈 (\\d+)人",
"(\\S+) 疑似患者 確診感染 (\\d+)人",
"(\\S+) 排除 疑似患者 (\\d+)人",
"([\\s\\S]*)"
};
/**
* 日志行對應的正則表達式
*/
protected String logLineTypeRegExp;
/**
* 責任鏈中的下一個元素
*/
protected AbstractLogLineType nextLogLineType;
public void setNextLogType(AbstractLogLineType nextLogLineType) {
this.nextLogLineType = nextLogLineType;
}
/**
* 匹配責任鏈
*/
public void matchLogLine(String logLineStr, LinkedHashMap<String, ProvinceStatus> linkedHashMap) {
if (logLineStr.matches(logLineTypeRegExp)) {
executeLogLine(logLineStr, linkedHashMap);
return;
}
if (nextLogLineType != null) {
nextLogLineType.matchLogLine(logLineStr, linkedHashMap);
}
}
protected abstract void executeLogLine(String logLineStr, LinkedHashMap<String, ProvinceStatus> linkedHashMap);
}
/**
* <省> 新增 感染患者 n 人
*/
class NewInfectorLogLine extends AbstractLogLineType {
public NewInfectorLogLine(String logLineTypeRegExp) {
this.logLineTypeRegExp = logLineTypeRegExp;
}
@Override
protected void executeLogLine(String logLineStr, LinkedHashMap<String, ProvinceStatus> linkedHashMap) {
Pattern pattern = Pattern.compile(AbstractLogLineType.LOGLINE_TYPE[0]);
Matcher matcher = pattern.matcher(logLineStr);
if (matcher.find()) {
String province = matcher.group(1);
int num = Integer.parseInt(matcher.group(2));
if (!linkedHashMap.containsKey(province)) {
linkedHashMap.put(province, new ProvinceStatus(province));
}
ProvinceStatus provinceStatus = linkedHashMap.get(province);
provinceStatus.setInfect(provinceStatus.getInfect() + num);
linkedHashMap.put(province, provinceStatus);
}
}
}
/**
* <省> 新增 疑似患者 n 人
*/
class NewSuspectLogLine extends AbstractLogLineType {}
/**
* <省1> 感染患者 流入 <省2> n人
*/
class InInfectorLogLine extends AbstractLogLineType {}
/**
* <省1> 疑似患者 流入 <省2> n人
*/
class InSuspectLogLine extends AbstractLogLineType {}
/**
* <省> 死亡 n人
*/
class DeathLogLine extends AbstractLogLineType {}
/**
* <省> 治愈 n人
*/
class CureLogLine extends AbstractLogLineType {}
/**
* <省> 疑似患者 確診感染 n人
*/
class DefiniteLogLine extends AbstractLogLineType {}
/**
* <省> 排除 疑似患者 n人
*/
class ExcludeLogLine extends AbstractLogLineType {}
/**
* 不匹配的情況
*/
class MismatchingLogLine extends AbstractLogLineType {}
//------------------------------代碼說明---------------------------
/*
通過責任鏈模式來匹配日志行,避免使用過多的 if-else ,有利於項目的拓展和維護
說明幾點:
① 8種日志行,每一種都有一條對應的正則表達式來匹配,如果不匹配,則在責任鏈的最后
面由通配類來接住,通配類的正則表達式為:"([\\s\\S]*)"
② 當一個日志行對應的類獲得執行時,通過設計好的正則表達式抽取出需要的變量,用這些
變量來操作一個全局的 LinkedHashMap<String, ProvinceStatus> linkedHashMap
③ 所有日志行執行結束后,就會將日志信息轉為:
LinkedHashMap<String, ProvinceStatus> linkedHashMap,再由這個map處理輸出的問題
*/
- ④ 省份實體類
/**
* 省/全國類
*/
class ProvinceStatus {
/**
* 省份名稱,包括“全國”
*/
private String name;
/**
* 感染患者數量
*/
private int infect;
/**
* 疑似患者數量
*/
private int suspect;
/**
* 治愈患者數量
*/
private int cure;
/**
* 死亡患者數量
*/
private int death;
public ProvinceStatus(String name) {
this.name = name;
// 默認等於 0
this.infect = 0;
this.suspect = 0;
this.cure = 0;
this.death = 0;
}
/**
* @param typeValue 命令行參數 -type 指定的一種輸出類型
* @return 返回整理好的字符串
*/
public String getOutTypeStr(String typeValue) {
switch (typeValue) {
case "ip":
return " " + "感染患者" + this.infect + "人";
case "sp":
return " " + "疑似患者" + this.suspect + "人";
case "cure":
return " " + "治愈" + this.cure + "人";
case "dead":
return " " + "死亡" + this.death + "人";
default:
throw new IllegalStateException("Unexpected value: " + typeValue);
}
}
}
//-------------------------------代碼說明--------------------------
/*
有必要展示出來,但是沒什么需要說明的東西 -:)
*/
6、單元測試截圖和描述
單元測試分為兩種,一種是僅通過函數內的代碼來實現,一種是將命令集寫入文件,並且將命令對應的標准輸出也寫入文件,再通過處理文件操作來實現。以下是代碼截圖:
- ① 單元測試目錄結構

- ② CmdArgs 類部分函數的測試


- ③ MyList 類部分函數測試


- ④ 通過文件來進行測試
存儲命令集

存儲命令對應的標准輸出文件路徑

測試函數

運行結果

7、單元測試覆蓋率優化和性能測試
以 MyListTest 測試類為例,說明覆蓋率優化問題
優化前:
(分析發現,因為測試中沒有用命令模式添加命令,而是直接調用命令執行,所以命令模式板塊全部沒有被調用到。優化措施:改用命令模式調用命令執行。)

優化后:
(命令模塊被調用到,單元測試覆蓋率明顯提升)

性能測試
(因為看不懂網上關於 JProfile 使用的文檔,摸索了一下,但是對於如何利用這個工具進行性能優化還是沒有入門,現在對於這個工具的認識僅停留在這是一個用於查找程序內存泄漏的工具並且和服務器開發密切相關。至於性能方面,因為這個本項目對於性能方面的要求並不高,並且我的程序是單線程的,運行結束就停止了,繼續研究下去的意義不大。我猜測是因為我對這個工具的認識還沒有入門,或許存在某一個按鈕可以將程序循環執行。下面我演示的是通過 JProfile 查找內存泄漏的方法,當然,內存泄漏的地方是我故意用死循環添加上去的)
添加死循環

執行測試
(多次 Run GC 仍然存在鋸齒圖,體現內存泄漏)

(show Source 定位到該死循環)

大家都放了這張圖

8、代碼規范的鏈接
https://github.com/aaHorse/InfectStatistic-main/blob/master/221701414/codestyle.md
9、心路歷程與收獲
做完整個項目下來,和之前做項目的感覺一樣:非常的痛苦。做項目的感覺和平時學習的感覺是不一樣的。平時學習遇到問題可以跳過,甚至放棄,但是做項目的時候,就不得不迎難而上。
選課的時候,我無意間在博客園看到汪老師的博客,大概知道了這是一門真·實踐課。做項目雖然痛苦,但是我當時還是用了一個很大的分值投了這門課,並且選上了。起碼到目前,我沒有后悔的念頭,希望能堅持下來。
做項目我的原則是認真對待,盡早開始,盡力完成。
這次作業,最需要感謝的是助教發的作業引導。通過學習和實踐,作業引導里面的東西也是我收獲最大的地方。通過里面的博客,我才知道命令模式、責任鏈模式、單元測試這些東西,在作業中才懂得應用。希望以后每次作業,都能發一下關於作業引導的博客。
閱讀了《構建之法》,我看到了優秀程序員應該具備的思想和能力,我也督促自己,朝着這個方向去增長自己的技能點,努力成為一個優秀的程序員。
10、5 個學習倉庫
① mall 商城
鏈接:https://github.com/macrozheng/mall
介紹:mall 項目是一套電商系統,包括前台商城系統及后台管理系統,基於 SpringBoot+MyBatis 實現,采用 Docker 容器化部署。
備注:一直都想學這個項目,之前曾經研究過一會,是一個值得花時間研究的好項目。
② 算法與數據結構
鏈接:https://github.com/doubleview/data-structure
介紹:此項目是基於 java 語言的關於數據結構的代碼實現,包含所有經典數據結構算法,並且注釋完善,非常適合了解和學習數據結構。
備注:現在有點后悔當初學數據結構的時候用 C 語言來描述,C 語言確實可以學到算法的思想,但是 Java 才是偏應用的,所以一定要補上這個知識點。
③ 開發模式
鏈接:https://github.com/youlookwhat/DesignPattern
介紹:Java 設計模式歸納 (觀察者、工廠、抽象工廠、單例、策略、適配器、命令、裝飾者、外觀、模板方法、狀態、建造者、原型、享元、代理、橋接、組合、迭代器、中介者、備忘錄、解釋器、責任鏈)。
備注:這次作業后發現,用開發模式寫出來的代碼真的一目了然,多學習,多實踐。
④Java 學習+面試
鏈接:https://github.com/Snailclimb/JavaGuide
介紹:一份涵蓋大部分 Java 程序員所需要掌握的核心知識。
備注:這個項目 Start 比較多,值得學習。
⑤Java 的 List 框架
鏈接:https://github.com/CarpenterLee/JCFInternals
介紹:主要從數據結構和算法層面分析 JCF 中 List, Set, Map, Stack, Queue 等典型容器,結合生動圖解和源代碼,幫助讀者對 Java 集合框架建立清晰而深入的理解。
備注:List 框架作為 Java 應用最廣泛的框架之一,值得花時間進行深入學習。