正如食物腐爛之前,可能會發出異味。當程式碼存在隱藏問題時,程式碼也會表現出一些異狀,我們稱之為程式碼異味(code smell),它存在於整體結構和程式碼設計階段,暗示程式碼塊或通用的程式設計模式中可能存在更深層次的問題。
程式碼異味通常被認為是暗示程式碼段需要重構的標誌,但這並不是說程式碼有bug或者是無用的 。通常情況下,存在程式碼異味的程式碼塊也能夠執行的很好,但是一般難以維護和擴充套件,這就會導致一些技術問題,特別是在大型專案中,這種現象更加明顯。
那麼常見的程式碼異味有哪些呢?又要如何解決呢?下面我們將重點介紹10種最常見的程式碼異味以及如何對它們進行“除臭”。
1.緊耦合
存在的問題
緊耦合是指兩個物件相互依賴於彼此的資料或函式,如果修改其中一個物件,那麼另一個物件也需要修改。當兩個物件過於緊耦合時,修改程式碼可能會是一場噩夢,同時更有可能在每次修改時引入bug。
例如:
在這種情況下,Worker類和Bike類緊密耦合。如果有一天你想開車去上班而不是騎自行車?你必須進入Worker類,將所有與Bike類相關的程式碼替換為與Car類相關的程式碼。這就會變得很混亂,很容易出錯。
解決方法
你可以通過新增一個抽象層來降低耦合程度。在這種情況下,Worker類不僅可以騎自行車,還可以開車,還可以開卡車,甚至還可以騎摩托車。這些都屬於交通工具,不是嗎? 所以可以建立一個交通工具介面,根據你的需求,允許插入和修改不同型別的交通工具。
例如:
2.上帝物件
存在的問題
上帝物件是指包含太多變數和函式的大型類或模組。“知道得太多”和“做得太多”都會造成一些問題,原因有以下兩點。首先,其他類或模組會變得過分依賴於資料(緊密耦合)。其次,由於所有程式碼都擠在同一個地方,使得整體結構雜亂無章。
解決方案
取一個上帝物件,然後根據它存在的問題來分離它的資料和函式,再將這些分組轉換成物件。相較於上帝物件,分解為許多小物件可能會更好。
例如,假設你有一個巨大的User類:
您可以將其轉換為以下內容的組合:
這樣在下次需要修改登入過程時,就不必通過巨大的User類,而是通過易於管理的Credentials類!
3.長函式
存在的問題
顧名思義,長函式是指函式太長了。雖然沒有一個特定的數字表示多少行程式碼對於一個函式來說“太長”,但當你看到這個函式時,你就會知道它是不是太長。這幾乎是上帝物件問題的一個更嚴重的版本,一個長函式包含了太多的功能實現。
解決方案
長函式應該被分解成許多子函式,其中每個子函式被設計為處理單個任務或問題。理想情況下,原始的長函式將變成一個子函式呼叫列表,從而使程式碼更清晰,更易於閱讀。
4.引數過多
存在的問題
函式或類的建構函式擁有太多的引數會造成一些問題,原因有以下兩點。首先,這會使得程式碼不易閱讀,測試也更加困難。其次,更重要的是,這意味著該函式的功能太模糊,承擔著太多功能的實現。
解決方案
儘管“過多”對於引數列表而言是主觀的,但我們建議對任何超過3個引數的函式保持關注並儘量避免。當然,有時候一個函式有5個甚至6個引數也是允許的,前提是有合理的理由。
大多數情況下,不存在一種方法能更好地將該函式分解為兩個或更多不同的函式。與“長函式”不同的是,這個問題不能僅僅通過用子函式替換程式碼來解決 ,因為是函式本身需要分解為單獨的子函式,而每個子函式都需要包含各自的功能。
5.命名模糊的識別符號
存在的問題
一個或兩個字母的變數名、無明顯意義的函式名稱、過分修飾的類名、使用變數型別標記的變數名稱(例如,b_isCounted表示布林變數),最糟糕的是,在一個程式碼中混合使用不同的命名規則,所有這些都將導致程式碼難以閱讀,難以理解和難以維護。
解決方案
為變數,函式和類命名是一個難學的技能。如果你正在參與一個已有的專案,請仔細觀察現有的識別符號命名方式。如果存在命名風格指南,那麼請記住它並時刻遵守它。如果是新專案,就可以考慮形成自己的命名風格並且堅持下去。
一般而言,變數名稱應該簡短但具有描述性。函式名通常應該至少有一個動詞,並且函式名稱應該表現出該函式的功能,但是不要使用太多的單詞,類名也是如此。
6.幻數
存在的問題
當你正在瀏覽一些其他人寫的程式碼,這時你發現了一些硬編碼的數字。它們也許是if語句的一部分,或者是一些難以理解的計算的一部分,看起來沒什麼意義,而你需要修改該模組,但卻無法理解這些數字的含義,這會使你非常苦惱。
解決方案
在程式設計時,應該不惜一切代價避免這些所謂的“幻數”。硬編碼數字在寫的時侯是有意義的,但是它們很快就會失去所有含義 ,特別是當其他人試圖維護你的程式碼時。
其中一種解決方法是留下數字的註釋,但更好的選擇是將幻數轉換為常量變數(用於計算)或列舉(用於if語句和switch語句)。通過給幻數起一個名字,程式碼可讀性一目瞭然,同時也不太容易出現錯誤。
7.深度巢狀
存在的問題
有兩種主要的語句可能造成深度巢狀程式碼:迴圈和條件語句。深度巢狀的程式碼並不總是很糟糕,但可能會產生問題,因為它很難理解(特別是變數沒有被很好地命名的情況下),甚至更加難以修改。
解決方案
如果你發現自己正在編寫一個雙重,三重甚至四重for迴圈,那麼程式碼將可能試圖在超出自身的範圍外查詢資料。所以你應該提供一種方法,使之可以通過包含該資料的物件或模組函式呼叫來請求資料。
另一方面,深層巢狀的if語句通常表明你試圖在單個函式或類中處理過多的邏輯程式碼塊。事實上,深層巢狀和長函式往往是同時出現的。如果你的程式碼有大量的switch語句或巢狀的if-then-else語句,你可能需要實現一個狀態機或策略模式。
8.未處理異常
存在的問題
異常的功能是非常強大的,但卻容易被濫用。不正確地使用throw-catch語句可能會導致除錯難度大幅增長。例如,忽略或掩蓋捕獲的異常。
解決方案
不要忽略或掩蓋捕獲的異常,而是要列印出異常的及其呼叫資訊,這樣除錯人員才可以發現錯誤。如果你的程式悄無聲息地執行失敗,那麼將來你可能就要頭痛不已了!此外,我更傾向於輸出特殊的異常資訊而非所有異常。
9.重複的程式碼
存在的問題
你在程式多個無關部分執行相同的邏輯程式碼塊,然後發現需要修改該邏輯程式碼塊,但是卻不記得所有執行該程式碼塊的地方,假設最終你只修改了5個位置,而實際上有8個位置的程式碼塊需要進行更改,這就會導致結果出現錯誤。
解決方案
解決重複程式碼問題的首要選擇是轉化為函式。假設你正在開發一個聊天應用程式,你是這樣編寫的:
在程式碼中的其他地方,你發現你需要執行一個相同的“這個使用者線上嗎?”檢查。這時不要複製貼上程式碼塊,而是把它放到一個函式中:
這樣在程式碼的任何地方,你都可以使用isUserOnline()函式進行檢查。如果你需要修改此邏輯程式碼塊,就只需要修改該方法,再將其應用於所有呼叫該方法的地方就可以了。
10.缺乏註釋
存在的問題
程式碼在任何地方都沒有註釋。沒有函式的功能註釋,沒有類的使用概述,沒有對演算法的解釋等等。有人可能會說,寫得好的程式碼不需要註釋,但事實上,即使是寫的最好的程式碼也不如註釋更容易被理解。
解決方案
易於維護的程式碼塊應該是程式碼寫得足夠好以至於不需要註釋,但它仍然有註釋。在寫註釋的時候,要記住你的目的是為解釋程式碼塊為什麼存在,而不是解釋程式碼塊在做什麼。註釋能幫助你更好的理解自己和他人的程式碼,減少工作量,所以不要忽視他們。
如何編寫風格良好的程式碼
顯而易見,大多數不規範的程式碼都是由於對良好的程式設計原則和程式碼風格的忽視。假如你能夠嚴格遵守DRY原則(Don't repeat yourself)就能消除大部分的重複程式碼,而掌握單一職責原則就可以避免創造巨大的上帝物件。
千萬不要輕視這個問題,如果連你都無法一目瞭然地看懂自己的程式碼,更何況其他人呢?