當前位置:
首頁 > 科技 > 誰都忍不了爛代碼,如何用重構的方式讓它整潔起來?

誰都忍不了爛代碼,如何用重構的方式讓它整潔起來?

作者|南志文

編輯|小智

「整潔的代碼簡單直接。整潔的代碼如同優美的散文。整潔的代碼從不隱藏設計者的意圖,充滿了乾淨利落的抽象和直截了當的控制語句。」怎樣讓代碼更加整潔?答案是重構!

寫在前面

現在的軟體系統開發難度主要在於其複雜度和規模,客戶需求也不再像 Winston Royce 瀑布模型期望那樣在系統編碼前完成所有的設計滿足用戶軟體需求。在這個信息爆炸技術日新月異的時代,需求總是在不斷的變化,隨之在 2001 年業界 17 位大牛聚集在美國猶他州的滑雪勝地雪鳥(Snowbird)雪場,提出了「Agile」(敏捷)軟體開發價值觀,並在他們的努力推動下,開始在業界流行起來。

在《代碼整潔之道》一書中提出:一種軟體質量,可持續開發不僅在於項目架構設計,還與代碼質量密切相關,代碼的整潔度和質量成正比,一份整潔的代碼在質量上是可靠的,為團隊開發,後期維護,重構奠定了良好的基礎。

接下來筆者將結合自己之前的重構實踐經驗,來探討平時實際開發過程中我們注重代碼優化實踐細節之道,而不是站在純空洞的理論來談論代碼整潔之道。

在具體探討如何進行代碼優化之前,我們首先需要去探討和明確下何謂是「代碼的壞味道」,何謂是「整潔優秀代碼」。因為一切優化的根源都是來自於我們平時開發過程中而且是開發人員自己產生的「代碼壞味道」。

代碼的壞味道

「如果尿布臭了,就換掉它。」-語出 Beck 奶奶,論撫養小孩的哲學。同樣,代碼如果有壞味道了,那麼我們就需要去重構它使其成為優秀的整潔代碼。

談論到何謂代碼的壞味道,重複代碼(Duplicated Code)首當其衝。重複在軟體系統是萬惡的, 我們熟悉的分離關注點,面向對象設計原則等都是為了減少重複提高重用,Don』t repeat yourself(DRY)。關於 DRY 原則,我們在平時開發過程中必須要嚴格遵守。

其次還有其他壞味道:過長函數 (Long Method)、過大的類 (Large Class)、過長參數列表 (Long Parameter List)、冗餘類(Lazy Class)、冗餘函數(Lazy Function)無用函數參數(Unused Function Parameter)、函數圈複雜度超過 10(The Complexity is over 10)、依戀情結(Feature Envy)、Switch 過多使用(Switch Abuse)、過度擴展設計(Over-extend design)、不可讀或者可讀性差的變數名和函數名 (unread variable or function name)、異曲同工類(Alternative Classes with Different Interfaces)、過度耦合的消息鏈(Message Chains)、令人迷惑的臨時欄位(Temporary Field)、過多注釋 (Too Many Comments) 等壞味道。

整潔代碼

什麼是整潔代碼?不同的人會站在不同的角度闡述不同的說法。而我最喜歡的是 Grady Booch(《面向對象分析與設計》作者)闡述:

「整潔的代碼簡單直接。整潔的代碼如同優美的散文。整潔的代碼從不隱藏設計者的意圖,充滿了乾淨利落的抽象和直截了當的控制語句。」

整潔的代碼就是一種簡約(簡單而不過於太簡單)的設計,閱讀代碼的人能很清晰的明白這裡在幹什麼,而不是隱澀難懂,整潔的代碼讀起來讓人感覺到就像閱讀散文 - 藝術的沉澱,作者是精心在意締造出來。

整潔代碼是相對於代碼壞味道的,如何將壞味道代碼優化成整潔代碼,正是筆者本文所探討的重點內容:整潔代碼之道-重構,接下來筆者將從幾個角度重點描述如何對軟體進行有效有技巧的重構。

重構 — Why

