代碼之美——《重構》、《代碼整潔之道》
什麽樣的代碼才是美的代碼?一千個coders可能會給出一千個答案。今天,讓我從一個簡單的角度來談談對於代碼之美的理解。
可讀性高的代碼才有可能是美的代碼
相信大家都有過這樣的經歷:接手一個項目要修復bug或者開發新功能的時候,發現代碼可讀性非常差。哪怕是在有說明文檔的情況下,都不太敢提交代碼,唯恐引入新的bug或者直接導致系統崩潰。
軟件系統在代碼方面的成本分為兩部分,第一部分是開發成本,第二部分是維護成本。
開發成本很好理解,就是把一個系統開發出來需要支付的各種成本,其中最大一塊很可能是人力成本。任何一個大型軟件系統都不可能沒有bug。系統開始運作之後,這些bug可能會導致業務出錯、運行緩慢,甚至是宕機,需要程序員找出漏洞並且修復上線。在這一階段的成本,我們稱之為維護成本。
運維成本往往會超過開發成本。我畢業後的第一份工作是在一家ERP公司做二次開發,最早的代碼註釋時間有十幾年前的,常年從事維護開發工作的人員超過十個人。開發需要的時間遠比維護的時間要短得多,維護人員的數量並不會比開發人員的數量要少太多。
如果適當增加開發成本可以大幅減少維護成本,那麽對節省整體成本有著極大的幫助。
在進入到互聯網時代的今天,像ERP這樣開發完成之後變化不大的“重”系統已經不多了,模塊化、服務化的“輕”系統已經占到絕大多數。這些現代軟件系統強調的是叠代開發,以月或者周為單位更新版本,可以說幾乎永遠不會進入到所謂的“維護”階段。因此,如何平衡版本間的成本占比,是程序員和管理者必須要關心的主題。
《重構》裏有這麽一段話:“任何一個傻瓜都能寫出計算機可以理解的代碼。唯有寫出人類容易理解的代碼,才是優秀的程序員。”如果只是寫出可以運行的代碼,卻不具備可讀性或可讀性很差,負責接手項目的下一位程序員就很難在原有的基礎上再進行開發工作。這樣做固然初始的開發成本會比較低,但是後續維護和再開發的成本就很容易變得不可接受地高。
哪怕有人抱著“我走後,哪管它洪水滔天”的想法,卻仍要考慮更普遍的一種情況。在更多的時候,需要讀懂這些糟糕的代碼並且付出代價的人,往往就是幾個月後已經忘記這段代碼為什麽要這麽寫的你自己。
不要寫過長函數
可讀性差的代碼有很多特征,其中最典型的就是存在大量過長的函數。
過長的函數之所以會導致難以理解,主要是因為裏面做了很多件事,而且容易導致抽象層次不一致。在完全陌生的情況下去讀這些代碼,我們經常會讀著讀著就不知道某幾行代碼是在做什麽、為什麽要這麽做,往往要花很大的功夫去蒙去猜。
剛入門的程序員很容易問出這樣的問題:“到底多少行代碼的函數才算過長呢?”我們可以從Martin Fowler那裏得到答案,“關鍵不在於函數的長度,而在於函數‘做什麽’和‘如何做’之間的語義距離”。這句話換個角度理解,首先你要確保你的函數做完且只做一件事,然後這個函數裏的代碼都只為這件事服務。
如果函數已經過長了應該怎麽辦?不要猶豫,立刻切分它。我們先要讀出面前這個過長函數到底做了多少件事,然後逐一地按照事務來提取代碼,以子函數的形式對其進行抽象。
有人可能會擔心,生怕這樣會導致一個類裏面的函數過多,會導致其他不良後果。這種憂慮其實毫無必要,因為實際經驗告訴我們:只要切分是恰當的,不但不會有壞處,還會有除了提升代碼可讀性的其他好處。
其中一個最明顯的好處就是可復用性會增強。一段代碼如果沒有提取出來成為獨立的函數,其他地方如果要想用到就必須復制粘貼,這不但體現不出代碼可復用的好處,還會引發代碼一個新的壞味道:重復代碼。
註釋是一把雙刃劍
對於註釋,我們的第一印象往往是正面的。註釋並不是代碼,對軟件系統本身並不會起任何作用。它是為了讓讀源代碼的人更好地理解原作者的意圖而存在的,因此我們在閱讀一段很難理解的代碼的時候總是希望能有註釋。
正式參加工作之後,我們對註釋的看法可能會發生一些轉變。首先,有些人的註釋寫得很糟糕,表達出來的意思常常讓人雲裏霧裏,甚至還不如不看這些註釋。然後,有些人的代碼寫得很糟糕,註釋雖然有所幫助,但是這就成了程序員逃避被要求寫出整潔代碼的法寶。更何況,還有一部分人的代碼和註釋都寫得很糟糕。
另外,註釋也有維護成本。註釋依附於代碼,所以如果代碼有所改動,註釋也要相應有所更新,這裏就產生了成本。如果更新不及時或者沒有更新,註釋就會過時,產生誤導代碼閱讀者的風險。
因此,除了涉及到比較復雜的算法或者不得不特別加以說明的約定等情況,我並不建議優先使用註釋。命名良好的獨立函數應該成為優秀程序員的第一選擇。
如果你在閱讀自己寫的或者是需要重構的代碼時,發現某一個地方需要用註釋來說明,這就表明這裏很有可能應該提取出成一個獨立函數。新的獨立函數需要有一個良好的命名,要做到讓人可以輕易地“望文生義”,這才是最好的“註釋”。
註釋不是代碼,要想提升代碼可讀性,最好的途徑還是應該通過代碼本身去實現。
測試!測試!
無論我們怎麽努力,也很難一下子就寫出可讀性很強的代碼。這就像寫文章一樣,我們的大部分精力都放在表達思想上面,文從字順有的時候就不太顧得上。寫代碼,第一要務是能運行,能實現軟件系統的功能。
《代碼整潔之道》的作者寫道:“我沒指望你能夠一次過寫出整潔、漂亮的程序。如果說我們從過去幾十年裏面學到什麽東西的話,那就是編程是一種技藝甚於科學的東西。要編寫整潔代碼,必須先寫骯臟的代碼,然後再清理它。”
很多人寫出了可以運行的、“骯臟”的代碼,或者說接手了一個可讀性比較差的系統,往往不願意去重構它們。他們的理由看上去是十分充分的,那就是容易引入新bug。業界有名言可以作為依據:如果沒壞,就別去修它(If ain’t broke, don’t fix it)。
我最近才犯了一個錯誤:在沒有經過測試的情況下,把一段重構過的代碼上線到生產環境。結果就是導致一個重要功能不可用,最終要通過緊急版本進行修復。
事後反省時,我並沒有賭咒發誓以後再也不重構代碼,而是在重構之外加上一個必要的環節——測試。
《重構》一書開頭便寫道:“每當我要進行重構的時候,第一個步驟永遠相同:我得為即將修改的代碼建立一組可靠的測試環境。這些測試是必要的,因為盡管遵循重構手法可以使我避免絕大多數引入bug的情形,但我畢竟是人,畢竟有可能犯錯。所以我需要可靠的測試。”
測試有很多種類型,其中單元測試和功能測試最為常用。對於Java程序員,我們可以使用JUnit等測試框架去寫單元測試,往往可以避免相當一部分引入bug的情形。然後,針對不同的業務場景,我們還需要做一些簡單的功能測試。
重構和測試當然都需要成本,還要承擔不可能完全避免的引入bug的風險。但是我相信,為了提升代碼的質量,為了降低維護和後續開發的成本,這些都是值得付出的代價。
Coding進階之道
對於代碼的理解,不同人能到達不同的層次。剛開始學習,停留在行級。入門之後,進步到方法級。工作之後,可以到達類級和模塊級。經驗積累足夠多了,會精進到架構級,甚至是更高的級別。
但是無論如何,任何進步都是從一點一滴的努力中得來的。如果你自我評價還沒有到達很高的級別,那就不妨一起來嘗試學習和實踐重構,讓經過自己手的代碼變得更美。
《重構》裏有一段話非常有啟發性:“一開始我所做的重構都像這樣停留在細枝末節上。隨著代碼漸趨簡潔,我發現自己可以看到一些以前看不到的設計層面的東西。如果不對代碼做這些修改,也許我永遠看不見它們,因為我的聰明才智不足以在腦子裏把這一切都想象出來。Ralph Johnson把這種‘早期重構’描述成‘擦掉窗戶上的汙垢,使你看得更遠’。研究代碼時我發現,重構把我帶到更高的理解層次上。如果沒有重構,我達不到這種層次。”
技術的精進要靠什麽?有的人說是天賦,這是老天爺賞飯吃,一般人都達不到。有的人說是經驗,一定要有多個大型項目的積累才有可能成為專家。有的人說是學習,必須把原理研究得很深很透才能把握技術的本質。然而,這些都不是一個腳踏實地的好答案。
技術的精進,靠的應該是一個又一個經過時間檢驗的、得到業界認可的工具。從業人員通過使用這些工具優化自己的工作輸出,日積月累,最終成為各自領域的佼佼者。重構,就是這麽一個經典的、優秀的、實用的工具。
學習重構、實踐重構、掌握重構,能夠幫助我們成為更好的coder。
我有一個公眾號,經常會發一些看書思考過後寫的文章,還有作為一個程序員的工作經驗分享。
如果你喜歡我的文字,可以用微信搜索“李文業的思考筆記”或者掃描二維碼關註。
代碼之美——《重構》、《代碼整潔之道》