表明你是一個糟糕程序員的各種跡象
為什么寫這篇文章?
本文提到的絕大多數錯誤,都是作者歷經一番艱辛才得以發現,要么是因為自己犯過,要么是在別人的工作中見過。
本文并非意圖對程序員劃分等級 ,只是適合某些程序員閱讀,他們相信自己有能力判斷一件事情在什么情況下是不良習慣的跡象,在什么情況下則是特殊環境導致的結果。
寫這篇文章是為了迫使作者自省,而發布出來是因為覺得大家也可能會從中找到感興趣的地方。
一、糟糕程序員的跡象
1. 無法對代碼進行推理
對代碼進行推理意味著能跟隨代碼的執行路徑(“在腦子里運行程序”),同時清楚地知道代碼執行的目標。
特征
- 程序里有“巫毒代碼( voodoo code )”;存在對程序目標毫無益處的代碼,但卻仍然勤勉地維護它們(例如,初始化從來不用的變量、調用和目標毫不相關的函數、生成用不著的輸出,等等)。 (譯者:巫毒代碼應該就是隱藏危險的代碼,不知道什么時候就會給程序造成危害,就像“巫毒術”。)
- 多次執行冪等函數(例如:多次調用 save() 函數“只是為了確保無誤”)。 (譯者:冪等函數,或冪等方法,是指可以使用相同參數重復執行,并能獲得相同結果的函數。這些函數不會影響系統狀態,也不用擔心重復執行會對系統造成改變。)
- 通過重寫錯誤代碼的結果來修復程序 bug。
- “溜溜球式代碼( Yo-Yo code )”就是將一個值轉換成另一種不同的格式,然后再轉換回到最初的格式(例如:將一個小數轉換成一個字符串,然后再轉回成小數;或是填充一個字符串,然后再裁剪它)。
- “推土機式代碼( 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 這樣描述這個風格:當看到一只鳥走起來像鴨子,游起泳來像鴨子,叫起來也像鴨子,那這只鳥就可以看出是鴨子。
特征
- 使用任何所需的語法來擺脫模型的束縛,接著用他們熟悉的語言風格來完成程序的剩余部分。
- (面向對象編程)試圖在未實例化的類中調用非靜態的函數或變量,并且無法理解為什么這樣不能編譯。
- (面向對象編程)寫了大量“ xxxxxManager ”這樣的類,類中包含所有控制對象字段的方法,而這些對象本身幾乎沒有定義方法。
- (關聯式編程)把關聯式數據庫當作對象倉庫,在客戶代碼中執行所有的聯結( joins )和關系約束( relation enforcement )。
- (函數式編程)為了處理不同類型的輸入或運算符,對同一個算法創建多個版本實現,而不是向一個泛型實現傳入高級函數。
- (函數式編程)非要在能自動緩存的平臺上手動緩存確定性函數的結果(比如 SQL 和 HasKell)。 (譯者:確定性函數就是在輸入特定的值集合時,調用函數得到相同的結果。HasKell 是一種純函數式編程語言。)
- 從別人的程序里剪切粘貼代碼來處理 I/O 和 Monads。 (譯者:Monads 是函數式編程中一種代表計算指令的結構,詳見 Monad 。)
- (聲明式編程)在命令式代碼中設置單一值,而不是使用數據綁定( data-binding )。
補救措施
如果你的技能不足,是因為別人教得不好或是自己沒學好,那編譯器自身就是一位備選老師。學習一個新的編程模型,最有效的辦法莫過于創建一個新工 程,不管都有哪些新的構造方法,強迫自己去使用它們,無論在工程中的使用是否明智。你也需要練習用自己最熟悉且通俗易懂的措辭來解釋模型特性,然后遞歸地 創建自己的新詞匯表,直到你對模型理解入微。舉個例子:
階段一:“OOP 就是方法的集合”
階段二:“OOP 里的方法就是函數,它們運行在自帶全局變量的小程序中”
階段三:“全局變量被稱為字段,其中有些是私有字段,在小程序外不可見”
階段四:“擁有私有和公有元素是為了隱藏實現細節,暴露干凈整潔的接口,這就叫封裝”
階段五:“封裝意味著實現細節不會破壞業務邏輯”
對所有編程語言來說階段五看起來都一樣,因為所有語言在階段五都試圖讓程序猿能表達出程序的 意圖 ,而不需要將其隱藏在 如何 實現的細節之中。拿函數式編程再舉個例子:
- 階段一:“函數式編程做的所有事情就是將確定性函數鏈接在一起”
- 階段二:“當函數是確定的,編譯器就能夠預測什么時候可以緩存結果或跳過求值,甚至在什么時候提前中止求值是安全的”
- 階段三:“為了支持惰性求值( Lzay Evaluation )和部分求值( Partial Evaluation ),編譯器要求函數定義如何轉換一個單一參數,甚至有時要將其轉換成另一個函數。這就叫函數柯里化( Currying )”
- 階段四:“有的時候編譯器可以替我們進行函數柯里化( Currying )”
- 階段五:“讓編譯器搞清楚普通細節,我就可以通過描述我想要 什么 來寫程序,而不是告訴它 怎么 給我結果”
3.缺乏研究技巧/長期缺乏對平臺特性的了解
如今,現代語言和框架都帶有非常了不起的內置命令和特性,一些主要的框架(像 Java 、 . Net 、 Cocoa)由于本身結構龐大,任何一個程序猿(甚至是一個很優秀的程序猿)都要花費好幾年時間去學習。但是,一個優秀的程序猿在自己開始構造所需函數之 前,會先搜索有沒有滿足需求的內置函數。而杰出的程序猿們則能夠分解并識別出任務中的抽象問題,接著在實際開始設計程序之前,去搜索適用的現有框架、模 式、模型和語言。
特征
如果在應該掌握新平臺很久以后,這些特征還繼續出現,那它們就暗示著存在問題。
- 重新發明或做一些費勁繁雜的工作來實現某種功能,而不使用語言內置的基礎機制,如事件-處理機制(events-and-handlers)或正則表達式。
- 重新發明框架的內置類和函數(比如定時器、數據集合、排序和搜索算法)。
- 在幫助論壇上發布這樣的信息“把代碼發到我的郵箱,謝謝”。
- 用很多條指令來實現“冗余代碼”,實際上可以簡單得多(比如:把一個小數轉換成格式化字符串來取整,然后再把這個字符串轉回成小數)。
- 堅持使用過時的技術,即便在那些情況下使用新技術更佳(比如:還在寫命名委托函數,而不用lambda表達式)。
- 有一個很刻板的“舒適區( comfort zone )”,不顧一切地使用原語來解決復雜問題。
譯者:“ comfort zone ”就是使人感到安全、舒服或在其掌控之下的形式或狀態。
也會偶爾復制代碼,復制的頻率和框架大小成比例,因此,按自己的程度來判斷吧。手寫鏈表的人 也許知道自己正在做什么 ,但手寫 StrCpy() 的人可能就不知道了。
補救措施
一個程序猿如果不放慢速度,就不可能學到這類知識。而且很有可能,這個人一直都在火急火燎地用任何需要的手段讓每個函數都工作起來。他需要在手 邊放一本平臺的技術參考手冊,并且能夠花最小的代價瀏覽它,這就是說要么在桌上的鍵盤右邊放一本打印稿,要么還有一個屏專門用來打開瀏覽器。為了開始培養 這種習慣,他應該重構舊代碼,目標是減少十分之一以上的指令數量。
4.無法理解指針
如果你不能理解指針,那你能寫的程序類型就非常有限,因為指針的概念創造出了很多復雜的數據結構和有效的 APIs。托管類語言使用引用來代替指針,兩者很像,但引用增加了自動解引用功能并禁止指針運算,從而消除特定類型的 bug。無論如何,它們還是非常相似,不能掌握這個概念就會導致數據結構的設計很差勁,并且出現一些由于不理解方法調用中值傳遞和引用傳遞的區別而導致的 問題。
特征
- 不會實現鏈表;從鏈表或樹中插入/刪除節點時,寫的代碼總是丟失數據。
- 憑經驗為長度可變的集合分配大數組,并且維護一個單獨的集合大小計數器,而不是使用動態數據結構。
- 無法找出或修復由指針運算錯誤導致的 bug。
- 對于作為參數傳遞給函數的指針,修改其指向的值,并且沒有預料到指針指向的對象會在函數外被改變。
- 復制指針,通過復制的指針改變其指向的值,然后假設原來的指針仍指向舊值。
- 在應該將指針的解引用值序列化時,卻把指針序列化到磁盤或網絡上。
- 通過比較指針值來對指針數組排序。
補救措施
“我有一個叫 Joe 的朋友待在賓館的某個房間里,而我不知道他的房間號。但我知道他的熟人 Frank 待在哪個房間”,因此我跑去敲門問他‘Joe 在哪個房間?’,Frank 表示他也不知道,但他知道 Joe 的同事 Theodore 在哪個房間,并給了我 Theodore 的房間號。因此我又跑到Theodore的房間問 Joe 在哪,Theodore 告訴我 Joe 在414房間。實際上,Joe 就是在那個房間。”
對于指針,可以用很多種不同的隱喻來描述,而數據結構則可以描述成多種比喻。上面是對鏈表的簡單類比,而且任何人都能發明自己的版本,即使他們 不是程序猿。提到指針大家都能理解,因此,你的描述不會比現有的描述還更全面。當程序猿試圖想象計算機的內存里正在發生什么,并把這個想象和他們對普通變 量的理解融合時,雖然這兩者很相似,但這個時候就會無法理解。也許將代碼解釋成一個簡單的故事有利于推理當前的狀況,直到發現其中的區別,直到程序猿可以 像面對標量值和數組一樣直觀地想象指針和數據結構。
5.難以看透遞歸
遞歸的思想很容易理解,但程序猿們經常在自己腦子里想象一次遞歸操作的結果時遇到困難,或想不通一個簡單函數是怎么計算出復雜結果的。這些不解使得要設計一個遞歸函數變得難上加難,因為當你要對初始條件或遞歸調用的參數進行測試時,你想象不出“當前走到哪一步了”。
特征
- 對問題設計極其復雜的迭代算法,但其實可以通過遞歸解決(比如:遍歷一個文件系統樹),尤其是在不用保證內存和性能的情況下。
- 遞歸函數在遞歸調用前后都會檢查相同的初始條件。
- 遞歸函數沒有測試初始條件。
- 遞歸子程序連接到一個全局變量或支持輸出的變量上,或者累計這些變量的和。
- 對于遞歸調用中要傳遞什么參數表現出明顯的困惑,或是不理解傳遞未修改參數的遞歸調用。
- 認為迭代的次數會被作為參數傳遞。
補救措施
先體會一下,準備好迎接某種堆棧溢出吧。首先,在代碼里只寫一個初始條件檢測并只調用一次遞歸,遞歸中使用同一個被傳遞的未修改參數。即使你覺得寫得不夠 好也要停下來,無論如何,讓代碼運行一下。它拋出了一個堆棧溢出的異常,那么現在返回去繼續寫,在遞歸調用中傳遞參數的已修改拷貝。產生了更多的堆棧溢出 錯誤?輸出過度?那就接著反復修改代碼再運行,從修改初始條件測試轉向修改遞歸調用,直到你開始憑直覺就知道函數怎么轉換它的輸入參數。忍住沖動,使用的 初始條件測試或遞歸調用不要超過一次,除非你真的 知道自己在做什么 。
你的目標是勇于進行遞歸調用,即使在這條想象中的遞歸路徑上,你沒有完全搞清楚“自己在哪里”。那么,等你需要為一個真正的項目去寫一個函數時,你會從寫單元測試開始,并且運用上面提到的相同技術來一步步推進。
6.不信任代碼
特征
- 寫這樣的函數:IsNull() 和 IsNotNull(), 或 IsTrue(bool) 和 IsFalse(bool)。
- 檢查一個布爾變量會不會出現除了 true 或 false 以外的值。
補救措施
別人是按代碼行數付錢給你嗎?這些舊習慣是不是你從一個擁有弱類型體系的語言中延續下來的?如果兩種都不是,那這種情況就類似于“無法推理代碼”,但是似 乎不是推理能力受損,而是無法信任和適應編程語言。有些特征更像是經不起邏輯分析的“comfort code”,但程序猿非要強迫自己這么寫。唯一的補救措施就是,多花時間熟悉編程語言。 (譯者:因為不熟悉所以感到不確定,因此非要寫這樣的代碼才能安心。)