案例分析|如何消除代碼壞味道

来源:https://www.cnblogs.com/88223100/archive/2023/02/17/How-to-eliminate-bad-code-smell.html
-Advertisement-
Play Games

一、背景 開發一款Idea插件,實現對yaml文件的定製化格式檢查。 !! 後指定的類路徑是否準確 yaml中的key是否equal類中field的name value是否能夠轉換成類中field的類型 …… 完成代碼功能上線後,使用過程發現很多問題。後在主管幫助下,對代碼進行了重構。事後對重構前後 ...


 

一、背景

開發一款Idea插件,實現對yaml文件的定製化格式檢查。
  • !! 後指定的類路徑是否準確
  • yaml中的key是否equal類中field的name
  • value是否能夠轉換成類中field的類型
  • ……

圖片

完成代碼功能上線後,使用過程發現很多問題。後在主管幫助下,對代碼進行了重構。事後對重構前後代碼的好壞進行分析總結,文章下麵將從結構設計、代碼可讀性、魯棒性3個角度對重構前後代碼作比較。

二、代碼比較

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

比較:
before:

1.name是Class中field中的name,通過函數名稱並不能夠看出,函數名傳達信息不准確

2.Value是yaml中map的概念前後單位不統一。兩者放在一起,會使閱讀代碼者很迷惑。

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標簽在項目中查找得到。

2.PsiClass中的變數類型得出。

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複雜判斷

before

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);
    }
}
每一步操作都會考慮異常情況,7、11、20行都有對空指針異常的處理。

比較:

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 關於靜態代碼塊和匿名代碼塊以及結構體在程式運行過程中的調用順序實驗(續) 之前發過一篇博客講述自己對靜態代碼塊、匿名代碼塊以及結構體在程式中運行結果的小實驗。本次再接觸到abstract抽象類後,覺得在做一個實驗,看看抽象類方法繼承中三個模塊的調用順序。所編寫的代碼如下: Application類( ...
  • 特別:下文的“容量”、“數組長度”,“capacity” 都是指底層數組長度,即 table.length 1 一般數據結構及特點 數組:占用連續記憶體的數據結構,查找容易[O(1)],插入困難[O(n)] 鏈表:由一組指向(單向或者雙向)的節點連接的數據結構,記憶體不連續,查找困難,但插入刪除容易 哈 ...
  • 1.前言 本文章是筆主在聲哥的手寫RPC框架的學習下,對註冊中心的一個拓展。因為聲哥某些部分沒有保留拓展性,所以本文章的項目與聲哥的工程有部分區別,核心內容在Curator的註冊發現與註銷,思想看準即可。 本文章Git倉庫:zko0/zko0-rpc 聲哥的RPC項目寫的確實很詳細,跟學一遍受益匪淺 ...
  • 來源:https://juejin.cn/post/7173271507047546893 近期,Spring 6 的第一個 GA 版本發佈了,其中帶來了一個新的特性——HTTP Interface。這個新特性,可以讓開發者將 HTTP 服務,定義成一個包含特定註解標記的方法的 Java 介面,然後 ...
  • 教程簡介 線上營銷簡介 - 從線上營銷,簡介,術語,SEO友好網站,線上廣告,移動廣告,搜索引擎營銷,電子郵件營銷,聯盟營銷,社交媒體營銷,從簡單和簡單的術語瞭解線上營銷,聲譽營銷,內容營銷,博客,橫幅和論壇,網站分析,努力,影響,利弊,名人線上營銷人員。 教程目錄 線上營銷介紹 線上營銷術語 SE ...
  • 1.讓伺服器監聽客戶端的連接請求 1.1 代碼塊 #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include<stdio.h> #include<stdlib.h> #define BUFFER_LEN 1 ...
  • 關於靜態代碼塊和匿名代碼塊以及結構體在程式運行過程中的調用順序實驗 ​ 今天學習JAVA看到了static修飾符部分,講到了有關匿名代碼和靜態代碼部分。此時又突然想到前面所學關於new是調用類的結構體知識,同時結合繼承關係,想看看在這些條件下匿名代碼塊、靜態代碼塊以及類構造體的調用順序。 ​ 編寫了 ...
  • 教程簡介 Google Plus初學者教程 - 從基本到高級概念的簡單簡單步驟學習Google Plus,其中包括簡介,業務頁面設置,創建新帳戶,瀏覽Google Plus,添加業務詳細信息,上傳個人資料圖片,添加封面圖片,圈子,社區,環聊,活動,上傳帖子,編輯帖子,刪除帖子,轉發帖子,報告帖子,促 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...