在軟體開發過程中往往開發者不經意間就能產生代碼的壞味道,特別是團隊人員水平參差不齊每個人的經驗和技術能力不同的情況下更容易產生不同階段的代碼壞味道。並且隨著需求的迭代和時間推移,代碼的壞味道越來越嚴重,甚至影響到團隊的開發效率,那麼遇到這個問題該如何去解決。

在軟體開發 Coding 之前我們不可能事先了解所有的需求,軟體設計肯定會有考慮不周到不全面的地方,而且隨著項目需求的 Change,很有可能原來的代碼設計結構已經不能滿足當前需求。

更何況,我們很少有機會從頭到尾參與並且最終完成一個項目,基本上都是接手別人的代碼,即使這個項目是從頭參與的,也有可能接手團隊其他成員的代碼。我們都有過這樣的類似的抱怨經歷,看到別人的代碼時感覺就像垃圾一樣特別差勁,有一種強烈的完全想重寫的衝動,但一定要壓制住這種衝動,你完全重寫,可能比原來的好一點,但浪費時間不說,還有可能引入原來不存在的 Bug,而且,你不一定比原來設計得好,也許原來的設計考慮到了一些你沒考慮到的分支或者異常情況。

我們寫的代碼,終有一天也會被別人接手,很可能到時別人會有和我們現在一樣的衝動,所以開發者在看別人代碼時候,要懷著一顆學習和敬畏之心,去發現別人的代碼之美,在這個過程中挑出寫的比較好的優秀代碼,吸取精華,去其糟粕,在這個基礎上,我們再去談重構,那麼你的重構會是一個好的開端。

總之,我們要做的是重構不是重寫,要先從小範圍的局部重構開始,然後逐步擴展到整個模塊。

重構 — 作用

重構,絕對是軟體開發寫程序過程中最重要的事之一。那麼什麼是重構,如何解釋重構。名詞:對軟體內部結構的一種調整,目的是在不改變軟體可觀察行為的前提下,提高其可理解性,降低其修改成本。動詞:使用一系列重構手法,在不改變軟體可觀察行為的前提下,調整其結構。

重構不只可以改善既有的設計結構,還可以幫助我們理解原來很難理解的流程。比如一個複雜的條件表達式,我們可能需要很久才能看明白這個表達式的作用,還可能看了好久終於看明白了,過了沒多長時間又忘了,現在還要從頭看,如果我們把這個表達式運用 Extract Method 抽象出來,並起一個易於理解的名字,如果函數名字起得好,下次當我們再看到這段代碼時,不用看邏輯我們就知道這個函數是做什麼的。

如果對這個函數內所有難於理解的地方我們做了適當的重構,把每個細小的邏輯抽象成一個小函數並起一個容易理解的名字,當我們看代碼時就有可能像看注釋一樣,不用再像以前一樣通過看代碼的實現來猜測這段代碼到底是做什麼的,我一直堅持和秉持這個觀點:好的代碼勝過注釋,畢竟注釋還是有可能更新不及時的,不及時最新的注釋容易更其他人帶來更多的理解上的困惑。

此外重構可以使我們增加對代碼和業務邏輯功能的理解,從而幫助我們找到 Bug;重構可以幫助我們提高編程速度,即重構改善了程序結構設計,並且因為重構的可擴展性使添加新功能變得更快更容易。

重構 — 時機

理解了重構的意義和作用,那麼我們何時開始重構呢?筆者一直堅持這種觀點:重構是一個持續的系統性的工程,它是貫穿於整個軟體開發過程中,我們無需專門的挑出時間進行重構,重構應該隨時隨地的進行,即遵循三次法則:事不過三,三則重構。這個準則表達的意思是:第一次去實現一個功能儘管去做,但是第二次做類似的功能設計時會產生反感,但是還是會去做,第三次還是實現類似的功能做同樣的事情,那你就應該去重構。三次準則比較抽象,那麼對應到我們具體的軟體開發流程中,一般可以在這三個時機去進行:

