糟糕程序員的各類跡象

爲何寫這篇文章?

本文提到的絕大多數錯誤,都是做者歷經一番艱辛才得以發現,要麼是由於本身犯過,要麼是在別人的工做中見過。程序員

本文並不是意圖對程序員劃分等級,只是適合某些程序員閱讀,他們相信本身有能力判斷一件事情在什麼狀況下是不良習慣的跡象,在什麼狀況下則是特殊環境致使的結果。正則表達式

寫這個系列是爲了迫使做者自省,而發佈出來,是由於以爲你們也可能會從中找到感興趣的地方。算法

1、糟糕程序員的跡象

1.  沒法對代碼進行推理

對代碼進行推理意味着能跟隨代碼的執行路徑(「在腦子裏運行程序」),同時清楚地知道代碼執行的目標。數據庫

特徵編程

  1. 程序裏有「巫毒代碼( voodoo code )」;存在對程序目標毫無益處的代碼,但卻仍然勤勉地維護它們(例如,初始化歷來不用的變量、調用和目標絕不相關的函數、生成用不着的輸出,等等)。(譯者:巫毒代碼應該就是隱藏危險的代碼,不知道何時就會給程序形成危害,就像「巫毒術」。)
  2. 屢次執行冪等函數(例如:屢次調用 save() 函數「只是爲了確保無誤」)。(譯者:冪等函數,或冪等方法,是指可使用相同參數重複執行,並能得到相同結果的函數。這些函數不會影響系統狀態,也不用擔憂重複執行會對系統形成改變。)
  3. 經過重寫錯誤代碼的結果來修復程序 bug。
  4. 「溜溜球式代碼( Yo-Yo code )」就是將一個值轉換成另外一種不一樣的格式,而後再轉換回到最初的格式(例如:將一個小數轉換成一個字符串,而後再轉回成小數;或是填充一個字符串,而後再裁剪它)。
  5. 「推土機式代碼( Bulldozer code )」將大塊代碼分解成多個子程序,看起來像是重構,但不可能在其餘環境下重用(耦合度過高)。

補救措施小程序

程序猿能夠經過實踐來克服這個缺點,若是 IDE 自帶的調試器能單步調試,就把它做爲助手使用。好比說在 Visual Studio 裏,這就意味着要在問題區域的起始處打上斷點,而後按下‘ F11 ’單步調試,查看變量的值(變化先後都要查看),直到你明白了代碼正在作什麼。若是你的目標環境不具有這種特性,那就找一個擁有這種特性的環境去實踐。數組

這麼作的目的是,讓你作到再也不須要調試器就能在腦子裏跟隨代碼的流程,並且有足夠的耐心去思考代碼正在對整個程序的狀態作什麼。這麼作的好處就是可以識別出冗餘且無用的代碼,並且不須要從頭執行整個路徑就能在當前代碼中找出 bug。瀏覽器

 

2.難以理解語言的編程模型

面向對象編程( Object Oriented Programming )就是一種語言模型,正如函數式編程( Functional programming )或聲明式編程( Declarative programming )同樣。它們每個都和過程式或命令式編程有着顯著不一樣,就像過程式編程明顯不一樣於彙編或基於 GOTO 的編程。此外,雖然有不少語言都跟隨同一個主流編程模型(如面向對象的編程),但它們都只介紹本身的改進,例如遞推式構造列表( list comprehensions )、泛型( generics )、鴨式分類( duck-typing )等等。緩存

譯者:duck-typing 是動態語言的一種程序設計風格,用以實踐方法多態。Duck-typing 並不關注對象的實際類型,而是關注其表現。概念提出者 James Whitcomb Riley 這樣描述這個風格:當看到一隻鳥走起來像鴨子,遊起泳來像鴨子,叫起來也像鴨子,那這隻鳥就能夠看出是鴨子。安全

特徵

  1. 使用任何所需的語法來擺脫模型的束縛,接着用他們熟悉的語言風格來完成程序的剩餘部分。
  2. (面向對象編程)試圖在未實例化的類中調用非靜態的函數或變量,而且沒法理解爲何這樣不能編譯。
  3. (面向對象編程)寫了大量「 xxxxxManager 」這樣的類,類中包含全部控制對象字段的方法,而這些對象自己幾乎沒有定義方法。
  4. (關聯式編程)把關聯式數據庫看成對象倉庫,在客戶代碼中執行全部的聯結( joins )和關係約束( relation enforcement )。
  5. (函數式編程)爲了處理不一樣類型的輸入或運算符,對同一個算法建立多個版本實現,而不是向一個泛型實現傳入高級函數。
  6. (函數式編程)非要在能自動緩存的平臺上手動緩存肯定性函數的結果(好比 SQL 和 HasKell)。(譯者:肯定性函數就是在輸入特定的值集合時,調用函數獲得相同的結果。HasKell 是一種純函數式編程語言。)
  7. 從別人的程序裏剪切粘貼代碼來處理 I/O 和 Monads。(譯者:Monads 是函數式編程中一種表明計算指令的結構,詳見Monad。)
  8. (聲明式編程)在命令式代碼中設置單一值,而不是使用數據綁定( data-binding )。

