如何寫出讓同事膜拜的漂亮代碼?

NO IMAGE

“代碼千萬行,註釋第一行;編程不規範,同事兩行淚”;”道路千萬條,安全第一條。代碼不規範,親人兩行淚。”在技術圈廣為盛傳,可見代碼不規範讓程序員們是多麼的頭痛。

近期全棧開發者 Nitin Sharma 分享了他理解的編程七宗罪:

  • 協作時不使用版本控制
  • 不使用合適的變量命名
  • 使用過多的依賴,不經思考直接升級
  • 不自解釋的代碼
  • 格式不一致
  • 不處理錯誤
  • 使用不當的數據類型/數據結構

你是否產生了共鳴?這些錯誤問題,只要肯努力都可以避免發生。

如何讓你的代碼整潔而優雅?今天小編介紹的這本書教你在不改變外部環境的情況下,有條不紊的改善代碼,也許你有所耳聞,他就是《重構:改善既有代碼的設計(第2版)(精裝版)》。全網已發售。首先了解一下重構的原則。

如何寫出讓同事膜拜的漂亮代碼?

何謂重構

一線的實踐者們經常很隨意地使用“重構”這個詞—軟件開發領域的很多詞彙都有此待遇。我使用這個詞的方式比較嚴謹,並且我發現這種嚴謹的方式很有好處。“重構”這個詞既可以用作名詞也可以用作動詞。名詞形式的定義是:

重構(名詞):對軟件內部結構的一種調整,目的是在不改變軟件可觀察行為的前提下,提高其可理解性,降低其修改成本。

這個定義適用於我在前面的例子中提到的那些有名字的重構,例如提煉函數(106)以多態取代條件表達式(272)

動詞形式的定義是:

重構(動詞):使用一系列重構手法,在不改變軟件可觀察行為的前提下,調整其結構。

所以,我可能會花一兩個小時進行重構(動詞),其間我會使用幾十個不同的重構(名詞)。

過去十幾年,這個行業裡的很多人用“重構”這個詞來指代任何形式的代碼清理,但上面的定義所指的是一種特定的清理代碼的方式。重構的關鍵在於運用大量微小且保持軟件行為的步驟,一步步達成大規模的修改。每個單獨的重構要麼很小,要麼由若干小步驟組合而成。因此,在重構的過程中,我的代碼很少進入不可工作的狀態,即便重構沒有完成,我也可以在任何時刻停下來。

如果有人說他們的代碼在重構過程中有一兩天時間不可用,基本上可以確定,他們在做的事不是重構。

我會用“結構調整”(restructuring)來泛指對代碼庫進行的各種形式的重新組織或清理,重構則是特定的一類結構調整。剛接觸重構的人看我用很多小步驟完成似乎可以一大步就能做完的事,可能會覺得這樣很低效。但小步前進能讓我走得更快,因為這些小步驟能完美地彼此組合,而且—更關鍵的是—整個過程中我不會花任何時間來調試。

在上述定義中,我用了“可觀察行為”的說法。它的意思是,整體而言,經過重構之後的代碼所做的事應該與重構之前大致一樣。這個說法並非完全嚴格,並且我是故意保留這點兒空間的:重構之後的代碼不一定與重構前行為完全一致。比如說,提煉函數(106)會改變函數調用棧,因此程序的性能就會有所改變;改變函數聲明(124)搬移函數(198)等重構經常會改變模塊的接口。不過就用戶應該關心的行為而言,不應該有任何改變。如果我在重構過程中發現了任何bug,重構完成後同樣的bug應該仍然存在(不過,如果潛在的bug還沒有被任何人發現,也可以當即把它改掉)。

重構與性能優化有很多相似之處:兩者都需要修改代碼,並且兩者都不會改變程序的整體功能。兩者的差別在於其目的:重構是為了讓代碼“更容易理解,更易於修改”。這可能使程序運行得更快,也可能使程序運行得更慢。在性能優化時,我只關心讓程序運行得更快,最終得到的代碼有可能更難理解和維護,對此我有心理準備。

兩頂帽子