(1) 當添加新功能時如果不是特別容易,可以通過重構使添加特性和新功能變得更容易。在添加新功能的時候,我們就先清理這個功能所需要的代碼。花一點時間,用滴水穿石的方法逐漸清理代碼,隨著時間的推移,我們的代碼就會越來越乾淨,開發速度也會越來越快。

(2) 修改 Bug 的時候去重構,比如你在查找定位 Bug 的過程中,發現以前自己的代碼或者別人的代碼因為設計缺陷比如可擴展性、健壯性比較差造成的,那麼此時就是一個比較好的重構時機。可能這個時候很多同學就有疑問了,認為我開發要趕進度,沒有時間去重構,或者認為我打過補丁把 Bug 解決不就行了,不需要去重構。根據筆者之前多年的經驗得出的結論:遇到即要解決即那就是每遇到一個問題,就馬上解決它,而不是選擇繞過它。完善當前正在使用的代碼,那些還沒有遇到的問題,就先不要理它。在當前前進的道路上,清除所有障礙,以後你肯定還會再一次走這條路,下次來到這裡的時候你會發現路上不再有障礙。

軟體開發就是這樣。或許解決這個問題需要你多花一點時間。但是從長遠來看,它會幫你節省下更多的時間。也就是重構是個循序漸進的過程,經過一段時間之後,你會發現之前所有的技術債務會逐步都不見了,所有的坑相繼都被填平了。這種循序漸進的代碼重構的好處開始顯現,編程的速度明顯會加快。

(3)Code Review 時去重構,很多公司研發團隊都會有定期的 Code Review, 這種活動的好處多多,比如有助於在開發團隊中傳播知識進行技術分享,有助於讓較有經驗的開發者把知識傳遞給欠缺經驗的人,並幫助更多的人對軟體的其他業務模塊更加熟悉從而實現跨模塊的迭代開發。Code Review 可以讓更多的人有機會對自己提出更多優秀好的建議。同時重構可以幫助審查別人的代碼,因為在重構前,你需要先閱讀代碼得到一定程度的理解和熟悉,從而提出一些建議和好的 idea, 並考慮是否可以通過重構快速實現自己的好想法,最終通過重構實踐你會得到更多的成就感滿足感。為了使審查代碼的工作變得高效有作用,據我以前的經驗,我建議一個審查者和一個原作者進行合作,審查者提出修改建議,然後兩人共同判斷這些修改是否能夠通過重構輕鬆實現,如果修改成本比較低,就在 Review 的過程中一起著手修改。

如果是比較大型比較複雜的設計複查審核工作,建議原作者使用 UML 類序列圖、時間序列圖、流程圖去向審查者展現設計的具體實現細節,在整個 Code Review 中,審查者可以提出自己的建議或者修改意見。在這種情景下,審查者一般由團隊裡面比較資深的工程師、架構師、技術專家等成員組成。

關於 Code Review 的形式,還可以採取極限編程中的「結對編程」形式。這種形式可以採取兩個人位置坐在一起去審查代碼,可以採取兩個平台比如 IOS 和 android 的開發人員一起去審查,或者經驗資深的和經驗不資深的人員一起搭配去審查。

重構的這三個時機要把握好原則,即什麼時候不應該重構,比如有時候既有代碼實現太混亂啦,重構它還不如重新寫一個來得簡;此外,如果你的項目已經進入了尾期,此時也應該避免重構,這時機應該儘可能以保持軟體的穩定性為主。

理解了重構是做什麼,重構的作用,為什麼要重構,以及重構的時機,我們對重構有了初步認識,接下來筆者重點篇幅來講解如何使用重構技巧去優化代碼質量達成 Clean Code .

重構技巧 — 函數重構

重構的源頭一切從重構函數開始,掌握函數重構技巧是重構過程中很關鍵的一步,接下來我們來探討下函數重構有那些實用技巧。

重命名函數(Rename Function Name) : Clean Code 要求定義的變數和函數名可讀性要強,從名字就可以知道這個變數和函數去做什麼事情,所以好的可讀性強的函數名稱很重要,特別是有助於理解比較複雜的業務邏輯。

移除參數(Remove Parameter): 當函數不再需要某個參數時,要果斷移除,不要為了某個未知需求預留參數,過多的參數會給使用者帶來參數困擾。