補救措施

若是你的技能不足,是由於別人教得很差或是本身沒學好,那編譯器自身就是一位備選老師。學習一個新的編程模型,最有效的 辦法莫過於建立一個新工程,無論都有哪些新的構造方法,強迫本身去使用它們,不管在工程中的使用是否明智。你也須要練習用本身最熟悉且通俗易懂的措辭來解 釋模型特性,而後遞歸地建立本身的新詞彙表,直到你對模型理解入微。舉個例子:

階段一:「OOP 就是方法的集合」

階段二:「OOP 裏的方法就是函數,它們運行在自帶全局變量的小程序中」

階段三:「全局變量被稱爲字段,其中有些是私有字段,在小程序外不可見」

階段四:「擁有私有和公有元素是爲了隱藏實現細節,暴露乾淨整潔的接口,這就叫封裝」

階段五:「封裝意味着實現細節不會破壞業務邏輯」

對全部編程語言來講階段五看起來都同樣,由於全部語言在階段五都試圖讓程序猿能表達出程序的意圖,而不須要將其隱藏在如何實現的細節之中。拿函數式編程再舉個例子:

  • 階段一:「函數式編程作的全部事情就是將肯定性函數連接在一塊兒」
  • 階段二:「當函數是肯定的,編譯器就可以預測何時能夠緩存結果或跳過求值,甚至在何時提早停止求值是安全的」
  • 階段三:「爲了支持惰性求值( Lzay Evaluation )和部分求值( Partial Evaluation ),編譯器要求函數定義如何轉換一個單一參數,甚至有時要將其轉換成另外一個函數。這就叫函數柯里化( Currying )」
  • 階段四:「有的時候編譯器能夠替咱們進行函數柯里化( Currying )」
  • 階段五:「讓編譯器搞清楚普通細節,我就能夠經過描述我想要什麼來寫程序,而不是告訴它怎麼給我結果」

3.缺少研究技巧/長期缺少對平臺特性的瞭解

現在,現代語言和框架都帶有很是了不得的內置命令和特性,一些主要的框架(像 Java 、 . Net 、 Cocoa)因爲自己結構龐大,任何一個程序猿(甚至是一個很優秀的程序猿)都要花費好幾year時間去學習。可是,一個優秀的程序猿在本身開始構造所需函數以前,會先搜索有沒有知足需求的內置函數。而傑出的程序猿們則可以分解並識別出任務中的抽象問題,接着在實際開始設計程序以前,去搜索適用的現有框架、模式、模型和語言。

特徵

若是在應該掌握新平臺好久之後,這些特徵還繼續出現,那它們就暗示着存在問題。

從新發明或作一些費勁繁雜的工做來實現某種功能,而不使用語言內置的基礎機制,如事件-處理機制(events-and-handlers)或正則表達式。
    從新發明框架的內置類和函數(好比定時器、數據集合、排序和搜索算法)。
    在幫助論壇上發佈這樣的信息「把代碼發到個人郵箱,謝謝」。
    用不少條指令來實現「冗餘代碼」,實際上能夠簡單得多(好比:把一個小數轉換成格式化字符串來取整,而後再把這個字符串轉回成小數)。
    堅持使用過期的技術,即使在那些狀況下使用新技術更佳(好比:還在寫命名委託函數,而不用lambda表達式)。
    有一個很刻板的「溫馨區( comfort zone )」,不顧一切地使用原語來解決複雜問題。

譯者:「 comfort zone 」就是令人感到安全、舒服或在其掌控之下的形式或狀態。

也會偶爾複製代碼,複製的頻率和框架大小成比例,所以,按本身的程度來判斷吧。手寫鏈表的人也許知道本身正在作什麼,但手寫 StrCpy() 的人可能就不知道了。

補救措施

一個程序猿若是不放慢速度,就不可能學到這類知識。並且頗有可能,這我的一直都在火急火燎地用任何須要的手段讓每一個函數 都工做起來。他須要在手邊放一本平臺的技術參考手冊,而且可以花最小的代價瀏覽它,這就是說要麼在桌上的鍵盤右邊放一本打印稿,要麼還有一個屏專門用來打 開瀏覽器。爲了開始培養這種習慣,他應該重構舊代碼,目標是減小十分之一以上的指令數量。

4.沒法理解指針

若是你不能理解指針,那你能寫的程序類型就很是有限,由於指針的概念創造出了不少複雜的數據結構和有效的 APIs。託管類語言使用引用來代替指針,二者很像,但引用增長了自動解引用功能並禁止指針運算,從而消除特定類型的 bug。不管如何,它們仍是很是類似,不能掌握這個概念就會致使數據結構的設計不好勁,而且出現一些因爲不理解方法調用中值傳遞和引用傳遞的區別而致使的 問題。