Kent Beck提出了“兩頂帽子”的比喻。使用重構技術開發軟件時,我把自己的時間分配給兩種截然不同的行為:添加新功能和重構。添加新功能時,我不應該修改既有代碼,只管添加新功能。通過添加測試並讓測試正常運行,我可以衡量自己的工作進度。重構時我就不能再添加功能,只管調整代碼的結構。此時我不應該添加任何測試(除非發現有先前遺漏的東西),只在絕對必要(用以處理接口變化)時才修改測試。

軟件開發過程中,我可能會發現自己經常變換帽子。首先我會嘗試添加新功能,然後會意識到:如果把程序結構改一下,功能的添加會容易得多。於是我換一頂帽子,做一會兒重構工作。程序結構調整好後,我又換上原先的帽子,繼續添加新功能。新功能正常工作後,我又發現自己的編碼造成程序難以理解,於是又換上重構帽子……整個過程或許只花10分鐘,但無論何時我都清楚自己戴的是哪一頂帽子,並且明白不同的帽子對編程狀態提出的不同要求。

為何重構

我不想把重構說成是包治百病的萬靈丹,它絕對不是所謂的“銀彈”。不過它的確很有價值,儘管它不是一顆“銀彈”,卻可以算是一把“銀鉗子”,可以幫你始終良好地控制自己的代碼。重構是一個工具,它可以(並且應該)用於以下幾個目的。

重構改進軟件的設計

如果沒有重構,程序的內部設計(或者叫架構)會逐漸腐敗變質。當人們只為短期目的而修改代碼時,他們經常沒有完全理解架構的整體設計,於是代碼逐漸失去了自己的結構。程序員越來越難通過閱讀源碼來理解原來的設計。代碼結構的流失有累積效應。越難看出代碼所代表的設計意圖,就越難保護其設計,於是設計就腐敗得越快。經常性的重構有助於代碼維持自己該有的形態。

完成同樣一件事,設計欠佳的程序往往需要更多代碼,這常常是因為代碼在不同的地方使用完全相同的語句做同樣的事,因此改進設計的一個重要方向就是消除重複代碼。代碼量減少並不會使系統運行更快,因為這對程序的資源佔用幾乎沒有任何明顯影響。然而代碼量減少將使未來可能的程序修改動作容易得多。代碼越多,做正確的修改就越困難,因為有更多代碼需要理解。我在這裡做了點兒修改,系統卻不如預期那樣工作,因為我沒有修改另一處—那裡的代碼做著幾乎完全一樣的事情,只是所處環境略有不同。消除重複代碼,我就可以確定所有事物和行為在代碼中只表述一次,這正是優秀設計的根本。

重構使軟件更容易理解

所謂程序設計,很大程度上就是與計算機對話:我編寫代碼告訴計算機做什麼事,而它的響應是按照我的指示精確行動。一言以蔽之,我所做的就是填補“我想要它做什麼”和“我告訴它做什麼”之間的縫隙。編程的核心就在於“準確說出我想要的”。然而別忘了,除了計算機外,源碼還有其他讀者:幾個月之後可能會有另一位程序員嘗試讀懂我的代碼並對其做一些修改。我們很容易忘記這這位讀者,但他才是最重要的。計算機是否多花了幾個時鐘週期來編譯,又有什麼關係呢?如果一個程序員花費一週時間來修改某段代碼,那才要命呢—如果他理解了我的代碼,這個修改原本只需一小時。

問題在於,當我努力讓程序運轉的時候,我不會想到未來出現的那個開發者。是的,我們應該改變一下開發節奏,讓代碼變得更易於理解。重構可以幫我讓代碼更易讀。開始進行重構前,代碼可以正常運行,但結構不夠理想。在重構上花一點點時間,就可以讓代碼更好地表達自己的意圖—更清晰地說出我想要做的。

