【深入淺出系列】之程式碼可讀性

2023-08-28 12:00:44

這是「深入淺出系列」文章的第一篇,主要記錄和分享程式設計的一些思想和方法論,如果讀者覺得所有受用,還請「一鍵三連」,這是對我最大的鼓勵。

一、老生常談,到底啥是可讀性

一句話:見名知其義。有人說好的程式碼必然有清晰完整的註釋,我不否認;也有人說程式碼即註釋,是程式碼簡潔之道的最高境界,我也不否認。但我都不完全接受,如果照搬前者,有人會在每個方法、每個迴圈、每個判斷都新增大量註釋,對於一個表達不嚴謹的coder來說,程式碼與漢字可能詞不達意;而且,一旦程式碼邏輯發生變化,註釋改不改?對於後者,英語水平可能也就是個半吊子,動詞名詞不區分,真能做到程式碼即註釋的有多少人?

二、罵歸罵,總歸要硬著頭皮幹

先來舉個簡單例子:

public StepExitEnum doExecute(StepContext stepContext) throws Exception {
    String targetFilePath = this.getOriginFilePath(stepContext.getJobContext());//獲取目標路徑
    File targetDir = new File(targetFilePath);
    if (!targetDir.exists()) {
        targetDir.mkdirs();//如果不存在目錄則建立
    }

    String encryptedFilePath = this.getEncryptedFilePath(stepContext.getJobContext());//獲取加密檔案路徑
    String fileName = this.getFileName(stepContext);//獲取檔名
    File[] encryptedFiles = new File(encryptedFilePath).listFiles(this.buildFilenameFilter(fileName));//過濾檔案

    FileEncryptor dencryptor = this.buildFileEncryptor(stepContext);//建立FileEncryptor
    Stream.of(encryptedFiles)
            .forEach(encryptFile -> {
                File targetFile = new File(targetFilePath, encryptFile.getName());
                dencryptor.invoke(encryptFile, targetFile);//解密檔案
            });

    return StepExitEnum.CONTINUING;
}

這種程式碼很常見,耐著性子其實也容易看懂:建立目錄->讀取加密檔案->解密檔案,就當前來說其實滿足了業務需求也就可以了,但不夠優雅,從長期來講,這會產生bad smell,首先,「如果不存在目錄則建立」、「獲取檔名」這類註釋有何意義?有可能這是coder當時的方案思路,但這裡真的需要嗎?它確確實實影響我的注意力了,但我沒有獲取到任何有價值資訊;其次,若想要理解doExecute這個方法的目的,必須通讀程式碼,而我只是想知道它做了什麼事;最後,這個方法如果某一行出問題了,那麼影響範圍是整個業務流程。

如果後期需要改動,大部分人可能會增加條件判斷,或是在後面繼續追加程式碼實現,最後會導致越來越難以閱讀,這其實也就是「能執行就不要動它」這個梗的根源了,因為沒人能讀明白它到底做了什麼,但又不得不改,同時可能伴隨著「口吐芬芳」。

三、意識先行,從一行做起

那麼到底該如何做呢?下面是我的一個例子:

public StepExitEnum doExecute(StepContext stepContext) throws Exception {
    initTempFilePath(stepContext);
    File[] encryptedFiles = findEncryptedFiles(stepContext);
    dencryptFiles(encryptedFiles, stepContext);
    return StepExitEnum.CONTINUING;
}

先不論具體實現細節,是不是一眼看過之後就瞭解doExecute做了什麼事?這個方法的確沒有任何註釋,是否影響閱讀?其實我做的只是把先前的程式碼重新歸類,分別放到了三個方法中,核心實現還是原本的程式碼,沒有改動,現在閱讀起來是不是順暢了許多?

通讀程式碼後我發現其實只做了三件事:建立目錄、讀取加密檔案、解密檔案,這是最核心的三個步驟,把它抽象出來,獨立為方法,既表達了邏輯功能,也清晰閱讀,還可以縮小影響範圍,今後哪裡有問題改哪裡,不需要再通讀程式碼了。

四、回到主題,再說可讀性

(1)抽象,合理的業務邏輯抽象

「一個方法只應該做一件事」,想必很多人聽過類似的表述,聽起來簡單做起來難,怎麼定義「只做一件事」?這件事的邊界是什麼?這就依賴coder對業務邏輯、對功能實現的深入理解和合理抽象,這才能清晰的區分出各個功能的邊界,或者說是如何定義這件「事」。

沒有基於業務的合理抽象,硬生生地寫了幾個方法,你會發現這幾個方法「藕斷絲連」,一個方法的引數變化總會影響到另一個方法,很難將一個方法單拎出來應用在其他場景,一處改,處處改,這時候就要考慮,方法抽象的是否合理?

合理的抽象,從功能角色、職責劃分上就很清晰,有了這個基礎,才能清晰的編寫業務邏輯程式碼,而不是堆砌各種條件判斷和迴圈,同時帶著兩條斜槓和註釋,這是可讀性的基礎。

(2)各司其職,職責單一

一個方法只做一件事,擴充套件到一個類也如此,職責單一,歸根結底還得基於合理的抽象,所以,它其實是抽象的一種具體體現,二者總是相輔相成。

(3)命名規範

這也是老生常談了,但真正做到的coder其實不多,類名、方法、變數的命名規則其實很有講究,但這不是本文的主題,不多贅述,類名用名詞,方法名用動詞,因為類表述的是做什麼事,而方法名錶述的是如何做,規範的命名和正確的詞法,這是編碼的基礎功底,這會有助於他人閱讀程式碼,當然也是為什麼我們讀spring原始碼會感覺順暢,而讀同事寫的業務程式碼卻很蹩腳的原因,我們太過於強調spring的IOC了,卻忽略了最基礎的東西。

(4)關鍵註釋

註釋不能少,但也不應該每個方法、每個判斷、每個迴圈到處都是///*,畢竟程式碼是主體不是註釋,而且這樣還會帶來隱性的工作量問題:程式碼修改,註釋也必須修改。所以好的註釋不是多,是關鍵。例如java.util.HashMap類的註釋上會告訴你執行緒安全問題:

Note that this implementation is not synchronized.

這是很關鍵的資訊,所以註釋要給出關鍵性的、使用上注意的事項,不在於多。

程式碼可讀性其實是一個比較寬泛的問題,也是一個老生常談的問題,隨著編碼經驗積累,在不同職業階段,我們對可讀性都會有不同的理解和認識,本文從我自己的角度和經驗,討論了一些比較淺的理解,如何寫出易讀、易懂的優秀程式碼,可能是我們coder永遠追尋的目標之一,即使它沒有終點。

作者:京東科技 張宇

來源:京東雲開發者社群 轉載請註明來源