關於爛代碼的那些事(上)

1.寫爛代碼很容易

剛入程序員這行的時候常常聽到一個觀點:你要把精力放在ABCD(需求文檔/功能設計/架構設計/理解原理)上,寫代碼只是把想法翻譯成編程語言而已,是一個沒什麼技術含量的事情。java

當時的我在聽到這種觀點時會有一種近似於高冷的不屑:大家就是一羣傻X,根本不懂代碼質量的重要性,這麼下去早晚有一天會踩坑,呸。程序員

但是幾個月以後,他們彷佛也沒怎麼踩坑。而隨着編程技術一直在不斷髮展,帶來了更多的我之前認爲是傻X的人加入到程序員這個行業中來。面試

語言愈來愈高級、封裝愈來愈完善,各類技術都在幫助程序員提升生產代碼的效率,依靠層層封裝,程序員真的不須要了解一丁點技術細節,只要把需求裏的內容逐行翻譯出來就能夠了。算法

不少程序員不知道要怎麼組織代碼、怎麼提高運行效率、底層是基於什麼原理,他們寫出來的是在我心目中爛成一坨翔同樣的代碼。數據庫

可是那一坨翔同樣代碼居然他媽的能正常工做。編程

即便我認爲他們寫的代碼是坨翔,可是從不接觸代碼的人的視角來看(好比說你的boss),代碼編譯過了,測試過了,上線運行了一個月都沒出問題,你還想要奢求什麼?架構

因此,即便不情願,也必須認可,時至今日,寫代碼這件事自己沒有那麼難了。編程語言

2.爛代碼終究是爛代碼

可是偶爾有那麼幾回,寫爛代碼的人離職了以後,事情彷佛又變得不同了。函數

想要修改功能時卻發現程序裏充斥着各類沒法理解的邏輯、改完以後莫名其妙的bug一個接一個,接手這個項目的人開始漫無目的的加班,而且本來一個挺樂觀開朗的人漸漸的開始喜歡問候別人祖宗了。工具

修改代碼

我總結了幾類常常被艹祖宗的爛代碼:

2.1.意義不明

能力差的程序員容易寫出意義不明的代碼,他們不知道本身究竟在作什麼.

就像這樣:

public void save() {
    for(int i=0;i<100;i++) {
        //防止保存失敗,重試100次
        document.save();
    }
}


對於這類程序員,我通常建議他們轉行。

2.2.不說人話

不說人話是新手最常常出現的問題,直接的表現就是寫了一段很簡單的代碼,其餘人卻看不懂。

好比下面這段:

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之類的全世界沒人懂的縮寫,或者一長串不知道在作什麼的連續調用。

還有不少程序員喜歡複雜,各類宏定義、位運算之類寫的天花亂墜,生怕代碼讓別人一會兒看懂了會顯得本身水平不夠。

簡單的說,他們的代碼是寫給機器的,不是給人看的。

2.3.不恰當的組織

不恰當的組織是高級一些的爛代碼,程序員在寫過一些代碼以後,有了基本的代碼風格,可是對於規模大一些的工程的掌控能力不夠,不知道代碼應該如何解耦、分層和組織。

這種反模式的現象是常常會看到一段代碼在工程裏拷來拷去;某個文件裏放了一大坨堆砌起來的代碼;一個函數堆了幾百上千行;或者一個簡單的功能七拐八繞的調了幾十個函數,在某個難以發現的猥瑣的小角落裏默默的調用了某些關鍵邏輯。

不恰當的組織

這類代碼大多複雜度高,難以修改,常常一改就崩;而另外一方面,創造了這些代碼的人傾向於修改代碼,畏懼創造代碼,他們寧願讓本來複雜的代碼一步步變得更復雜,也不肯意從新組織代碼。當你面對一個幾千行的類,問爲何不把某某邏輯提取出來的時候,他們會說:

「可是,那樣就多了一個類了呀。」

2.4.假設和缺乏抽象

相對於前面的例子,假設這種反模式出現的場景更頻繁,花樣更多,始做俑者也更難以本身意識到問題。好比:

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的時候,程序員迫切的想要把需求落實成代碼,而這每每也會是個循環:寫代碼的時候來不及考慮複用,代碼難複用致使以後的需求還要繼續寫大量的代碼。

