程式碼重構之法——方法重構分析

WeihanLi發表於2020-09-09

程式碼重構之法——方法重構分析

Intro

想要寫出比較優秀的程式碼,需要時刻警惕程式碼中的壞味道,今天想寫一篇文章介紹一下如何分析你的方法是不是需要考慮重構

一個方法通常有三個部分組成,輸入(Input),輸出(Output),方法體(Method Body),我們就從這三個方面來分析一個方法是否該考慮重構

Input

方法輸入也就是方法的引數,通常來說一個方法的引數基本可以控制在7個以內(僅作參考,可以自己衡量,SonarQube 預設方法最多七個引數),如果你的方法引數過多的話,可能就需要考慮重構一個方法引數了,通常的做法是封裝一個獨立的 model,引數作為 model 的屬性。

舉一個常見的例子,比如一個新聞列表的API,起初可能很簡單,就只需要一個 lastId,一個 count 兩個引數,但是隨著業務需求的增加,可能會增加很多別的引數,比如前端提供一個 keyword 進行全文檢索,提供一個 sortBy 進行排序,根據新聞標題匹配,作者名稱匹配,分類匹配,根據釋出時間篩選等等,最後可能會導致這個方法的引數有很多

通常我會新增一個 XxxRequest 的 model,然後方法引數替換成這個 model,然後指定 [FromQuery] 就可以了,可以對比一個修改前後的差異,是不是後面的方式更清爽一些呢

Task<IActionResult> List(int lastId, int count, string title, string author, string keyword, int categoryId, string sortBy, DateTime? beginTime, DateTime? endTime)
Task<IActionResult> List([FromQuery]NewsListQueryRequest request)

Output

Output 就是方法的返回值,儘可能返回具體的型別,儘可能避免使用 Tuple 等型別,方法的返回值應該具有明確的意義

使用具體的 Model 代替 Tuple 返回值,尤其是一些 public 的,要被外部訪問的方法更應該返回具體的型別,雖然 C# 7.2 開始支援了 named tuple,會比之前友好很多,支援給 tuple 指定名稱,但是這只是編譯器級別的,實際還是 Item1,Item2 ...,還是比較推薦使用具體的 model,更加明瞭

Body

通常一個方法不要太長,曾經在群裡看到群友吐槽一個方法兩千多行,這樣的方法維護起來簡直就是災難,不要讓一個方法太長,保持方體體的簡單,一些通用的邏輯通過 Filter 或結合 AOP 來實現

Sonar 有一個分析方法複雜度的一個方法,官方稱之為 Cognitive Complexity

簡單介紹一下,程式碼裡的 if/switch/for/foreach/try...catch/while 都會增加方法的複雜度,出現一層巢狀則複雜度再加1, Sonar 預設的一個方法的複雜度不能超過 15

來幾個簡單的示例:

下面這個方法的複雜度是 3,有三個 if(else) 分支

void Method1(int num)
{
  if(num > 0)
  {
  } 
  else if(num <0)
  {
  } 
  else
  {
  }
}

下面這個方法的複雜度是 3,foreach 帶來了 1 的複雜度,if 也是1的複雜度,但是因為 if 是巢狀在 foreach 內部的,一層巢狀會導致複雜度增加1

void Method1(int[] nums)
{
  foreach(var num in nums)
  {
    if(num > 0)
    {
    }
  }
}

下面這個方法的複雜度在上面的基礎上增加了兩個 catch,這使得複雜度會加 2,從而變成 5

void Method1(int[] nums)
{
  try
  {
    foreach(var num in nums)
    {
      if(num > 0)
      {
      }
    }
  }
  catch(InvalidOperationException e)
  {
  }
  catch(Exception e)
  {
  }
}

更多示例可以參考官方介紹: https://www.sonarsource.com/docs/CognitiveComplexity.pdf

Reduce Complexity

前面我們介紹了一些複雜度的分析,如何能夠切實有效的降低方法的複雜度呢:

  1. 方法引數不宜過多,引數過多考慮重構輸入引數,通常可以新建一個 model 來管理輸入引數
  2. 方法返回值不宜使用意義不明的返回值,儘量不用 Tuple 作為返回值
  3. 方法的行數不要太多,利用新語法減少行數,減少 if 判斷,比如使用 null 傳播符代替一系列的 iflist?.FirstOrDefault()?.Name, a ??="test"
  4. 多個方法的相同邏輯使用切面邏輯處理,比如每個方法裡都有 try...catch,那我們就可以使用一個複雜 try...catch 的切面邏輯,如果是 mvc/webapi 也可以藉助 ExceptionFilter 來實現
  5. 引數校驗使用微軟的 ModelValidator 或者使用 FluentValidation 進行校驗,在程式碼裡儘量避免使用大量的 if 判斷導致複雜度的增加
  6. 仔細 review 程式碼,有些邏輯是否合併在一起,避免在多個 if 裡巢狀相同遍歷邏輯
  7. and more...(等你來補充

More

除了自己主動感知方法的複雜度之外,我們也可以藉助一些第三方的靜態程式碼分析工具來分析我們的程式碼,從而獲得一些修改建議進而保證程式碼的高質量。

SonarQube 是目前使用較多的工具,可以方便的和 CI 整合,有一個 SonarCloud 網站提供雲服務,可以輕鬆為你的開源專案整合靜態程式碼分析,有興趣可以看看,地址是 https://sonarcloud.io/,之前還用過 codacy,似乎不太流行,推薦 SonarQube

Reference

相關文章