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

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

近期全棧開發者 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 -

相關文章
相關標籤/搜索