關於這一點,我沒必要表現得多麼無私。很多時候那個未來的開發者就是我自己。此時重構就顯得尤其重要了。我是一個很懶惰的程序員,我的懶惰表現形式之一就是:總是記不住自己寫過的代碼。事實上,對於任何能夠立刻查閱的東西,我都故意不去記它,因為我怕把自己的腦袋塞爆。我總是儘量把該記住的東西寫進代碼裡,這樣我就不必記住它了。這麼一來,下班後我還可以喝上兩杯Maudite啤酒,不必太擔心它殺光我的腦細胞。

重構幫助找到bug

對代碼的理解,可以幫我找到bug。我承認我不太擅長找bug。有些人只要盯著一大段代碼就可以找出裡面的bug,我不行。但我發現,如果對代碼進行重構,我就可以深入理解代碼的所作所為,並立即把新的理解反映在代碼當中。搞清楚程序結構的同時,我也驗證了自己所做的一些假設,於是想不把bug揪出來都難。

這讓我想起了Kent Beck經常形容自己的一句話:“我不是一個特別好的程序員,我只是一個有著一些特別好的習慣的還不錯的程序員。”重構能夠幫助我更有效地寫出健壯的代碼。

重構提高編程速度

最後,前面的一切都歸結到了這一點:重構幫我更快速地開發程序。

聽起來有點兒違反直覺。當我談到重構時,人們很容易看出它能夠提高質量。改善設計、提升可讀性、減少bug,這些都能提高質量。但花在重構上的時間,難道不是在降低開發速度嗎?

當我跟那些在一個系統上工作較長時間的軟件開發者交談時,經常會聽到這樣的故事:一開始他們進展很快,但如今想要添加一個新功能需要的時間就要長得多。他們需要花越來越多的時間去考慮如何把新功能塞進現有的代碼庫,不斷蹦出來的bug修復起來也越來越慢。代碼庫看起來就像補丁摞補丁,需要細緻的考古工作才能弄明白整個系統是如何工作的。這份負擔不斷拖慢新增功能的速度,到最後程序員恨不得從頭開始重寫整個系統。

下面這幅圖可以描繪他們經歷的困境。

如何寫出讓同事膜拜的漂亮代碼?

但有些團隊的境遇則截然不同。他們添加新功能的速度越來越快,因為他們能利用已有的功能,基於已有的功能快速構建新功能。

如何寫出讓同事膜拜的漂亮代碼?

兩種團隊的區別就在於軟件的內部質量。需要添加新功能時,內部質量良好的軟件讓我可以很容易找到在哪裡修改、如何修改。良好的模塊劃分使我只需要理解代碼庫的一小部分,就可以做出修改。如果代碼很清晰,我引入bug的可能性就會變小,即使引入了bug,調試也會容易得多。理想情況下,我的代碼庫會逐步演化成一個平臺,在其上可以很容易地構造與其領域相關的新功能。

我把這種現象稱為“設計耐久性假說”:通過投入精力改善內部設計,我們增加了軟件的耐久性,從而可以更長時間地保持開發的快速。我還無法科學地證明這個理論,所以我說它是一個“假說”。但我的經驗,以及我在職業生涯中認識的上百名優秀程序員的經驗,都支持這個假說。

20年前,行業的陳規認為:良好的設計必須在開始編程之前完成,因為一旦開始編寫代碼,設計就只會逐漸腐敗。重構改變了這個圖景。現在我們可以改善已有代碼的設計,因此我們可以先做一個設計,然後不斷改善它,哪怕程序本身的功能也在不斷髮生著變化。由於預先做出良好的設計非常困難,想要既體面又快速地開發功能,重構必不可少。

何時重構

在我編程的每個小時,我都會做重構。有幾種方式可以把重構融入我的工作過程裡。

三次法則
Don Roberts給了我一條準則:第一次做某件事時只管去做;第二次做類似的事會產生反感,但無論如何還是可以去做;第三次再做類似的事,你就應該重構。
正如老話說的:事不過三,三則重構。

預備性重構:讓添加新功能更容易