一點點積累起來的大量的代碼又帶來了組織和風格一致性等問題,最後造成了一個新功能基本靠拷的遺留系統。

2.5.還有嗎

爛代碼還有不少種類型,沿着功能-性能-可讀-可測試-可擴展這條路線走下去,還能看到不少匪夷所思的例子。

那麼什麼是爛代碼?我的認爲,爛代碼包含了幾個層次:

  • 若是隻是一我的維護的代碼,知足功能和性能要求倒也足夠了。
  • 若是在一個團隊裏工做,那就必須易於理解和測試,讓其它人員有能力修改各自的代碼。
  • 同時,越是處於系統底層的代碼,擴展性也越重要。

因此,當一個團隊裏的底層代碼難以閱讀、耦合了上層的邏輯致使難以測試、或者對使用場景作了過多的假設致使難以複用時,雖然完成了功能,它依然是坨翔同樣的代碼。

2.6.夠用的代碼

而相對的,若是一個工程的代碼難以閱讀,能不能說這個是爛代碼?很難下定義,可能算不上好,可是能說它爛嗎?若是這個工程自始至終只有一我的維護,那我的也維護的很好,那它彷佛就成了「夠用的代碼」。

不少工程剛開始可能只是一我的負責的小項目,你們關心的重點只是代碼能不能順利的實現功能、按時完工。

過上一段時間,其餘人蔘與時才發現代碼寫的有問題,看不懂,不敢動。需求方又開始催着上線了,怎麼辦?只好當心翼翼的只改邏輯而不動結構,而後在註釋裏寫上這麼實現很ugly,之後明白內部邏輯了再重構。

再過上一段時間,有個類似的需求,想要複用裏面的邏輯,這時才意識到代碼裏作了各類特定場景的專用邏輯,複用很是麻煩。爲了趕進度只好拷代碼而後改一改。問題解決了,問題也加倍了。

幾乎全部的爛代碼都是從「夠用的代碼」演化來的,代碼沒變,使用代碼的場景發生變了,本來夠用的代碼不符合新的場景,那麼它就成了爛代碼。

3.重構不是萬能藥

程序員最喜歡跟程序員說的謊言之一就是:如今進度比較緊,等X個月以後項目進度寬鬆一些再去作重構。

不可否認在某些(極其有限的)場景下重構是解決問題的手段之一,可是寫了很多代碼以後發現,重構每每是程序開發過程當中最複雜的工做。花一個月寫的爛代碼,要花更長的時間、更高的風險去重構。

曾經經歷過幾回忍無可忍的大規模重構,每一次重構以前都是找齊了組裏的高手,開了無數次分析會,把組內需求所有暫停以後纔敢開工,而重構過程當中每每哀嚎遍野,幾乎天天都會出上不少意料以外的問題,上線時也幾乎必然會出幾個問題。

重構

從技術上來講,重構複雜代碼時,要作三件事:理解舊代碼、分解舊代碼、構建新代碼。而待重構的舊代碼每每難以理解;模塊之間過分耦合致使牽一髮而動全身,不易控制影響範圍;舊代碼不易測試致使沒法保證新代碼的正確性。

這裏還有一個核心問題,重構的複雜度跟代碼的複雜度不是線性相關的。好比有1000行爛代碼,重構要花1個小時,那麼5000行爛代碼的重構可能要花二、3天。要對一個失去控制的工程作重構,每每還不如重寫更有效率。

而拋開具體的重構方式,從受益上來講,重構也是一件很麻煩的事情:它很難帶來直接受益,也很難量化。這裏有個頗有意思的現象,基本關於重構的書籍無一例外的都會有獨立的章節介紹「如何向boss說明重構的必要性」。

重構以後能提高多少效率?能下降多少風險?很難答上來,爛代碼自己就不是一個能夠簡單的標準化的東西。

舉個例子,一個工程的代碼可讀性不好,那麼它會影響多少開發效率?

你能夠說:以前改一個模塊要3天,重構以後1天就能夠了。可是怎麼應對「不就是作個數據庫操做嗎爲何要3天」這類問題?爛代碼「爛」的因素有不肯定性、開發效率也因人而異,想要證實這個東西「確實」會增長兩天開發時間,每每反而會變成「我看了3天才看懂這個函數是作什麼的」或者「我作這麼簡單的修改要花3天」這種神經病纔會去證實的命題。

