一、背景 開發一款Idea插件,實現對yaml文件的定製化格式檢查。 !! 後指定的類路徑是否準確 yaml中的key是否equal類中field的name value是否能夠轉換成類中field的類型 …… 完成代碼功能上線後,使用過程發現很多問題。後在主管幫助下,對代碼進行了重構。事後對重構前後 ...
一、背景
-
!! 後指定的類路徑是否準確 -
yaml中的key是否equal類中field的name -
value是否能夠轉換成類中field的類型 -
……
二、代碼比較
1 結構設計
before:
after:
after:增加抽象類中的celtVisitMapping層代碼,對多個代碼檢查模塊做統一代理。做了錯誤的捕獲,後面也可以做一些其他的統一處理(日誌、標識參數等),方便拓展。
2 代碼可讀性
2.1命名
一個好的命名能輸出更多的信息,它會告訴你,它為什麼存在,它是做什麼事的,應該怎麼使用。
2.1.1 類
功能 |
時間 |
類名稱 |
檢查yaml文件是否可以成功反序列化成項目中的對象。 |
before |
YamlBaseInspection |
after |
CeltClassInspection |
比較:
類的命名要做到見名知意,before的命名YamlBaseInspection做不到這一點,通過類名並不能夠獲取到有用的信息。對於CeltClassInspection的命名格式,在瞭解插件功能的基礎上,可以直接判斷出屬於yaml類格式檢查。
2.1.2 函數
功能 |
時間 |
函數名稱 |
比較value是否可以反序列化成PsiClass |
before |
compareNameAndValue |
after |
compareKeyAndValue |
1.name是Class中field中的name,通過函數名稱並不能夠看出,函數名傳達信息不准確。
after:函數名前後單位統一,key和Value是一個yaml中map的兩個概念。能從函數名得出函數功能:檢驗Key和Value的是否準確。
2.1.3 變數
//before
ASTNode node = mapping.getNode().findChildByType(YAMLTokenTypes.TAG);
String className = node.getText().substring(2);
//after
ASTNode node = mapping.getNode().findChildByType(YAMLTokenTypes.TAG);
String tagClassName = node.getText().substring(2);
比較:
String className 來源可以有兩個:
1.通過yaml中tag標簽在項目中查找得到。
after:通過變數名 tagClass 可以快速準確的獲取變數名屬於上述來源中的第一個,能夠降低閱讀代碼的複雜度。變數名可以傳遞更多有用的信息。
2.2 註釋
2.2.1 註釋格式
-
before 1.無註釋 2.有註釋不符合規範 -
after 有註釋符合JavaDoc規範
//before
private boolean checkSimpleValue(PsiClass psiClass, PsiElement value)
/**
* 檢查枚舉類的value
* @return
*/
boolean checkEnum(PsiClass psiClass,String text)
//after
/**
* @param psiClass
* @param value
* @return true 正常;false 異常
*/
private boolean checkSimpleValue(PsiClass psiClass, PsiElement value, ProblemsHolder holder)
2.2.2 註釋位置
before:
//simple類型,檢查keyName 和 value格式
if (PsiClassUtil.isSimpleType(psiClass)) {
//泛型(T)、Object、白名單:不進行檢查
} else if (PsiClassUtil.isGenericType(psiClass)) {
//complex類型
} else {
}
after:
// simpleValue 為 null 或者 "null"
if (YamlUtil.isNull(value)) {
}
if (PsiClassUtil.isSimpleType(psiClass)) {
// simple類型,檢查keyName 和 value格式
checkSimpleValue(psiClass, value, holder);
} else if (PsiClassUtil.isGenericType(psiClass)) {
//泛型(T)、Object、白名單:不進行檢查
} else {
checkComplexValue(psiClass, value, holder);
}
行內註釋應該在解釋的代碼塊內。
2.3 方法抽象
before:
public void compareNameAndValue(PsiClass psiClass, YAMLValue value) {
//simple類型,檢查keyName 和 value格式
if (PsiClassUtil.isSimpleType(psiClass)) {
//泛型(T)、Object、白名單:不進行檢查
} else if (PsiClassUtil.isGenericType(psiClass)) {
//complex類型
} else {
Map<String, PsiType> map = new HashMap<>();
Map<YAMLKeyValue, PsiType> keyValuePsiTypeMap = new HashMap<>();
//init Map<KeyValue,PsiType>, 註冊keyName Error的錯誤
PsiField[] allFields = psiClass.getAllFields();
YAMLMapping mapping = (YAMLMapping) value;
Collection<YAMLKeyValue> keyValues = mapping.getKeyValues();
for (PsiField field : allFields) {
map.put(field.getName(), field.getType());
}
for (YAMLKeyValue keyValue : keyValues) {
if (map.containsKey(keyValue.getName())) {
keyValuePsiTypeMap.put(keyValue, map.get(keyValue.getName()));
} else {
holder.registerProblem(keyValue.getKey(), "找不到這個屬性", ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);
}
}
keyValuePsiTypeMap.forEach((yamlKeyValue, psiType) -> {
//todo:數組類型type 的 check
if (psiType instanceof PsiArrayType || PsiClassUtil.isCollectionOrMap(PsiTypeUtil.getPsiCLass(psiType, yamlKeyValue))) {
} else {
compareNameAndValue(PsiTypeUtil.getPsiCLass(psiType, yamlKeyValue), yamlKeyValue.getValue());
}
});
}
}
after:
public void compareKeyAndValue(PsiClass psiClass, YAMLValue value, ProblemsHolder holder) {
// simpleValue 為 null 或者 "null"
if (YamlUtil.isNull(value)) {
return;
}
if (PsiClassUtil.isSimpleType(psiClass)) {
// simple類型,檢查keyName 和 value格式
checkSimpleValue(psiClass, value, holder);
} else if (PsiClassUtil.isGenericType(psiClass)) {
//泛型(T)、Object、白名單:不進行檢查
} else {
checkComplexValue(psiClass, value, holder);
}
}
boolean checkComplexValue();
比較:
before: compareNameAndValue方法代碼過長,一個屏幕不能瀏覽整個方法。方法的框架不能夠簡潔明亮,即要負責判斷類型,進行分發處理,還需要負責complex類型的比較,功能耦合。
after:把對complex對象的比較抽離出一個方法,該方法負責進行複雜類型的比較。原方法只負責區分類型,並調用實際的方法比較。能夠清晰的看出方法架構,代碼後期易維護。
2.4 if複雜判斷
after
before:代碼中使用複雜的if嵌套,if是造成閱讀代碼困難的最重要因素之一。if和for迴圈的嵌套深V嵌套,代碼邏輯不清晰,代碼維護比較高,拓展複雜。
after:減少了if嵌套,代碼理解成本低,代碼易維護,易拓展。
3.魯棒性
3.1 報錯信息精準
//before
holder.registerProblem(value, "類型無法轉換", ProblemHighlightType.GENERIC_ERROR);
//after
String errorMsg = String.format("cannot find field:%s in class:%s", yamlKeyValue.getName(), psiClass.getQualifiedName());
holder.registerProblem(yamlKeyValue.getKey(), errorMsg, ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);
before:對於格式檢查出的錯誤提示很隨意,只說明瞭類型無法轉換,from是什麼?to是什麼?都沒有說明白,很多有用的信息並沒有反饋到用戶。用戶使用體驗會比較差,像是一個完全不成熟的產品。
after:提示無法在class中找到某一個field。並且明確說明瞭是哪一個field,哪一個class。幫組用戶及時準確定位錯誤並解決。
3.2 代碼健壯性(異常處理)
空指針
before:
代碼需要考慮異常(空指針、預期之外的場景),下麵代碼有空指針異常,deleteSqlList可能為null,3行調用會拋出NPE,程式沒有捕獲處理。
YAMLKeyValue deleteSqlList = mapping.getKeyValueByKey("deleteSQLList");
YAMLSequence sequence = (YAMLSequence) deleteSqlList.getValue();
List<YAMLSequenceItem> items = sequence.getItems();
for (YAMLSequenceItem item : items) {
if (!DELETE_SQL_PATTERN.matcher(item.getValue().getText()).find()) {
holder.registerProblem(item.getValue(), "sql error", ProblemHighlightType.GENERIC_ERROR);
}
}
after:
@Override
public void doVisitMapping(@NotNull YAMLMapping mapping, @NotNull ProblemsHolder holder) {
ASTNode node = mapping.getNode().findChildByType(YAMLTokenTypes.TAG);
//取出node
if (YamlUtil.isNull(node)) {
return;
}
if (node.getText() == null || !node.getText().startsWith("!!")) {
// throw new RuntimeException("yaml插件監測異常,YAMLQuotedTextImpl text is null或者不是!!開頭");
holder.registerProblem(node.getPsi(), "yaml插件監測異常,YAMLQuotedTextImpl text is null或者不是!!開頭", ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);
return;
}
String tagClassName = node.getText().substring(2);
PsiClass[] psiClasses = ProjectService.findPsiClasses(tagClassName, mapping.getProject());
if (ArrayUtils.isEmpty(psiClasses)) {
String errorMsg = String.format("cannot find className = %s", tagClassName);
holder.registerProblem(node.getPsi(), errorMsg, ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);
return;
}
if (psiClasses.length == 1) {
compareKeyAndValue(psiClasses[0], mapping, holder);
}
}
比較:
after:代碼對異常場景考慮更全面,tagString格式非法,空指針,數組越界等等情況。代碼更健壯。
switch中的default
before:
switch (className) {
case "java.lang.Boolean":
break;
case "java.lang.Character":
break;
case "java.math.BigDecimal":
break;
case "java.util.Date":
break;
default:
}
after:
switch (className) {
case "java.lang.Boolean":
break;
case "java.lang.Character":
break;
case "java.math.BigDecimal":
break;
case "java.util.Date":
case "java.lang.String":
return true;
default:
holder.registerProblem(value, "未識別的className:" +className, ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);
return false;
}
比較:
before:代碼存在隱藏邏輯String類型會走default邏輯不處理,增加代碼理解的難度。未對非simple類型的default有異常處理。
after:對String類型寫到具體case,暴漏隱藏邏輯。並對default做異常處理,代碼更健壯。
作者|王耀興(承録)
本文來自博客園,作者:古道輕風,轉載請註明原文鏈接:https://www.cnblogs.com/88223100/p/How-to-eliminate-bad-code-smell.html