重構的最佳時機就在添加新功能之前。在動手添加新功能之前,我會看看現有的代碼庫,此時經常會發現:如果對代碼結構做一點微調,我的工作會容易得多。也許已經有個函數提供了我需要的大部分功能,但有幾個字面量的值與我的需要略有衝突。如果不做重構,我可能會把整個函數複製過來,修改這幾個值,但這就會導致重複代碼—如果將來我需要做修改,就必須同時修改兩處(更麻煩的是,我得先找到這兩處)。而且,如果將來我還需要一個類似又略有不同的功能,就只能再複製粘貼一次,這可不是個好主意。所以我戴上重構的帽子,使用函數參數化(310)。做完這件事以後,接下來我就只需要調用這個函數,傳入我需要的參數。

這就好像我要往東去100公里。我不會往東一頭把車開進樹林,而是先往北開20公里上高速,然後再向東開100公里。後者的速度比前者要快上3倍。如果有人催著你“趕快直接去那兒”,有時你需要說:“等等,我要先看看地圖,找出最快的路徑。”這就是預備性重構於我的意義。
——Jessica Kerr

修復bug時的情況也是一樣。在尋找問題根因時,我可能會發現:如果把3段一模一樣且都會導致錯誤的代碼合併到一處,問題修復起來會容易得多。或者,如果把某些更新數據的邏輯與查詢邏輯分開,會更容易避免造成錯誤的邏輯糾纏。用重構改善這些情況,在同樣場合再次出現同樣bug的概率也會降低。

幫助理解的重構:使代碼更易懂

我需要先理解代碼在做什麼,然後才能著手修改。這段代碼可能是我寫的,也可能是別人寫的。一旦我需要思考“這段代碼到底在做什麼”,我就會自問:能不能重構這段代碼,令其一目瞭然?我可能看見了一段結構糟糕的條件邏輯,也可能希望複用一個函數,但花費了幾分鐘才弄懂它到底在做什麼,因為它的函數命名實在是太糟糕了。這些都是重構的機會。

看代碼時,我會在腦海裡形成一些理解,但我的記性不好,記不住那麼多細節。正如Ward Cunningham所說,通過重構,我就把腦子裡的理解轉移到了代碼本身。隨後我運行這個軟件,看它是否正常工作,來檢查這些理解是否正確。如果把對代碼的理解植入代碼中,這份知識會保存得更久,並且我的同事也能看到。

重構帶來的幫助不僅發生在將來—常常是立竿見影。我會先在一些小細節上使用重構來幫助理解,給一兩個變量改名,讓它們更清楚地表達意圖,以方便理解,或是將一個長函數拆成幾個小函數。當代碼變得更清晰一些時,我就會看見之前看不見的設計問題。如果不做前面的重構,我可能永遠都看不見這些設計問題,因為我不夠聰明,無法在腦海中推演所有這些變化。Ralph Johnson說,這些初步的重構就像掃去窗上的塵埃,使我們得以看到窗外的風景。在研讀代碼時,重構會引領我獲得更高層面的理解,如果只是閱讀代碼很難有此領悟。有些人以為這些重構只是毫無意義地把玩代碼,他們沒有意識到,缺少了這些細微的整理,他們就無法看到隱藏在一片混亂背後的機遇。

撿垃圾式重構

幫助理解的重構還有一個變體:我已經理解代碼在做什麼,但發現它做得不好,例如邏輯不必要地迂迴複雜,或者兩個函數幾乎完全相同,可以用一個參數化的函數取而代之。這裡有一個取捨:我不想從眼下正要完成的任務上跑題太多,但我也不想把垃圾留在原地,給將來的修改增加麻煩。如果我發現的垃圾很容易重構,我會馬上重構它;如果重構需要花一些精力,我可能會拿一張便箋紙把它記下來,完成當下的任務再回來重構它。

當然,有時這樣的垃圾需要好幾個小時才能解決,而我又有更緊急的事要完成。不過即便如此,稍微花一點工夫做一點兒清理,通常都是值得的。正如野營者的老話所說:至少要讓營地比你到達時更乾淨。如果每次經過這段代碼時都把它變好一點點,積少成多,垃圾總會被處理乾淨。重構的妙處就在於,每個小步驟都不會破壞代碼—所以,有時一塊垃圾在好幾個月之後才終於清理乾淨,但即便每次清理並不完整,代碼也不會被破壞。

