http://kb.cnblogs.com/page/526768/html
============上篇============java
最近寫了很多代碼,review了很多代碼,也作了很多重構,總之是對着爛代碼工做了幾周。爲了抒發一下這幾周裏好幾回到達崩潰邊緣的情緒,我決定寫一篇文章談一談爛代碼的那些事。這裏是上篇,談一談爛代碼產生的緣由和現象。linux
剛入程序員這行的時候常常聽到一個觀點:你要把精力放在ABCD(需求文檔/功能設計/架構設計/理解原理)上,寫代碼只是把想法翻譯成編程語言而已,是一個沒什麼技術含量的事情。git
當時的我在聽到這種觀點時會有一種近似於高冷的不屑:大家就是一羣傻X,根本不懂代碼質量的重要性,這麼下去早晚有一天會踩坑,呸。程序員
但是幾個月以後,他們彷佛也沒怎麼踩坑。而隨着編程技術一直在不斷髮展,帶來了更多的我之前認爲是傻X的人加入到程序員這個行業中來。github
語言愈來愈高級、封裝愈來愈完善,各類技術都在幫助程序員提升生產代碼的效率,依靠層層封裝,程序員真的不須要了解一丁點技術細節,只要把需求裏的內容逐行翻譯出來就能夠了?面試
不少程序員不知道要怎麼組織代碼、怎麼提高運行效率、底層是基於什麼原理,他們寫出來的是在我心目中爛成一坨翔同樣的代碼。算法
可是那一坨翔同樣代碼居然他媽的能正常工做。sql
即便我認爲他們寫的代碼是坨翔,可是從不接觸代碼的人的視角來看(好比說你的boss),代碼編譯過了,測試過了,上線運行了一個月都沒出問題,你還想要奢求什麼?數據庫
因此,即便不情願,也必須認可,時至今日,寫代碼這件事自己沒有那麼難了。
可是偶爾有那麼幾回,寫爛代碼的人離職以後,事情彷佛又變得不同了。
想要修改功能時卻發現程序裏充斥着各類沒法理解的邏輯,改完以後莫名其妙的bug一個接一個,接手這個項目的人開始漫無目的的加班,而且本來一個挺樂觀開朗的人漸漸的開始喜歡問候別人祖宗了。
我總結了幾類常常被艹祖宗的爛代碼:
能力差的程序員容易寫出意義不明的代碼,他們不知道本身究竟在作什麼.
就像這樣:
public void save() { for(int i=0;i<100;i++) { //防止保存失敗,重試100次 document.save(); } }
對於這類程序員,我通常建議他們轉行。
不說人話是新手最常常出現的問題,直接的表現就是寫了一段很簡單的代碼,其餘人卻看不懂。
好比下面這段:
public boolean getUrl(Long id) { UserProfile up = us.getUser(ms.get(id).getMessage().aid); if (up == null) { return false; } if (up.type == 4 || ((up.id >> 2) & 1) == 1) { return false; } if(Util.getUrl(up.description)) { return true; } else { return false; } }
不少程序員喜歡簡單的東西:簡單的函數名、簡單的變量名、代碼裏翻來覆去只用那麼幾個單詞命名;能縮寫就縮寫、能省略就省略、能合併就合併。這類人寫出來的代碼裏充斥着各類g/s/gos/of/mss之類的全世界沒人懂的縮寫,或者一長串不知道在作什麼的連續調用。
還有不少程序員喜歡複雜,各類宏定義、位運算之類寫的天花亂墜,生怕代碼讓別人一會兒看懂了會顯得本身水平不夠。
簡單的說,他們的代碼是寫給機器的,不是給人看的。
不恰當的組織是高級一些的爛代碼,程序員在寫過一些代碼以後,有了基本的代碼風格,可是對於規模大一些的工程的掌控能力不夠,不知道代碼應該如何解耦、分層和組織。
這種反模式的現象是常常會看到一段代碼在工程裏拷來拷去;某個文件裏放了一大坨堆砌起來的代碼;一個函數堆了幾百上千行;或者一個簡單的功能七拐八繞的調了幾十個函數,在某個難以發現的猥瑣的小角落裏默默的調用了某些關鍵邏輯。
這類代碼大多複雜度高,難以修改,常常一改就崩;而另外一方面,創造了這些代碼的人傾向於修改代碼,畏懼創造代碼,他們寧願讓本來複雜的代碼一步步變得更復雜,也不肯意從新組織代碼。當你面對一個幾千行的類,問爲何不把某某邏輯提取出來的時候,他們會說:
「可是,那樣就多了一個類了呀。」
相對於前面的例子,假設這種反模式出現的場景更頻繁,花樣更多,始做俑者也更難以本身意識到問題。好比:
public String loadString() { File file = new File("c:/config.txt"); // read something }
文件路徑變動的時候,會把代碼改爲這樣:
public String loadString(String name) { File file = new File(name); // read something }
須要加載的內容更豐富的時候,會再變成這樣:
public String loadString(String name) { File file = new File(name); // read something } public Integer loadInt(String name) { File file = new File(name); // read something }
以後可能會再變成這樣:
public String loadString(String name) { File file = new File(name); // read something } public String loadStringUtf8(String name) { File file = new File(name); // read something } public Integer loadInt(String name) { File file = new File(name); // read something } public String loadStringFromNet(String url) { HttpClient ... } public Integer loadIntFromNet(String url) { HttpClient ... }
這類程序員每每是項目組裏開發效率比較高的人,可是大量的業務開發工做致使他們不會作多餘的思考,他們的口頭禪是:「我天天要作XX個需求」或者「先作完需求再考慮其餘的吧」。
這種反模式表現出來的後果每每是代碼很難複用,面對deadline的時候,程序員迫切的想要把需求落實成代碼,而這每每也會是個循環:寫代碼的時候來不及考慮複用,代碼難複用致使以後的需求還要繼續寫大量的代碼。
一點點積累起來的大量的代碼又帶來了組織和風格一致性等問題,最後造成了一個新功能基本靠拷的遺留系統。
爛代碼還有不少種類型,沿着功能-性能-可讀-可測試-可擴展這條路線走下去,還能看到不少匪夷所思的例子。
那麼什麼是爛代碼?我的認爲,爛代碼包含了幾個層次:
因此,當一個團隊裏的底層代碼難以閱讀、耦合了上層的邏輯致使難以測試、或者對使用場景作了過多的假設致使難以複用時,雖然完成了功能,它依然是坨翔同樣的代碼。
而相對的,若是一個工程的代碼難以閱讀,能不能說這個是爛代碼?很難下定義,可能算不上好,可是能說它爛嗎?若是這個工程自始至終只有一我的維護,那我的也維護的很好,那它彷佛就成了「夠用的代碼」。
不少工程剛開始可能只是一我的負責的小項目,你們關心的重點只是代碼能不能順利的實現功能、按時完工。
過上一段時間,其餘人蔘與時才發現代碼寫的有問題,看不懂,不敢動。需求方又開始催着上線了,怎麼辦?只好當心翼翼的只改邏輯而不動結構,而後在註釋裏寫上這麼實現很ugly,之後明白內部邏輯了再重構。
再過上一段時間,有個類似的需求,想要複用裏面的邏輯,這時才意識到代碼裏作了各類特定場景的專用邏輯,複用很是麻煩。爲了趕進度只好拷代碼而後改一改。問題解決了,問題也加倍了。
幾乎全部的爛代碼都是從「夠用的代碼」演化來的,代碼沒變,使用代碼的場景發生變了,本來夠用的代碼不符合新的場景,那麼它就成了爛代碼。
程序員最喜歡跟程序員說的謊言之一就是:如今進度比較緊,等X個月以後項目進度寬鬆一些再去作重構。
不可否認在某些(極其有限的)場景下重構是解決問題的手段之一,可是寫了很多代碼以後發現,重構每每是程序開發過程當中最複雜的工做。花一個月寫的爛代碼,要花更長的時間、更高的風險去重構。
曾經經歷過幾回忍無可忍的大規模重構,每一次重構以前都是找齊了組裏的高手,開了無數次分析會,把組內需求所有暫停以後纔敢開工,而重構過程當中每每哀嚎遍野,幾乎天天都會出上不少意料以外的問題,上線時也幾乎必然會出幾個問題。
從技術上來講,重構複雜代碼時,要作三件事:理解舊代碼、分解舊代碼、構建新代碼。而待重構的舊代碼每每難以理解;模塊之間過分耦合致使牽一髮而動全身,不易控制影響範圍;舊代碼不易測試致使沒法保證新代碼的正確性。
這裏還有一個核心問題,重構的複雜度跟代碼的複雜度不是線性相關的。好比有1000行爛代碼,重構要花1個小時,那麼5000行爛代碼的重構可能要花二、3天。要對一個失去控制的工程作重構,每每還不如重寫更有效率。
而拋開具體的重構方式,從受益上來講,重構也是一件很麻煩的事情:它很難帶來直接受益,也很難量化。這裏有個頗有意思的現象,基本關於重構的書籍無一例外的都會有獨立的章節介紹「如何向boss說明重構的必要性」。
重構以後能提高多少效率?能下降多少風險?很難答上來,爛代碼自己就不是一個能夠簡單的標準化的東西。
舉個例子,一個工程的代碼可讀性不好,那麼它會影響多少開發效率?
你能夠說:以前改一個模塊要3天,重構以後1天就能夠了。可是怎麼應對「不就是作個數據庫操做嗎,爲何要3天」這類問題?爛代碼「爛」的因素有不肯定性,開發效率也因人而異,想要證實這個東西「確實」會增長2天開發時間,每每反而會變成「我看了3天才看懂這個函數是作什麼的」或者「我作這麼簡單的修改要花3天」這種神經病纔會去證實的命題。
而另外一面,許多技術負責人也意識到了代碼質量和重構的必要性,「那就重構嘛」,或者「若是看到問題了,那就重構」。上一個問題解決了,但實際上關於重構的代價和收益仍然是一筆糊塗帳,在沒有分配給你更多資源、沒有明確的目標、沒有具體方法的狀況下,很難想象除了有代碼潔癖的人還有誰會去執行這種莫名其妙的任務。
因而每每就會造成這種局面:
與寫出爛代碼不一樣的是,想寫出好代碼有不少前提:
寫出好代碼的方法論不少,但我認爲寫出好代碼的核心反而是聽起來很是low的「持續不斷的練習」。這裏就不展開了,留到下篇再說。
不少程序員在寫了幾年代碼以後並無什麼長進,代碼仍然爛的讓人不忍直視,緣由有兩個主要方面:
而工做幾年以後的人很難再說服他們去提升代碼質量,你只會反覆不斷的聽到:「那又有什麼用呢?」或者「之前就是這麼作的啊?」之類的說法。
那麼從源頭入手,提升招人時對代碼的質量的要求怎麼樣?
前一陣面試的時候增長了白板編程,最近又增長了上機編程的題目。發現了一個現象:一我的工做了幾年、作過不少項目、帶過團隊、發了一些文章,不必定能表明他代碼寫的好;反之,一我的代碼寫的好,其它方面的能力通常不會太差。
舉個例子,最近喜歡用「寫一個代碼行數統計工具」做爲面試的上機編程題目。不少人看到題目以後第一反映是,這道題太簡單了,這不就是寫寫代碼嘛。
從實際效果來看,這道題識別度卻還不錯。
首先,題目足夠簡單,即便沒有看過《面試寶典》之類書的人也不會吃虧。而題目的擴展性很好,即便提早知道題目,配合不一樣的條件,能夠變成不一樣的題目。好比要求按文件類型統計行數、或者要求提升統計效率、或者統計的同時輸出某些單詞出現的次數,等等。
從考察點來看,首先是基本的樹的遍歷算法;其次有必定代碼量,能夠看出程序員對代碼的組織能力、對問題的抽象能力;上機編碼能夠很簡單的看出應聘者是否是好久沒寫程序了;還包括對於程序易用性和性能的理解。
最重要的是,最後的結果是一個完整的程序,我能夠按照平常工做的標準去評價程序員的能力,而不是從十幾行的函數裏意淫這我的在平常工做中大概會有什麼表現。
但即便這樣,也很難拍着胸脯說,這我的寫的代碼質量沒問題。畢竟面試只是表明他有寫出好代碼的能力,而不是他未來會寫出好代碼。
說了那麼多,結論其實只有兩條,做爲程序員:
若是你看到了這裏尚未喪失但願,那麼能夠期待一下這篇文章的第二部分,關於如何提升代碼質量的一些建議和方法。
============中篇============
這是爛代碼系列的第二篇,在文章中我會跟你們討論一下如何儘量高效和客觀的評價代碼的優劣。
在發佈了關於爛代碼的那些事(上)以後,發現這篇文章居然意外的很受歡迎,不少人也描(tu)述(cao)了各自代碼中這樣或者那樣的問題。
最近部門在組織bootcamp,正好我負責培訓代碼質量部分,在培訓課程中讓你們花了很多時間去討論、改進、完善本身的代碼。雖然剛畢業的同窗對於代碼質量都很用心,但最終呈現出來的質量仍然沒能達到「十分優秀」的程度。 究其緣由,主要是不瞭解好的代碼「應該」是什麼樣的。
寫代碼的第一步是理解什麼是好代碼。在準備bootcamp課程的時候,我就爲這個問題犯了難,我嘗試着用一些精確的定義區分出「優等品」、「良品」、「不良品」,可是在總結的過程當中,關於「什麼是好代碼」的描述卻大多沒有可操做性
隨便從網上搜索了一下「優雅的代碼」,找到了下面這樣的定義:
Bjarne Stroustrup,C++之父:
- 邏輯應該是清晰的,bug難以隱藏;
- 依賴最少,易於維護;
- 錯誤處理徹底根據一個明確的策略;
- 性能接近最佳化,避免代碼混亂和無原則的優化;
- 整潔的代碼只作一件事。
Grady Booch,《面向對象分析與設計》做者:
- 整潔的代碼是簡單、直接的;
- 整潔的代碼,讀起來像是一篇寫得很好的散文;
- 整潔的代碼永遠不會掩蓋設計者的意圖,而是具備少許的抽象和清晰的控制行。
Michael Feathers,《修改代碼的藝術》做者:
- 整潔的代碼看起來老是像很在意代碼質量的人寫的;
- 沒有明顯的須要改善的地方;
- 代碼的做者彷佛考慮到了全部的事情。
看起來彷佛說的都頗有道理,但是實際評判的時候卻難以參考,尤爲是對於新人來講,如何理解「簡單的、直接的代碼」或者「沒有明顯的須要改善的地方」?
而實踐過程當中,不少同窗也確實面對這種問題:對本身的代碼老是處在一種內心不踏實的狀態,或者是本身以爲很好了,可是卻被其餘人認爲很爛,甚至有幾回我和新同窗由於代碼質量的標準一連討論好幾天,卻誰也說服不了誰:咱們都堅持本身對於好代碼的標準纔是正確的。
在經歷了無數次code review以後,我以爲這張圖彷佛總結的更好一些:
代碼質量的評價標準某種意義上有點相似於文學做品,好比對小說的質量的評價主要來自於它的讀者,由個體主觀評價造成一個相對客觀的評價。並非依靠字數,或者做者使用了哪些修辭手法之類的看似徹底客觀但實際沒有什麼意義的評價手段。
但代碼和小說還有些不同,它實際存在兩個讀者:計算機和程序員。就像上篇文章裏說的,即便全部程序員都看不懂這段代碼,它也是能夠被計算機理解並運行的。
因此對於代碼質量的定義我須要於從兩個維度分析:主觀的,被人類理解的部分;還有客觀的,在計算機裏運行的情況。
既然存在主觀部分,那麼就會存在個體差別,對於同一段代碼評價會由於看代碼的人的水平不一樣而得出不同的結論,這也是大多數新人面對的問題:他們沒有一個能夠執行的評價標準,因此寫出來的代碼質量也很難提升。
有些介紹代碼質量的文章講述的都是傾向或者原則,雖說的很對,可是實際指導做用不大。因此在這篇文章裏我但願儘量把評價代碼的標準用(我自認爲)與實際水平無關的評價方式表示出來。
在權衡好久以後,我決定把可讀性的優先級排在前面:一個程序員更但願接手一個有bug可是看得懂的工程,仍是一個沒bug可是看不懂的工程?若是是後者,能夠直接關掉這個網頁,去作些對你來講更有意義的事情。
在不少跟代碼質量有關的書裏都強調了一個觀點:程序首先是給人看的,其次纔是能被機器執行,我也比較認同這個觀點。在評價一段代碼能不能讓人看懂的時候,我習慣讓做者把這段代碼逐字翻譯成中文,試着組成句子,以後把中文句子讀給另外一我的沒有看過這段代碼的人聽,若是另外一我的能聽懂,那麼這段代碼的可讀性基本就合格了。
用這種判斷方式的緣由很簡單:其餘人在理解一段代碼的時候就是這麼作的。閱讀代碼的人會一個詞一個詞的閱讀,推斷這句話的意思,若是僅靠句子沒法理解,那麼就須要聯繫上下文理解這句代碼,若是簡單的聯繫上下文也理解不了,可能還要掌握更多其它部分的細節來幫助推斷。大部分狀況下,理解一句代碼在作什麼須要聯繫的上下文越多,意味着代碼的質量越差。
逐字翻譯的好處是能讓做者能輕易的發現那些只有本身知道的、沒有體如今代碼裏的假設和可讀性陷阱。沒法從字面意義上翻譯出本來意思的代碼大多都是爛代碼,好比「ms表明messageService「,或者「ms.proc()是發消息「,或者「tmp表明當前的文件」。
約定包括代碼和文檔如何組織,註釋如何編寫,編碼風格的約定等等,這對於代碼將來的維護很重要。對於遵循何種約定沒有一個強制的標準,不過我更傾向於遵照更多人的約定。
與開源項目保持風格一致通常來講比較靠譜,其次也能夠遵照公司內部的編碼風格。可是若是公司內部的編碼風格和當前開源項目的風格衝突比較嚴重,每每表明着這個公司的技術傾向於封閉,或者已經有些跟不上節奏了。
可是不管如何,遵照一個約定總比本身創造出一些規則要好不少,這下降了理解、溝通和維護的成本。若是一個項目本身創造出了一些奇怪的規則,可能意味着做者看過的代碼不夠多。
一個工程是否遵循了約定,每每須要代碼閱讀者有必定經驗,或者須要藉助checkstyle這樣的靜態檢查工具。若是感受無處下手,那麼大部分狀況下跟着google作應該不會有什麼大問題:能夠參考google code style,其中一部分有對應的中文版。
另外,沒有必要糾結於遵循了約定到底有什麼收益,就好像走路是靠左好仍是靠右好同樣,即便得出告終論也沒有什麼意義,大部分約定只要遵照就能夠了。
文檔和註釋是程序很重要的部分,他們是理解一個工程或項目的途徑之一。二者在某些場景下定位會有些重合或者交叉(好比javadoc實際能夠算是文檔)。
對於文檔的標準很簡單,能找到、能讀懂就能夠了,通常來講我比較關心這幾類文檔:
有一部分註釋實際是文檔,好比以前提到的javadoc。這樣能把源碼和註釋放在一塊兒,對於讀者更清晰,也能簡化很多文檔的維護的工做。
還有一類註釋並不做爲文檔的一部分,好比函數內部的註釋,這類註釋的職責是說明一些代碼自己沒法表達的做者在編碼時的思考,好比「爲何這裏沒有作XXX」,或者「這裏要注意XXX問題」。
通常來講我首先會關心註釋的數量:函數內部註釋的數量應該不會有不少,也不會徹底沒有,我的的經驗值是滾動幾屏幕看到一兩處左右比較正常。過多的話可能意味着代碼自己的可讀性有問題,而若是一點都沒有可能意味着有些隱藏的邏輯沒有說明,須要考慮適當的增長一點註釋了。
其次也須要考慮註釋的質量:在代碼可讀性合格的基礎上,註釋應該提供比代碼更多的信息。文檔和註釋並非越多越好,它們可能會致使維護成本增長。關於這部分的討論能夠參考簡潔部分的內容。
《代碼整潔之道》
新人的代碼有一個比較典型的特徵,因爲缺乏維護項目的經驗,寫的代碼總會有不少考慮不到的地方。好比說測試的時候彷佛沒什麼異常,項目發佈以後才發現有不少意料以外的情況;而出了問題以後不知道從哪下手排查,或者僅能讓系統處於一個並不穩定的狀態,依靠一些巧合勉強運行。
新手程序員廣泛沒有處理異常的意識,但代碼的實際運行環境中充滿了異常:服務器會死機,網絡會超時,用戶會胡亂操做,不懷好意的人會惡意攻擊你的系統。
我對一段代碼異常處理能力的第一印象來自於單元測試的覆蓋率。大部分異常難以在開發或者測試環境裏復現,即便有專業的測試團隊也很難在集成測試環境中模擬全部的異常狀況。
而單元測試能夠比較簡單的模擬各類異常狀況,若是一個模塊的單元測試覆蓋率連50%都不到,很難想象這些代碼考慮了異常狀況下的處理,即便考慮了,這些異常處理的分支都沒有被驗證過,怎麼期望實際運行環境中出現問題時表現良好呢?
我收到的不少簡歷裏都寫着:精通併發編程/熟悉多線程機制,諸如此類,跟他們聊的時候也說的頭頭是道,什麼鎖啊互斥啊線程池啊同步啊信號量啊一堆一堆的名詞口若懸河。而給應聘者一個實際場景,讓應聘者寫一段很簡單的併發編程的小程序,能寫好的卻很少。
實際上併發編程也確實很難,若是說寫好同步代碼的難度爲5,那麼併發編程的難度能夠達到100。這並非危言聳聽,不少看似穩定的程序,在面對併發場景的時候仍然可能出現問題:好比最近咱們就碰到了一個linux kernel在調用某個系統函數時因爲同步問題而出現crash的狀況。
而是否高質量的實現併發編程的關鍵並非是否應用了某種同步策略,而是看代碼中是否保護了共享資源:
前三種狀況可以比較簡單的經過代碼自己分辨出來,只要簡單培養一下本身對於共享資源調用的敏感度就能夠了。
可是對於最後一種狀況,每每很難簡單的經過看代碼的方式看出來,甚至出現併發問題的兩處調用並非在同一個程序裏(好比兩個系統同時讀寫一個數據庫,或者併發的調用了一個程序的不一樣模塊等)。可是,只要是代碼裏出現了不加鎖的,訪問共享資源的「先作A,再作B」之類的邏輯,可能就須要提升警戒了。
性能是評價程序員能力的一個重要指標,不少程序員也對程序的性能津津樂道。但程序的性能很難直接經過代碼看出來,每每要藉助於一些性能測試工具,或者在實際環境中執行纔能有結果。
若是僅從代碼的角度考慮,有兩個評價執行效率的辦法:
而實際工做中,也會見到一些程序員過於熱衷優化效率,相對的會帶來程序易讀性的下降、複雜度提升、或者增長工期等等。對於這類狀況,簡單的辦法是讓做者說出這段程序的瓶頸在哪裏,爲何會有這個瓶頸,以及優化帶來的收益。
固然,不管是優化不足仍是優化過分,判斷性能指標最好的辦法是用數聽說話,而不是單純看代碼,性能測試這部份內容有些超出這篇文章的範圍,就不詳細展開了。
日誌表明了程序在出現問題時排查的難易程度,經(jing)驗(chang)豐(cai)富(keng)的程序員大概都會遇到過這個場景:排查問題時就少一句日誌,查不到某個變量的值不知道是什麼,致使死活分析不出來問題到底出在哪。
對於日誌的評價標準有三個:
對於線上系統來講,通常能夠經過調整日誌級別來控制日誌的數量,因此打印日誌的代碼只要不對閱讀形成障礙,基本上都是能夠接受的。
- 《Release It!: Design and Deploy Production-Ready Software》(不要看中文版,翻譯的實在是太爛了)
- Numbers Everyone Should Know
相對於前兩類代碼來講,可維護的代碼評價標準更模糊一些,由於它要對應的是將來的狀況,通常新人很難想象如今的一些作法會對將來形成什麼影響。不過根據個人經驗,通常來講,只要反覆的提問兩個問題就能夠了:
幾乎全部程序員都知道要避免拷代碼,可是拷代碼這個現象仍是不可避免的成爲了程序可維護性的殺手。
代碼重複分爲兩種:模塊內重複和模塊間重複。不管何種重複,都在必定程度上說明了程序員的水平有問題,模塊內重複的問題更大一些,若是在同一個文件裏都能出現大片重複的代碼,那表示他什麼難以想象的代碼都有可能寫出來。
對於重複的判斷並不須要反覆閱讀代碼,通常來講現代的IDE都提供了檢查重複代碼的工具,只需點幾下鼠標就能夠了。
除了代碼重複以外,不少熱衷於維護代碼質量的程序員新人很容易出現另外一類重複:信息重複。
我見過一些新人喜歡在每行代碼前面寫一句註釋,好比:
// 成員列表的長度>0而且<200 if(memberList.size() > 0 && memberList.size() < 200) { // 返回當前成員列表 return memberList; }
看起來彷佛很好懂,可是幾年以後,這段代碼就變成了:
// 成員列表的長度>0而且<200 if(memberList.size() > 0 && memberList.size() < 200 || (tmp.isOpen() && flag)) { // 返回當前成員列表 return memberList; }
再以後可能會改爲這樣:
// edit by axb 2015.07.30 // 成員列表的長度>0而且<200 //if(memberList.size() > 0 && memberList.size() < 200 || (tmp.isOpen() && flag)) { // 返回當前成員列表 // return memberList; //} if(tmp.isOpen() && flag) { return memberList; }
隨着項目的演進,無用的信息會越積越多,最終甚至讓人沒法分辨哪些信息是有效的,哪些是無效的。
若是在項目中發現好幾個東西都在作同一件事情,好比經過註釋描述代碼在作什麼,或者依靠註釋替代版本管理的功能,那麼這些代碼也不能稱爲好代碼。
模塊內高內聚與模塊間低耦合是大部分設計遵循的標準,經過合理的模塊劃分可以把複雜的功能拆分爲更易於維護的更小的功能點。
通常來講能夠從代碼長度上初步評價一個模塊劃分的是否合理,一個類的長度大於2000行,或者一個函數的長度大於兩屏幕都是比較危險的信號。
另外一個可以體現模塊劃分水平的地方是依賴。若是一個模塊依賴特別多,甚至出現了循環依賴,那麼也能夠反映出做者對模塊的規劃比較差,從此在維護這個工程的時候頗有可能出現牽一髮而動全身的狀況。
通常來講有很多工具能提供依賴分析,好比IDEA中提供的Dependencies Analysis功能,學會這些工具的使用對於評價代碼質量會有很大的幫助。
值得一提的是,絕大部分狀況下,不恰當的模塊劃分也會伴隨着極低的單元測試覆蓋率:複雜模塊的單元測試很是難寫的,甚至是不可能完成的任務。因此直接查看單元測試覆蓋率也是一個比較靠譜的評價方式。
只要提到代碼質量,必然會提到簡潔、優雅之類的形容詞。簡潔這個詞實際涵蓋了不少東西,代碼避免重複是簡潔、設計足夠抽象是簡潔,一切對於提升可維護性的嘗試實際都是在試圖作減法。
編程經驗不足的程序員每每不能意識到簡潔的重要性,樂於搗鼓一些複雜的玩意並樂此不疲。但複雜是代碼可維護性的天敵,也是程序員能力的一道門檻。
跨過門檻的程序員應該有能力控制逐漸增加的複雜度,總結和抽象出事物的本質,並體現到本身設計和編碼中。一個程序的生命週期也是在由簡入繁到化繁爲簡中不斷迭代的過程。
對於這部分我難以總結出簡單易行的評價標準,它更像是一種思惟方式,除了要理解,還須要練習。多看、多想、多交流,不少時候能夠簡化的東西會大大超出原先的預計。
- 《重構-改善既有代碼的設計》
- 《設計模式-可複用面向對象軟件的基礎》
- 《Software Architecture Patterns-Understanding Common Architecture Patterns and When to Use Them》
這篇文章主要介紹了一些評價代碼質量優劣的手段,這些手段中,有些比較客觀,有些主觀性更強。以前也說過,對代碼質量的評價是一件主觀的事情,這篇文章裏雖然列舉了不少評價手段。可是實際上,不少我認爲沒有問題的代碼也會被其餘人吐槽,因此這篇文章只能算是初稿,更多內容還須要從此繼續補充和完善。
雖然每一個人對於代碼質量評價的傾向都不同,可是整體來講評價代碼質量的能力能夠被比做程序員的「品味」,評價的準確度會隨着自身經驗的增長而增加。在這個過程當中,須要隨時保持思考、學習和批判的精神。
下篇文章裏,會談一談具體如何提升本身的代碼質量。
============下篇============
假設你已經讀過爛代碼系列的前兩篇:瞭解了什麼是爛代碼,什麼是好代碼,可是仍是不可避免的接觸到了爛代碼(就像以前說的,幾乎沒有程序員能夠徹底避免寫出爛代碼!)接下來的問題即是:如何應對這些身邊的爛代碼。
改善代碼質量是項大工程,要開始這項工程,從可維護性入手每每是一個好的開始,但也僅僅只是開始而已。
不少人把重構當作一種一次性運動,代碼實在是爛的無法改了,或者沒什麼新的需求了,就召集一幫人專門拿出來一段時間作重構。這在傳統企業開發中多少能生效,可是對於互聯網開發來講卻很難適應,緣由有兩個:
這就造成了一個悖論:一方面那些變動頻繁的系統更須要重構;另外一方面重構又會耽誤開發進度,影響變動效率。
面對這種矛盾,一種方式是放棄重構,讓代碼質量天然降低,直到工程的生命週期結束,選擇放棄或者重來。在某些場景下這種方式確實是有效的,可是我並不喜歡:比起讓工程師不得不把天天的精力都浪費在毫無心義的事情上,爲何不作些更有意義的事呢?
開始改善代碼的第一步是把IDE的重構快捷鍵設到一個順手的鍵位上,這一步很是重要:決定重構成敗的每每不是你的新設計有多麼牛逼,而是重構自己會佔用多少時間。
好比對於IDEA來講,我會把重構菜單設爲快捷鍵:
這樣在我想去重構的時候就能夠隨手打開菜單,而不是用鼠標慢慢去點,快捷鍵每次只能爲重構節省幾秒鐘時間,可是卻能明顯減小工程師重構時的心理負擔,後面會提到,小規模的重構應該跟敲代碼同樣屬於平常開發的一部分。
我把重構分爲三類:模塊內部的重構、模塊級別的重構、工程級別的重構。分爲這三類並非由於我是什麼分類強迫症,後面會看到對重構的分類對於重構的意義。
模塊內部重構的目的是把模塊內部的邏輯梳理清楚,而且把一個巨大無比的函數拆分紅可維護的小塊代碼。大部分IDE都提供了對這類重構的支持,相似於:
這類重構的特色是修改基本集中在一個地方,對代碼邏輯的修改不多而且基本可控,IDE的重構工具比較健壯,於是基本沒有什麼風險。
如下例子演示瞭如何經過IDE把一個冗長的函數作重構:
上圖的例子中,咱們基本依靠IDE就把一個冗長的函數分紅了兩個子函數,接下來就能夠針對子函數中的一些爛代碼作進一步的小規模重構,而兩個函數內部的重構也能夠用一樣的方法。每一次小規模重構的時間都不該該超過60s,不然將會嚴重影響開發的效率,進而致使重構被無盡的開發需求淹沒。
在這個階段須要對現有的模塊補充一些單元測試,以保證重構的正確。不過以個人經驗來看,一些簡單的重構,例如修改局部變量名稱,或者提取變量之類的重構,即便沒有測試也是基本可靠的,若是要在快速完成模塊內部重構和100%的單元測試覆蓋率中選一個,我可能會選擇快速完成重構。
而這類重構的收益主要是提升函數級別的可讀性,以及消除超大函數,爲將來進一步作模塊級別的拆分打好基礎。
以後的重構開始牽扯到多個模塊,例如:
IDE每每對這類重構的支持有限,而且偶爾會出一些莫名其妙的問題,(例如修改類名時一不當心把配置文件裏的常量字符串也給修改了)。
這類重構主要在於優化代碼的設計,剝離不相關的耦合代碼,在這類重構期間你須要建立大量新的類和新的單元測試,而此時的單元測試則是必須的了。
爲何要建立單元測試?
- 一方面,這類重構由於涉及到具體代碼邏輯的修改,靠集成測試很難覆蓋全部狀況,而單元測試能夠驗證修改的正確性。
- 更重要的意義在於,寫不出單元測試的代碼每每意味着糟糕的設計:模塊依賴太多或者一個函數的職責過重,想象一下,想要執行一個函數卻要模擬十幾個輸入對象,每一個對象還要模擬本身依賴的對象……若是一個模塊沒法被單獨測試,那麼從設計的角度來考慮,無疑是不合格的。
還須要囉嗦一下,這裏說的單元測試只對一個模塊進行測試,依賴多個模塊共同完成的測試並不包含在內——例如在內存裏模擬了一個數據庫,並在上層代碼中測試業務邏輯-這類測試並不能改善你的設計。
在這個期間還會寫一些過渡用的臨時邏輯,好比各類adapter、proxy或者wrapper,這些臨時邏輯的生存期可能會有幾個月到幾年,這些看起來沒什麼必要的工做是爲了控制重構範圍,例如:
class Foo { String foo() { ... } }
若是要把函數聲明改爲
class Foo { boolean foo() { ... } }
那麼最好經過加一個過渡模塊來實現:
class FooAdaptor { private Foo foo; boolean foo() { return foo.foo().isEmpty(); } }
這樣作的好處是修改函數時不須要改動全部調用方,爛代碼的特徵之一就是模塊間的耦合比較高,每每一個函數有幾十處調用,牽一髮而動全身。而一旦開始全面改造,每每就會把一次看起來很簡單的重構演變成幾周的大工程,這種大規模重構每每是不可靠的。
每次模塊級別的重構都須要精心設計,提早劃分好哪些是須要修改的,哪些是須要用兼容邏輯作過渡的。但實際動手修改的時間都不該該超過一天,若是超過一天就意味着此次重構改動太多,須要控制一下修改節奏了。
不安全的重構相對而言影響範圍比較大,好比:
我更建議這類操做不要用IDE,若是使用IDE,也只使用最簡單的「移動」操做。這類重構單元測試已經徹底沒有做用,須要集成測試的覆蓋。不過也沒必要緊張,若是隻作「移動」的話,大部分狀況下基本的冒煙測試就能夠保證重構的正確性。
這類重構的目的是根據代碼的層次或者類型進行拆分,切斷循環依賴和結構上不合理的地方。若是不知道如何拆分,能夠依照以下思路:
而這類重構絕對不能跟正常的需求開發並行執行:代碼衝突幾乎沒法避免,而且會讓全部人崩潰。個人作法通常是在這類重構前先演練一次:把模塊按大體的想法拖來拖去,經過編譯器找到依賴問題,在平常上線中把容易處理的依賴問題解決掉;而後集中團隊裏的精英,通知全部人暫停開發,花最多二、3天時間把全部問題集中突擊掉,新的需求都在新代碼的基礎上進行開發。
若是歷史包袱實在過重,能夠把這類重構也拆成幾回作:先大致拆分紅幾塊,再分別拆分。不管如何,這類重構務必控制好變動範圍,一次嚴重的合併衝突有可能讓團隊中的全部人幾個周緩不過勁來。
典型的重構週期相似下面的過程:
性能這個話題愈來愈多的被人提起,隨便收到一份簡歷不寫上點什麼熟悉高併發、作過性能優化之類的彷佛都很差意思跟人打招呼。
說個真事,幾年前在我作某公司的ERP項目,裏面有個功能是生成一個報表。而使用咱們系統的公司裏有一我的,他天天要在下班前點一下報表,導出到excel,再發一封郵件出去。
問題是,那個報表每次都要2,3分鐘才能生成。
我當時正年輕氣盛,看到有個兩分鐘才能生成的報表一下就來了興趣,翻出了那段不知道誰寫的代碼,發現裏面用了3層循環,每次都會去數據庫查一次數據,再把一堆數據拼起來,一股腦塞進一個tableview裏。
面對這種代碼,我還能作什麼呢?
作了這些以後,界面只須要不到1s就能展現出來了,不過我要說的不是這個。
後來我去客戶公司給那個操做員演示新的模塊的時候,點一下,刷,數據出來了。那我的很驚恐的看着我,而後問我,是否是數據不許了。
再後來,我又加了一個功能,那個模塊每次打開以後都會顯示一個進度條,上面的標題是「正在校驗數據……」,進度條走完大概要1分鐘左右,我跟那人說校驗數據計算量很大,會比較慢。固然,實際上那60秒里程序毛事都沒作,只是在一點點的更新那個進度條(我還作了個彩蛋,在讀進度的時候按上上下下左右左右BABA的話就能夠加速10倍讀條…)。客戶很開心,說感受數據準確多了,固然,他沒發現彩蛋。
我寫了這麼多,是想讓你明白一個事實:大部分程序對性能並不敏感。而少數對性能敏感的程序裏,一大半能夠靠調節參數解決性能問題;最後那一小撮須要修改代碼優化性能的程序裏,性價比高的工做又是少數。
什麼是性價比?回到剛纔的例子裏,我作了那麼多事,每件事的收益是多少?
我如今遇到的不少面試者說程序優化時老是喜歡說一些玄乎的東西:調用棧、尾遞歸、內聯函數、GC調優……可是當我問他們:把一個普通函數改爲內聯函數是把原來運行速度是多少的程序優化成多少了,卻不多有人答出來;或者是扭扭捏捏的說,應該不少,由於這個函數會被調用不少遍。我再問會被調用多少遍,每遍是多長時間,就答不上來了。
因此關於性能優化,我有兩個觀點:
至於具體的優化措施,無外乎幾類:
關於性能優化的話題還能夠講不少內容,不過對於這篇文章來講有點跑題,這裏就再也不詳細展開了。
前一陣聽一個技術分享,說是他們在編程的時候要考慮太陽黑子對cpu計算的影響,或者是農民伯伯的豬把基站拱塌了之類的特殊場景。若是要優化程序的健壯性,那麼有時候就不得不去考慮這些極端狀況對程序的影響。
大部分的人應該不用考慮太陽黑子之類的高深的問題,可是咱們須要考慮一些常見的特殊場景,大部分程序員的代碼對於一些特殊場景都會有或多或少考慮不周全的地方,例如:
常規的方法確實可以發現代碼中的一些bug,可是到了複雜的生產環境中時,總會出現一些徹底沒有想到的問題。雖然我也想了好久,遺憾的是,對於健壯性來講,我並無找到什麼立竿見影的解決方案,所以,我只能謹慎的提出一點點建議:
看了上面的那麼多東西以後,你能夠想一下這麼個場景:
在你作了不少事情以後,代碼質量彷佛有了質的飛躍。正當你覺得終於能夠擺脫每天踩屎的日子了的時候,某次不當心瞥見某個類又長到幾千行了。
你憤怒的翻看提交日誌,想找出罪魁禍首是誰,結果卻發現天天都會有人往文件裏提交那麼十幾二十行代碼,每次的改動看起來都沒什麼問題,可是日積月累,一年年過去,當初花了九牛二虎之力重構的工程又成了一坨爛代碼……
任何一個對代碼有追求的程序員都有可能遇到這種問題,技術在更新,需求在變化,公司人員會流動,而代碼質量總會在不經意間偷偷的變差……
想要改善代碼質量,最後每每就會變成改善生存環境。
團隊須要一套統一的編碼規範、統一的語言版本、統一的編輯器配置、統一的文件編碼,若是有條件最好能使用統一的操做系統,這能避免不少無心義的工做。
就好像最近渣浪給開發所有換成了統一的macbook,一晚上之間之前的不少問題都變得不是問題了:字符集、換行符、IDE之類的問題只要一個配置文件就解決了,再也不有各類稀奇古怪的代碼衝突或者不兼容的問題,也不會有人忽然提交上來一些編碼格式稀奇古怪的文件了。
代碼倉庫基本上已是每一個公司的標配,而如今的代碼倉庫除了儲存代碼,還能夠承擔一些團隊溝通、代碼review甚至工做流程方面的任務,現在這類開源的系統不少,像gitlab(github)、Phabricator這類優秀的工具都能讓代碼管理變得簡單不少。我這裏無心討論svn、git、hg仍是什麼其它的代碼管理工具更好,就算最近火熱的git在複雜性和集中化管理上也有一些問題,其實我是比較期待能有替代git的工具產生的,扯遠了。
代碼倉庫的意義在於讓更多的人可以得到和修改代碼,從而提升代碼的生命週期,而代碼自己的生命週期足夠持久,對代碼質量作的優化纔有意義。
大多數爛代碼就像癌症同樣,當爛代碼已經產生了能夠感受到的影響時,基本已是晚期,很難治好了。
所以提早發現代碼變爛的趨勢很重要,這類工做能夠依賴相似於checkstyle,findbug之類的靜態檢查工具,及時發現代碼質量下滑的趨勢,例如:
有了代碼倉庫以後,就能夠把這種工具與倉庫的觸發機制結合起來,每次提交的時候作覆蓋率、靜態代碼檢查等工做,jenkins+sonarqube或者相似的工具就能夠完成基本的流程:伴隨着代碼提交進行各類靜態檢查、運行各類測試、生成報告並供人蔘考。
在實踐中會發現,關於持續反饋的五花八門的工具不少,可是真正有用的每每只有那麼一兩個,大部分人並不會去在每次提交代碼以後再打開一個網頁點擊「生成報告」,或者去登錄什麼系統看一下測試的覆蓋率是否是變低了,所以一個一站式的系統大多數狀況下會表現的更好。與其追求更多的功能,不如把有限的幾個功能整合起來,例如咱們把代碼管理、迴歸測試、代碼檢查、和code review集成起來,就是這個樣子:
固然,關於持續集成還能夠作的更多,篇幅所限,就很少說了。
不一樣的團隊文化會對技術產生微妙的影響,關於代碼質量沒有什麼共同的文化,每一個公司都有本身的一套觀點,而且彷佛都能說得通。
對於我本身來講,關於代碼質量是這樣的觀點:
如何讓大多數人認同關於代碼質量的觀點其實是有一些難度的,大部分技術人員對代碼質量的觀點是既不同意、也不反對的中立態度,而代碼質量就像是熵值同樣,放着無論老是會像更加混亂的方向演進,而且寫爛代碼的成本實在是過低了,以致於一個實習生花上一個禮拜就能夠毀了你花了半年精心設計的工程。
因此在提升代碼質量時,務必想辦法拉上團隊裏的其餘人一塊兒。雖然「引導團隊提升代碼質量」這件事情一開始會很辛苦,可是一旦有了一些支持者,而且有了能夠參考的模板以後,剩下的工做就簡單多了。
這裏推薦《佈道之道:引領團隊擁抱技術創新》這本書,裏面大部分的觀點對於代碼質量也是能夠借鑑的。僅靠喊口號很難讓其餘人寫出高質量的代碼,讓團隊中的其餘人體會到高質量代碼的收益,比喊口號更有說服力。
優化代碼質量是一件頗有意思,也頗有挑戰性的事情,而挑戰不光來自於代碼本來有多爛,要改進的也並不僅是代碼自己,還有工具、習慣、練習、開發流程、甚至團隊文化這些方方面面的事情。
寫這一系列文章前先後後花了半年多時間,一直處在寫一點刪一點的狀態:我自身關於代碼質量的想法和實踐也在經歷着不斷變化。我更但願能寫出一些可以實踐落地的東西,而不是喊喊口號,忽悠忽悠「敏捷開發」、「測試驅動」之類的幾個名詞就結束了。
可是在寫文章的過程當中就會慢慢發現,不少問題的改進方法確實不是一兩篇文章能夠說明白的,問題之間每每又相互關聯,全都展開說甚至超出了一本書的信息量,因此這篇文章也只能刪去了不少內容。
我參與過不少代碼質量很好的項目,也參與過一些質量很爛的項目,改進了不少項目,也放棄了一些項目,從最初的單打獨鬥本身改代碼,到後來帶領團隊優化工做流程,經歷了不少。不管如何,關於爛代碼,我決定引用一下《佈道之道》這本書裏的一句話:
「‘更好’,其實不是一個目的地,而是一個方向…在當前的位置和未來的目標之間,可能有不少至關不錯的地方。你只需關注離開如今的位置,而不要關心去向何方。」