特徵

  1. 不會實現鏈表;從鏈表或樹中插入/刪除節點時,寫的代碼老是丟失數據。
  2. 憑經驗爲長度可變的集合分配大數組,而且維護一個單獨的集合大小計數器,而不是使用動態數據結構。
  3. 沒法找出或修復由指針運算錯誤致使的 bug。
  4. 對於做爲參數傳遞給函數的指針,修改其指向的值,而且沒有預料到指針指向的對象會在函數外被改變。
  5. 複製指針,經過複製的指針改變其指向的值,而後假設原來的指針仍指向舊值。
  6. 在應該將指針的解引用值序列化時,卻把指針序列化到磁盤或網絡上。
  7. 經過比較指針值來對指針數組排序。

補救措施

「我有一個叫 Joe 的朋友待在賓館的某個房間裏,而我不知道他的房間號。但我知道他的熟人 Frank 待在哪一個房間」,所以我跑去敲門問他‘Joe 在哪一個房間?’,Frank 表示他也不知道,但他知道 Joe 的同事 Theodore 在哪一個房間,並給了我 Theodore 的房間號。所以我又跑到Theodore的房間問 Joe 在哪,Theodore 告訴我 Joe 在414房間。實際上,Joe 就是在那個房間。」

對於指針,能夠用不少種不一樣的隱喻來描述,而數據結構則能夠描述成多種比喻。上面是對鏈表的簡單類比,並且任何人都能發 明本身的版本,即便他們不是程序猿。提到指針你們都能理解,所以,你的描述不會比現有的描述還更全面。當程序猿試圖想象計算機的內存里正在發生什麼,並把 這個想象和他們對普通變量的理解融合時,雖然這二者很類似,但這個時候就會沒法理解。也許將代碼解釋成一個簡單的故事有利於推理當前的情況,直到發現其中 的區別,直到程序猿能夠像面對標量值和數組同樣直觀地想象指針和數據結構。

5.難以看透遞歸

遞歸的思想很容易理解,但程序猿們常常在本身腦子裏想象一次遞歸操做的結果時遇到困難,或想不通一個簡單函數是怎麼計算出複雜結果的。這些不解使得要設計一個遞歸函數變得難上加難,由於當你要對初始條件或遞歸調用的參數進行測試時,你想象不出「當前走到哪一步了」。

特徵

  1. 對問題設計極其複雜的迭代算法,但其實能夠經過遞歸解決(好比:遍歷一個文件系統樹),尤爲是在不用保證內存和性能的狀況下。
  2. 遞歸函數在遞歸調用先後都會檢查相同的初始條件。
  3. 遞歸函數沒有測試初始條件。
  4. 遞歸子程序鏈接到一個全局變量或支持輸出的變量上,或者累計這些變量的和。
  5. 對於遞歸調用中要傳遞什麼參數表現出明顯的困惑,或是不理解傳遞未修改參數的遞歸調用。
  6. 認爲迭代的次數會被做爲參數傳遞。

補救措施

先體會一下,準備好迎接某種堆棧溢出吧。首先,在代碼裏只寫一個初始條件檢測並只調用一次遞歸,遞歸中使用同一個被傳遞 的未修改參數。即便你以爲寫得不夠好也要停下來,不管如何,讓代碼運行一下。它拋出了一個堆棧溢出的異常,那麼如今返回去繼續寫,在遞歸調用中傳遞參數的 已修改拷貝。產生了更多的堆棧溢出錯誤?輸出過分?那就接着反覆修改代碼再運行,從修改初始條件測試轉向修改遞歸調用,直到你開始憑直覺就知道函數怎麼轉 換它的輸入參數。忍住衝動,使用的初始條件測試或遞歸調用不要超過一次,除非你真的知道本身在作什麼

你的目標是敢於進行遞歸調用,即便在這條想象中的遞歸路徑上,你沒有徹底搞清楚「本身在哪裏」。那麼,等你須要爲一個真正的項目去寫一個函數時,你會從寫單元測試開始,而且運用上面提到的相同技術來一步步推動。

6.不信任代碼

特徵

  1. 寫這樣的函數:IsNull() 和 IsNotNull(), 或 IsTrue(bool) 和 IsFalse(bool)。
  2. 檢查一個布爾變量會不會出現除了 true 或 false 之外的值。

補救措施

別人是按代碼行數付錢給你嗎?這些舊習慣是否是你從一個擁有弱類型體系的語言中延續下來的?若是兩種都不是,那這種狀況 就相似於「沒法推理代碼」,可是彷佛不是推理能力受損,而是沒法信任和適應編程語言。有些特徵更像是經不起邏輯分析的「comfort code」,但程序猿非要強迫本身這麼寫。惟一的補救措施就是,多花時間熟悉編程語言。(譯者:由於不熟悉因此感到不肯定,所以非要寫這樣的代碼才能安心。)

相關文章
相關標籤/搜索