有計劃的重構和見機行事的重構

上面的例子—預備性重構、幫助理解的重構、撿垃圾式重構—都是見機行事的:我並不專門安排一段時間來重構,而是在添加功能或修復bug的同時順便重構。這是我自然的編程流的一部分。不管是要添加功能還是修復bug,重構對我當下的任務有幫助,而且讓我未來的工作更輕鬆。這是一件很重要而又常被誤解的事:重構不是與編程割裂的行為。你不會專門安排時間重構,正如你不會專門安排時間寫if語句。我的項目計劃上沒有專門留給重構的時間,絕大多數重構都在我做其他事的過程中自然發生。

骯髒的代碼必須重構,但漂亮的代碼也需要很多重構

還有一種常見的誤解認為,重構就是人們彌補過去的錯誤或者清理骯髒的代碼。當然,如果遇上了骯髒的代碼,你必須重構,但漂亮的代碼也需要很多重構。在寫代碼時,我會做出很多權衡取捨:參數化需要做到什麼程度?函數之間的邊界應該劃在哪裡?對於昨天的功能完全合理的權衡,在今天要添加新功能時可能就不再合理。好在,當我需要改變這些權衡以反映現實情況的變化時,整潔的代碼重構起來會更容易。

每次要修改時,首先令修改很容易(警告:這件事有時會很難),然後再進行這次容易的修改。
——Kent Beck

長久以來,人們認為編寫軟件是一個累加的過程:要添加新功能,我們就應該增加新代碼。但優秀的程序員知道,添加新功能最快的方法往往是先修改現有的代碼,使新功能容易被加入。所以,軟件永遠不應該被視為“完成”。每當需要新能力時,軟件就應該做出相應的改變。越是在已有代碼中,這樣的改變就越顯重要。

不過,說了這麼多,並不表示有計劃的重構總是錯的。如果團隊過去忽視了重構,那麼常常會需要專門花一些時間來優化代碼庫,以便更容易添加新功能。在重構上花一個星期的時間,會在未來幾個月裡發揮價值。有時,即便團隊做了日常的重構,還是會有問題在某個區域逐漸累積長大,最終需要專門花些時間來解決。但這種有計劃的重構應該很少,大部分重構應該是不起眼的、見機行事的。

我聽過的一條建議是:將重構與添加新功能在版本控制的提交中分開。這樣做的一大好處是可以各自獨立地審閱和批准這些提交。但我並不認同這種做法。重構常常與新添功能緊密交織,不值得花工夫把它們分開。並且這樣做也使重構脫離了上下文,使人看不出這些“重構提交”的價值。每個團隊應該嘗試並找出適合自己的工作方式,只是要記住:分離重構提交併不是毋庸置疑的原則,只有當你真的感到有益時,才值得這樣做。

長期重構

大多數重構可以在幾分鐘—最多幾小時—內完成。但有一些大型的重構可能要花上幾個星期,例如要替換一個正在使用的庫,或者將整塊代碼抽取到一個組件中並共享給另一支團隊使用,再或者要處理一大堆混亂的依賴關係,等等。

即便在這樣的情況下,我仍然不願讓一支團隊專門做重構。可以讓整個團隊達成共識,在未來幾周時間裡逐步解決這個問題,這經常是一個有效的策略。每當有人靠近“重構區”的代碼,就把它朝想要改進的方向推動一點。這個策略的好處在於,重構不會破壞代碼—每次小改動之後,整個系統仍然照常工作。例如,如果想替換掉一個正在使用的庫,可以先引入一層新的抽象,使其兼容新舊兩個庫的接口。一旦調用方已經完全改為使用這層抽象,替換下面的庫就會容易得多。(這個策略叫作Branch By Abstraction[mf-bba]。)

複審代碼時重構