而另外一面,許多技術負責人也意識到了代碼質量和重構的必要性,「那就重構嘛」,或者「若是看到問題了,那就重構」。上一個問題解決了,但實際上關於重構的代價和收益仍然是一筆糊塗帳,在沒有分配給你更多資源、沒有明確的目標、沒有具體方法的狀況下,很難想象除了有代碼潔癖的人還有誰會去執行這種莫名其妙的任務。

因而每每就會造成這種局面:

  • 不寫代碼的人認爲應該重構,重構很簡單,不管新人仍是老人都有責任作重構。
  • 寫代碼老手認爲應該早晚應該重構,重構很難,如今湊合用,這事別落在我頭上。
  • 寫代碼的新手認爲不出bug就謝天謝地了,我也不知道怎麼重構。

4.寫好代碼很難

與寫出爛代碼不一樣的是,想寫出好代碼有不少前提:

  • 理解要開發的功能需求。
  • 瞭解程序的運行原理。
  • 作出合理的抽象。
  • 組織複雜的邏輯。
  • 對本身開發效率的正確估算。
  • 持續不斷的練習。

寫出好代碼的方法論不少,但我認爲寫出好代碼的核心反而是聽起來很是low的「持續不斷的練習」。這裏就不展開了,留到下篇再說。

不少程序員在寫了幾年代碼以後並無什麼長進,代碼仍然爛的讓人不忍直視,緣由有兩個主要方面:

  • 環境是很重要的因素之一,在爛代碼的薰陶下很難理解什麼是好代碼,知道的人大部分也會選擇隨波逐流。
  • 還有我的性格之類的說不清道不明的主觀因素,寫出爛代碼的程序員反而都是一些很好相處的人,他們每每熱愛公司團結同事平易近人工做不辭辛苦–只是代碼很爛而已。

而工做幾年以後的人很難再說服他們去提升代碼質量,你只會反覆不斷的聽到:「那又有什麼用呢?」或者「之前就是這麼作的啊?」之類的說法。

那麼從源頭入手,提升招人時對代碼的質量的要求怎麼樣?

前一陣面試的時候增長了白板編程、最近又增長了上機編程的題目。發現了一個現象:一我的工做了幾年、作過不少項目、帶過團隊、發了一些文章,不必定能表明他代碼寫的好;反之,一我的代碼寫的好,其它方面的能力通常不會太差。

舉個例子,最近喜歡用「寫一個代碼行數統計工具」做爲面試的上機編程題目。不少人看到題目以後第一反映是,這道題太簡單了,這不就是寫寫代碼嘛。

從實際效果來看,這道題識別度卻還不錯。

首先,題目足夠簡單,即便沒有看過《面試寶典》之類書的人也不會吃虧。而題目的擴展性很好,即便提早知道題目,配合不一樣的條件,能夠變成不一樣的題目。好比要求按文件類型統計行數、或者要求提升統計效率、或者統計的同時輸出某些單詞出現的次數,等等。

從考察點來看,首先是基本的樹的遍歷算法;其次有必定代碼量,能夠看出程序員對代碼的組織能力、對問題的抽象能力;上機編碼能夠很簡單的看出應聘者是否是好久沒寫程序了;還包括對於程序易用性和性能的理解。

最重要的是,最後的結果是一個完整的程序,我能夠按照平常工做的標準去評價程序員的能力,而不是從十幾行的函數裏意淫這我的在平常工做中大概會有什麼表現。

但即便這樣,也很難拍着胸脯說,這我的寫的代碼質量沒問題。畢竟面試只是表明他有寫出好代碼的能力,而不是他未來會寫出好代碼。

5.悲觀的結語

說了那麼多,結論其實只有兩條,做爲程序員:

  • 不要奢望其餘人會寫出高質量的代碼
  • 不要覺得本身寫出來的是高質量的代碼

若是你看到了這裏尚未喪失但願,那麼能夠期待一下這篇文章的第二部分,關於如何提升代碼質量的一些建議和方法。

相關文章
相關標籤/搜索