將查詢函數和修改函數分離:如果某個函數既返回對象值,又修改對象狀態。這時候應該建立兩個不同的函數,其中一個負責查詢,另一個負責修改。如果查詢函數只是簡單的返回一個值而沒有副作用,就可以無限次的調用查詢函數。對於複雜的計算也可以緩存結果。

令函數攜帶參數:如果若干函數做了類似的工作,只是少數幾個值不同導致行為略有不同,合并這些函數,以參數來表達不同的值。

以明確函數取代參數:有一個函數其中的邏輯完全取決於參數值而採取不同行為,針對該參數的每一個可能值建立一個單獨的函數。

保持對象完整性:如果你需要從某個對象取若干值,作為函數的多個參數傳進去,特別是需要傳入較多參數比如 5 個參數或者更多參數時,這種情況建議直接將這個對象直接傳入作為函數參數,這樣既可以減少參數的個數,增加了對象間的信賴性,而且這樣被調用者需要這個對象的其他屬性時可以不用人為的再去修改函數參數。

以函數取代參數:對象調用某個函數,並將所得結果作為參數傳遞給另外一個函數,而那個函數本身也能夠調用前一個函數,直接讓那個函數調用就行,可以直接去除那個參數,從而減少參數個數。

引入參數對象:某些參數總是同時出現,新建一個對象取代這些參數,不但可以減少參數個數,而且也許還有一些參數可以遷移到新建的參數類中,增加類的參數擴展性。

移除設值函數(Setting Method):如果類中的某個欄位應該在對象創建時賦值,此後就不再改變,這種情景下就不需要添加 Setting method。

隱藏函數:如果有一個函數從來沒有被其他類有用到,或者是本來被用到,但隨著類動態添加介面或者需求變更,之後就使用不到了,那麼需要隱藏這個函數,也就是減小作用域。

以工廠函數取代構造函數:如果你希望創建對象時候不僅僅做簡單的構建動作,最顯而易見的動機就是派生子類時根據類型碼創建不同的子類,或者控制類的實例個數。

重構技巧 — 條件表達式

分解條件表達式:如果有一個複雜的條件語句,if/else 語句的段落邏輯提取成一個函數。

合并條件表達式:一系列條件測試,都得到相同的測試結果,可以將這些測試表達式合并成成一個,並將合并後的表達式提煉成一個獨立函數,如果這些條件測試是相互獨立不相關的,就不要合并。

合并重複的條件片段:在條件表達式的每個分支上有著相同的一段代碼,把這段代碼遷移到表達式之外。

移除控制標記:不必遵循單一出口的原則,不用通過控制標記來決定是否退出循環或者跳過函數剩下的操作,直接 break 或者 return。

以衛語句替代嵌套條件表達式:條件表達式通常有兩種表現形式,一:所有分支都屬於正常行為;二:只有一種是正常行為,其他都是不常見的情況。對於一的情況,應該使用 if/else 條件表達式;對於二這種情況,如果某個條件不常見,應該單獨檢查條件並在該條件為真時立即從函數返回,這樣的單獨檢查常常被稱為衛語句。

以多態取代條件表達式:如果有個條件表達式根據對象類型的不同選擇而選擇不同的行為,將條件表達式的每個分支放進一個子類內的覆寫函數中,將原始函數聲明為抽象函數。

引入 Null 對象:當執行一些操作時,需要再三檢查某對象是否為 NULL,可以專門新建一個 NULL 對象,讓相應函數執行原來檢查條件為 NULL 時要執行的動作,除 NULL 對象外,對特殊情況還可以有 Special 對象,這類對象一般是 Singleton.

引入斷言:程序狀態的一種假設

以 MAP 取代條件表達式:通過 HashMap 的 Key-Value 鍵值對優化條件表達式,條件表達式的判斷條件作為 key 值,value 值存儲條件表達式的返回值。

通過反射取代條件表達式:通過動態反射原理

重構技巧 — 案例

前面這多章節內容主要都是理論內容,接下來我們來看看具體的重構案例。