一些公司會做常規的代碼複審(code review),因為這種活動可以改善開發狀況。代碼複審有助於在開發團隊中傳播知識,也有助於讓較有經驗的開發者把知識傳遞給比較欠缺經驗的人,並幫助更多人理解大型軟件系統中的更多部分。代碼複審對於編寫清晰代碼也很重要。我的代碼也許對我自己來說很清晰,對他人則不然。這是無法避免的,因為要讓開發者設身處地為那些不熟悉自己所作所為的人著想,實在太困難了。代碼複審也讓更多人有機會提出有用的建議,畢竟我在一個星期之內能夠想出的好點子很有限。如果能得到別人的幫助,我的生活會滋潤得多,所以我總是期待更多複審。

我發現,重構可以幫助我複審別人的代碼。開始重構前我可以先閱讀代碼,得到一定程度的理解,並提出一些建議。一旦想到一些點子,我就會考慮是否可以通過重構立即輕鬆地實現它們。如果可以,我就會動手。這樣做了幾次以後,我可以更清楚地看到,當我的建議被實施以後,代碼會是什麼樣。我不必想象代碼應該是什麼樣,我可以真實看見。於是我可以獲得更高層次的認識。如果不進行重構,我永遠無法得到這樣的認識。

重構還可以幫助代碼複審工作得到更具體的結果。不僅獲得建議,而且其中許多建議能夠立刻實現。最終你將從實踐中得到比以往多得多的成就感。

至於如何在代碼複審的過程中加入重構,這要取決於複審的形式。在常見的pull request模式下,複審者獨自瀏覽代碼,代碼的作者不在旁邊,此時進行重構效果並不好。如果代碼的原作者在旁邊會好很多,因為作者能提供關於代碼的上下文信息,並且充分認同複審者進行修改的意圖。對我個人而言,與原作者肩並肩坐在一起,一邊瀏覽代碼一邊重構,體驗是最佳的。這種工作方式很自然地導向結對編程:在編程的過程中持續不斷地進行代碼複審。

何時不應該重構

聽起來好像我一直在提倡重構,但確實有一些不值得重構的情況。

如果我看見一塊凌亂的代碼,但並不需要修改它,那麼我就不需要重構它。如果醜陋的代碼能被隱藏在一個API之下,我就可以容忍它繼續保持醜陋。只有當我需要理解其工作原理時,對其進行重構才有價值。

另一種情況是,如果重寫比重構還容易,就別重構了。這是個困難的決定。如果不花一點兒時間嘗試,往往很難真實瞭解重構一塊代碼的難度。決定到底應該重構還是重寫,需要良好的判斷力與豐富的經驗,我無法給出一條簡單的建議。

如何寫出讓同事膜拜的漂亮代碼?

《重構:改善既有代碼的設計(第2版)》
作者:馬丁·福勒(Martin Fowler)

本書是經典著作《重構》出版20年後的更新版。書中清晰揭示了重構的過程,解釋了重構的原理和最佳實踐方式,並給出了何時以及何地應該開始挖掘代碼以求改善。書中給出了60多個可行的重構,每個重構都介紹了種經過驗證的代碼變換手法的動機和技術。本書提出的重構準則將幫助開發人員小步地修改代碼,從而減少了開發過程中的風險。

本書適合軟件開發人員、項目管理人員等閱讀,也可作為高等院校計算機及相關專業師生的參考讀物。

作者:馬丁·福勒(Martin Fowler)
世界軟件開發大師,ThoughtWorks的首席科學家。他是一位作家、演說者、諮詢師和泛軟件開發領域的意見領袖。他致力於改善企業級的軟件設計,對優秀的設計以及支撐優秀設計的工程實踐孜孜以求。他在重構、面向對象分析設計、模式、XP和UML等領域都有卓越貢獻。著有《重構》《分析模式》《領域特定語言》等經典著作。

如何寫出讓同事膜拜的漂亮代碼?

點擊圖片直接下單

如何寫出讓同事膜拜的漂亮代碼?

異步社區每滿100減50​

pro.m.jd.com

如何寫出讓同事膜拜的漂亮代碼?

– END –

相關文章

你為什麼不敢重構代碼?聽高手親授祕笈!

Python3破冰人工智能,你需要掌握一些數學方法

人生苦短,初學者應該如何更快的學好Python?

專訪李強:我為什麼要讓孩子學習編程?