Map 去除 if 條件表達式

關於該技巧的實現方法,上章節有講述,我們直接看代碼案例如下代碼所示:

原始的條件表達式代碼如下圖 1 所示:

重構後的代碼如下所示:

上述代碼是直接通過 Map 結構,將條件表達式分解, Key 是條件變數,Value 是條件表達式返回值。取值很方便,顯然高效率 O(1)時間複雜度取值。這種重構技巧適合於比較簡單的條件表達式場景,下面是比較複雜的沒有返回值的條件表達式場景,我們去看看如何處理。

反射去除分支結構

原始的條件表達式代碼如下圖 1 所示:

圖 1 條件表達式示範

圖 2 通過 Map 和反射重構示範

如上圖 2 所示,通過 Map 和反射去分解條件表達式,將條件表達式分支的邏輯抽取到子類中的覆寫函數中,提取了共同的抽象類,裡面包含抽象介面 handleBusinessData, 子類繼承實現它。

多態取代條件表達式

圖 3 重構後的案例結果圖

圖 4 重構後的案例-多態如何使用

圖 5 重構後的代碼結構圖

圖 6 重構-抽象類、簡單工廠模式思想去實現條件表達式的分解

如上圖 6 所示,在原始的條件表達式中,有兩個條件表達式分支(分支邏輯):

中文入住人操作 HotelCNPasserngerOperaton 類

英文入住人操作 HotelEnPassengerOperation 類

共同抽取了基類抽象類:AbstractPassengerOperation, 其兩個分支子類去繼承抽象類。

為了分解條件表達式,筆者採取了多態的重構技巧去實現,具體有兩種實現方式,第一種實現方式是採用抽象類去實現多態,代碼結構圖如圖 5 passenger 文件夾,UML 類圖如上圖 6 所示。第二種實現方式是採用介面去實現多態,代碼結構如圖 5 passenger2 文件夾,UML 類圖如上圖 7 所示。

圖 7 重構-介面狀態者模式思想去實現條件表達式的分解

如上圖 7 所示,在原始的條件表達式中,有兩個條件表達式分支(分支邏輯),其分支邏輯分別放在了子類 HotelCNPassengerState 和 HotelENPassengerState 中,統一提取了介面類 PassengerState 類,裡面包含子類都需要實現的兩個基礎介面。從圖 7,可以看出,是使用了狀態者模式。

經過了上述重構之後,我們達成了什麼效果:

邏輯清晰

主邏輯代碼行數減少

業務邏輯,更好的封裝解藕,無需關注具體的業務細節

採用了多態、抽象、狀態模式、工廠模式、Build 模式的等不同的思想和方法,很多不同的重構技巧去重構一個功能,值得推廣和借簽;

寫在最後

重構是一門比較大而深的話題和課題,筆者這次主要探討了如何通過有效的重構技巧去寫成優秀的整潔代碼,代碼整潔之道就是要將重構始終貫穿在整個開發過程中,不斷的持續的漸進重構,從而將以前的技術債全部還完。

重構是個技術活,需要很資深的人士去整體架構把控技術方案和產品質量,才能使重構做的更加有效並且不會引入新的問題,但是無論我們最終採取什麼手段去重構,最終我們都需要盡量符合 Solid 設計相關原則:

類的單一職責:體現了類只應該做一件事,良好的軟體設計中系統是由一組大量的短小的類組成,以及需要他們之間功能協作完成,而不是幾個上帝類。如果類的職責超過一個,這些職責之間就會產生耦合。改變一個職責,可能會影響和妨礙類為其他人服務的功能。這種類型的耦合將會導致脆弱的設計,在修改的時候可能會引入不少未知的問題。

開閉原則:其定義是說一個軟體實體如類,模塊和函數應該對擴展開放,而對修改關閉,具體來說就是你應該通過擴展來實現變化,而不是通過修改原有的代碼來實現變化,該原則是面相對象設計最基本的原則。其指導思想就是(1)抽象出相對穩定的介面,這部分應該不動或者很少改動;(2)封裝變化;不過在軟體開發過程中,要一開始就完全按照開閉原則來可能比較困難,更多的情況是在不斷的迭代重構過程中去改進,在可預見的變化範圍內去做設計。

里氏替換原則:子類可以擴展父類的功能,但不能改變父類原有的功能。簡單來說,所有使用基類代碼的地方,如果換成子類對象的時候還能夠正常運行,則滿足這個原則,否則就是繼承關係有問題,應該廢除兩者的繼承關係,這個原則可以用來判斷我們的對象繼承關係是否合理。通常在設計的時候,我們都會優先採用組合而不是繼承,因為繼承雖然減少了代碼,提高了代碼的重用性,但是父類跟子類會有很強的耦合性,破壞了封裝。

介面隔離原則:不能強迫用戶去依賴那些他們不使用的介面。簡單來說就是客戶端需要什麼介面,就提供給它什麼樣的介面,其它多餘的介面就不要提供,不要讓介面變得臃腫,否則當對象一個沒有使用的方法被改變了,這個對象也將會受到影響。介面的設計應該遵循最小介面原則,其實這也是高內聚的一種表現,換句話說,使用多個功能單一、高內聚的介面總比使用一個龐大的介面要好。

依賴倒置(DIP):高層模塊不應該依賴低層模塊,兩者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。其實這就是我們經常說的「針對介面編程」,這裡的介面就是抽象,我們應該依賴介面,而不是依賴具體的實現來編程。DIP 描述組件之間高層組件不應該依賴於底層組件。依賴倒置是指實現和介面倒置,採用自頂向下的方式關注所需的底層組件介面,而不是其實現。DI 模式很好例子的就是應用 IOC(控制反轉)框架,構造方式分為分構造注入,函數注入,屬性注入 。

當我們在做重構優化的時候應該充分考慮上面這幾個原則,一開始可能設計並不完美,不過可以在重構的過程中不斷完善。但其實很多人都跳過了設計這個環節,拿到一個模塊直接動手編寫代碼,更不用說去思考設計了,項目中也有很多這樣的例子。當然對於簡單的模塊或許不用什麼設計,不過假如模塊相對複雜的話,能夠在動手寫代碼之前好好設計思考一下,養成這個習慣,肯定會對編寫出可讀性、穩定性、健壯性、靈活性、可服用性、可擴展性較高的代碼有幫助。

今日薦文

點擊展開全文

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 InfoQ 的精彩文章:

ArchSummit演講+晚場免費報名通道:金融運維和AI技術大派送
從容器到運維,一篇文章看懂技術的變革與未來
Java 老矣,尚能飯否?
前端工程師如何提高職場核心競爭力
今晚別睡!聽Java、Docker、DevOps大神講開源

TAG:InfoQ |

您可能感興趣

「對不起,我沒有你們想看的代碼!」
如果不寫代碼,還可以做什麼
這代碼誰寫的,太可怕了!
程序員,除了代碼你還需要懂得這些!
寧可不賣也不能給的戰鬥機源代碼有多重要?
代碼重構!你敢嗎?
唐藝昕這裙子真絕了,竟然寫滿了「代碼」,程序員看了要氣瘋!
代碼都一樣,但是速度比之前慢了一半,是不是硬體出現問題,請求上門!
寫代碼寫到崩潰的時候,我經常這樣鼓勵自己
都說程序猿的眼裡只有代碼,現在我發現我錯了,原來代碼里真的有女人!
華為手機的這幾個功能你知道嗎?必須用隱藏代碼才能進入
不了解「專業代碼」後果很嚴重!有可能志願白填
我不會編程,但「自主可控」的紅芯瀏覽器被我用一行代碼搞定
你們這些程序員,真得每天都在讀代碼嗎?
學會讀懂電腦這些錯誤代碼,再也不用擔心問題出現
這麼糟糕的代碼,真的是我以前寫的嗎?
趣圖:這代碼誰寫的,太可怕了!
李笑來:弱智代碼也可以永存,但不能因為永存就智能了
代碼從不撒謊,但注釋有時候卻會……
面試官問:請拿出一段體現你水平的代碼,我該如何回答?