如何編寫無法維護的程式碼
讓自己穩拿鐵飯碗 ;-)
— Roedy Green(翻譯版略有刪節)
簡介
永遠不要(把自己遇到的問題)歸因於(他人的)惡意,這恰恰說明了(你自己的)無能。 — 拿破崙
為了造福大眾,在Java程式設計領域創造就業機會,兄弟我在此傳授大師們的祕籍。這些大師寫的程式碼極其難以維護,後繼者就是想對它做最簡單的修改都需要花上數年時間。而且,如果你能對照祕籍潛心修煉,你甚至可以給自己弄個鐵飯碗,因為除了你之外,沒人能維護你寫的程式碼。再而且,如果你能練就祕籍中的全部招式,那麼連你自己都無法維護你的程式碼了!
(伯樂線上配圖)
你不想練功過度走火入魔吧。那就不要讓你的程式碼一眼看去就完全無法維護,只要它實質上是那樣就行了。否則,你的程式碼就有被重寫或重構的風險!
總體原則
Quidquid latine dictum sit, altum sonatur.
(隨便用拉丁文寫點啥都會顯得高大上。)
想挫敗維護程式碼的程式設計師,你必須先明白他的思維方式。他接手了你的龐大程式,沒有時間把它全部讀一遍,更別說理解它了。他無非是想快速找到修改程式碼的位置、改程式碼、編譯,然後就能交差,並希望他的修改不會出現意外的副作用。
他檢視你的程式碼不過是管中窺豹,一次只能看到一小段而已。你要確保他永遠看不到全貌。要儘量讓他難以找到他想找的程式碼。但更重要的是,要讓他不能有把握忽略任何東西。
程式設計師都被程式設計慣例洗腦了,還為此自鳴得意。每一次你處心積慮地違背程式設計慣例,都會迫使他必須用放大鏡去仔細閱讀你的每一行程式碼。
你可能會覺得每個語言特性都可以用來讓程式碼難以維護,其實不然。你必須精心地誤用它們才行。
命名
“當我使用一個單詞的時候” Humpty Dumpty 曾經用一種輕蔑的口氣說, “它就是我想表達的意思,不多也不少。“
– Lewis Carroll — 《愛麗絲魔鏡之旅》, 第6章
編寫無法維護程式碼的技巧的重中之重是變數和方法命名的藝術。如何命名是和編譯器無關的。這就讓你有巨大的自由度去利用它們迷惑維護程式碼的程式設計師。
妙用 寶寶起名大全
- 買本寶寶起名大全,你就永遠不缺變數名了。比如 Fred 就是個好名字,而且鍵盤輸入它也省事。如果你就想找一些容易輸入的變數名,可以試試 adsf 或者 aoeu之類。
單字母變數名
- 如果你給變數起名為a,b,c,用簡單的文字編輯器就沒法搜尋它們的引用。而且,沒人能猜到它們的含義。
創造性的拼寫錯誤
- 如果你必須使用描述性的變數和函式名,那就把它們都拼錯。還可以把某些函式和變數名拼錯,再把其他的拼對(例如 SetPintleOpening 和 SetPintalClosing) ,我們就能有效地將grep或IDE搜尋技術玩弄於股掌之上。這招超級管用。還可以混淆不同語言(比如colour — 英國英語,和 color — 美國英語)。
抽象
- 在命名函式和變數的時候,充分利用抽象單詞,例如 it, everything, data, handle, stuff, do, routine, perform 和數字,像這樣命名的好例子有 routineX48, PerformDataFunction, DoIt, HandleStuff還有 do_args_method。
首字母大寫的縮寫
- 用首字母大寫縮寫(比如GNU 代表 GNU’s Not Unix) 使程式碼簡潔難懂。真正的漢子(無論男女)從來不說明這種縮寫的含義,他們生下來就懂。
辭典大輪換
- 為了打破沉悶的程式設計氣氛,你可以用一本辭典來查詢儘量多的同義詞。例如 display, show, present。在註釋裡含糊其辭地暗示這些命名之間有細微的差別,其實根本沒有。不過,如果有兩個命名相似的函式真的有重大差別,那倒是一定要確保它們用相同的單詞來命名(例如,對於 “寫入檔案”, “在紙上書寫” 和 “螢幕顯示” 都用 print 來命名)。 在任何情況下都不要屈服於編寫明確的專案詞彙表這種無理要求。你可以辯解說,這種要求是一種不專業的行為,它違反了結構化設計的資訊隱藏原則。
首字母大寫
- 隨機地把單詞中間某個音節的首字母大寫。例如 ComputeReSult()。
重用命名
- 在語言規則允許的地方,儘量把類、構造器、方法、成員變數、引數和區域性變數都命名成一樣。更高階的技巧是在{}塊中重用區域性變數。這樣做的目的是迫使維護程式碼的程式設計師認真檢查每個例項的作用域。特別是在Java程式碼中,可以把普通方法偽裝成構造器。
使用非英語字母
- 在命名中偷偷使用不易察覺的非英語字母,例如
1 |
typedef struct { int i; } ínt; |
看上去沒啥不對是吧?嘿嘿嘿…這裡的第二個 ínt 的 í 實際上是東北歐字母,並不是英語中的 i 。在簡單的文字編輯器裡,想看出這一點點區別幾乎是不可能的。
巧妙利用編譯器對於命名長度的限制
- 如果編譯器只區分命名的前幾位,比如前8位,那麼就把後面的字母寫得不一樣。比如,其實是同一個變數,有時候寫成 var_unit_update() ,有時候又寫成 var_unit_setup(),看起來是兩個不同的函式呼叫。而在編譯的時候,它們其實是同一個變數 var_unit。
下劃線,真正的朋友
- 可以拿 _ 和 __ 作為標示符。
混合多語言
- 隨機地混用兩種語言(人類語言或計算機語言都行)。如果老闆要求使用他指定的語言,你就告訴他你用自己的語言更有利於組織你的思路,萬一這招不管用,就去控訴這是語言歧視,並威脅起訴老闆要求鉅額精神損失賠償。
擴充套件 ASCII 字元
- 擴充套件 ASCII 字元用於變數命名是完全合法的,包括 ß, Ð, 和 ñ 等。在簡單的文字編輯器裡,除了拷貝/貼上,基本上沒法輸入。
其他語言的命名
- 使用外語字典作為變數名的來源。例如,可以用德語單詞 punkt 代替 point。除非維護程式碼的程式設計師也像你一樣熟練掌握了德語. 不然他就只能盡情地在程式碼中享受異域風情了。
數學命名
- 用數學操作符的單詞來命名變數。例如:
1 |
openParen = (slash + asterix) / equals; |
- (左圓括號 = (斜槓 + 星號)/等號;)
令人眩暈的命名
- 用帶有完全不相關的感情色彩的單詞來命名變數。例如:
1 |
marypoppins = (superman + starship) / god; |
- (歡樂滿人間 = (超人 + 星河戰隊)/上帝;)
- 這一招可以讓閱讀程式碼的人陷入迷惑之中,因為他們在試圖想清楚這些命名的邏輯時,會不自覺地聯絡到不同的感情場景裡而無法自拔。
何時使用 i
- 永遠不要把 i 用作最內層的迴圈變數。 用什麼命名都行,就是別用i。把 i 用在其他地方就隨便了,用作非整數變數尤其好。
慣例 — 明修棧道,暗度陳倉
忽視 Java 編碼慣例,Sun 自己就是這樣做的。幸運的是,你違反了它編譯器也不會打小報告。這一招的目的是搞出一些在某些特殊情況下有細微差別的名字來。如果你被強迫遵循駝峰法命名,你還是可以在某些模稜兩可的情況下顛覆它。例如,inputFilename 和 inputfileName 兩個命名都可以合法使用。在此基礎上自己發明一套複雜到變態的命名慣例,然後就可以對其他人反咬一口,說他們違反了慣例。
小寫的 l 看上去很像數字 1
- 用小寫字母 l 標識 long 常數。例如 10l 更容易被誤認為是 101 而不是 10L 。 禁用所有能讓人準確區分 uvw wW gq9 2z 5s il17|!j oO08 `'” ;,. m nn rn {[()]} 的字型。要做個有創造力的人。
把全域性命名重用為私有
- 在A 模組裡宣告一個全域性陣列,然後在B 模組的標頭檔案裡再宣告一個同名的私有陣列,這樣看起來你在B 模組裡引用的是那個全域性陣列,其實卻不是。不要在註釋裡提到這個重複的情況。
誤導性的命名
- 讓每個方法都和它的名字蘊含的功能有一些差異。例如,一個叫 isValid(x)的方法在判斷完引數x的合法性之後,還順帶著把它轉換成二進位制並儲存到資料庫裡。
偽裝
當一個bug需要越長的時間才會暴露,它就越難被發現。– Roedy Green(本文作者)
編寫無法維護程式碼的另一大祕訣就是偽裝的藝術,即隱藏它或者讓它看起來像其他東西。很多招式有賴於這樣一個事實:編譯器比肉眼或文字編輯器更有分辨能力。下面是一些偽裝的最佳招式。
把程式碼偽裝成註釋,反之亦然
- 下面包括了一些被註釋掉的程式碼,但是一眼看去卻像是正常程式碼。
如果不是用單綠色標出來,你能注意到這三行程式碼被註釋掉了麼?
用連線符隱藏變數
- 對於下面的定義
1 |
#define local_var xy_z |
可以把 “xy_z” 打散到兩行裡:
1 2 |
#define local_var xy\ _z // local_var OK |
這樣全域性搜尋 xy_z 的操作在這個檔案裡就一無所獲了。 對於 C 前處理器來說,第一行最後的 “\” 表示繼續拼接下一行的內容。
文件
任何傻瓜都能說真話,而要把謊編圓則需要相當的智慧。– Samuel Butler (1835 – 1902)
不正確的文件往往比沒有文件還糟糕。– Bertrand Meyer
既然計算機是忽略註釋和文件的,你就可以在裡邊堂而皇之地編織彌天大謊,讓可憐的維護程式碼的程式設計師徹底迷失。
在註釋中撒謊
- 實際上你不需要主動地撒謊,只要沒有及時保持註釋和程式碼更新的一致性就可以了。
只記錄顯而易見的東西
- 往程式碼裡摻進去類似於
1 |
/* 給 i 加 1 */ |
- 這樣的註釋,但是永遠不要記錄包或者方法的整體設計這樣的乾貨。
記錄 How 而不是 Why
- 只解釋一個程式功能的細節,而不是它要完成的任務是什麼。這樣的話,如果出現了一個bug,修復者就搞不清這裡的程式碼應有的功能。
該寫的別寫
- 比如你在開發一套航班預定系統,那就要精心設計,讓它在增加另一個航空公司的時候至少有25處程式碼需要修改。永遠不要在文件裡說明要修改的位置。後來的開發人員要想修改你的程式碼?門都沒有,除非他們能把每一行程式碼都讀懂。
計量單位
- 永遠不要在文件中說明任何變數、輸入、輸出或引數的計量單位,如英尺、米、加侖等。計量單位對數豆子不是太重要,但在工程領域就相當重要了。同理,永遠不要說明任何轉換常量的計量單位,或者是它的取值如何獲得。要想讓程式碼更亂的話,你還可以在註釋裡寫上錯誤的計量單位,這是赤裸裸的欺騙,但是非常有效。如果你想做一個惡貫滿盈的人,不妨自己發明一套計量單位,用自己或某個小人物的名字命名這套計量單位,但不要給出定義。萬一有人挑刺兒,你就告訴他們,你這麼做是為了把浮點數運算湊成整數運算而進行的轉換。
坑
- 永遠不要記錄程式碼中的坑。如果你懷疑某個類裡可能有bug,天知地知你知就好。如果你想到了重構或重寫程式碼的思路,看在老天爺的份上,千萬別寫出來。切記電影《小鹿斑比》裡那句臺詞 “如果你不能說好聽的話,那就什麼也不要說。”。萬一這段程式碼的原作者看到你的註釋怎麼辦?萬一老闆看到了怎麼辦?萬一客戶看到了怎麼辦?搞不好最後你自己被解僱了。一句”這裡需要修改“的匿名註釋就好多了,尤其是當看不清這句註釋指的是哪裡需要修改的情況下。切記“難得糊塗”四個字,這樣大家都不會感覺受到了批評。
說明變數
- 永遠不要對變數宣告加註釋。有關變數使用的方式、邊界值、合法值、小數點後的位數、計量單位、顯示格式、資料錄入規則等等,後繼者完全可以自己從程式程式碼中去理解和整理嘛。如果老闆強迫你寫註釋,就在方法體裡胡亂多寫點,但絕對不要對變數宣告寫註釋,即使是臨時變數!
在註釋裡挑撥離間
- 為了阻撓任何僱傭外部維護承包商的傾向,可以在程式碼中散佈針對其他同行軟體公司的攻擊和抹黑,特別是可能接替你工作的其中任何一家。例如:
1 2 3 4 5 6 7 8 |
/* 優化後的內層迴圈 這套技巧對於SSI軟體服務公司的那幫蠢材來說太高深了,他們只會 用 <math.h> 裡的笨例程,消耗50倍的記憶體和處理時間。 */ class clever_SSInc { .. . } |
- 可能的話,除了註釋之外,這些攻擊抹黑的內容也要摻到程式碼裡的重要語義部分,這樣如果管理層想清理掉這些攻擊性的言論然後發給外部承包商去維護,就會破壞程式碼結構。
程式設計
編寫無法維護程式碼的基本規則就是:在儘可能多的地方,以儘可能多的方式表述每一個事實。- Roedy Green
- 編寫可維護程式碼的關鍵因素是隻在一個地方表述應用裡的一個事實。如果你的想法變了,你也只在一個地方修改,這樣就能保證整個程式正常工作。所以,編寫無法維護程式碼的關鍵因素就是反覆地表述同一個事實,在儘可能多的地方,以儘可能多的方式進行。令人高興的是,像Java這樣的語言讓編寫這種無法維護程式碼變得非常容易。例如,改變一個被引用很多的變數的型別幾乎是不可能的,因為所有造型和轉換功能都會出錯,而且關聯的臨時變數的型別也不合適了。而且,如果變數值要在螢幕上顯示,那麼所有相關的顯示和資料錄入程式碼都必須一一找到並手工進行修改。類似的還有很多,比如由C和Java組成的Algol語言系列,Abundance甚至Smalltalk對於陣列等結構的處理,都是大有可為的。
Java 造型
- Java的造型機制是上帝的禮物。你可以問心無愧地使用它,因為Java語言本身就需要它。每次你從一個Collection 裡獲取一個物件,你都必須把它造型為原始型別。這樣這個變數的型別就必須在無數地方表述。如果後來型別變了,所有的造型都要修改才能匹配。如果倒黴的維護程式碼的程式設計師沒有找全(或者修改太多),編譯器能不能檢測到也不好說。類似的,如果變數型別從short 變成 int,所有匹配的造型也都要從(short) 改成 (int)。
利用Java的冗餘
- Java要求你給每個變數的型別寫兩次表述。 Java 程式設計師已經習慣了這種冗餘,他們不會注意到你的兩次表述有細微的差別,例如
1 |
Bubblegum b = new Bubblegom(); |
- 不幸的是 ++ 操作符的盛行讓下面這種偽冗餘程式碼得手的難度變大了:
1 |
swimmer = swimner + 1; |
永遠不做校驗
- 永遠不要對輸入資料做任何的正確性或差異性檢查。這樣能表現你對公司裝置的絕對信任,以及你是一位信任所有專案夥伴和系統管理員的團隊合作者。總是返回合理的值,即使資料輸入有問題或者錯誤。
有禮貌,無斷言
- 避免使用 assert() 機制,因為它可能把三天的debug盛宴變成10分鐘的快餐。
避免封裝
- 為了提高效率,不要使用封裝。方法的呼叫者需要所有能得到的外部資訊,以便了解方法的內部是如何工作的。
複製貼上修改
- 以效率的名義,使用 複製+貼上+修改。這樣比寫成小型可複用模組效率高得多。在用程式碼行數衡量你的進度的小作坊裡,這招尤其管用。
使用靜態陣列
- 如果一個庫裡的模組需要一個陣列來存放圖片,就定義一個靜態陣列。沒人會有比512 X 512 更大的圖片,所以固定大小的陣列就可以了。為了最佳精度,就把它定義成 double 型別的陣列。
傻瓜介面
- 編寫一個名為 “WrittenByMe” 之類的空介面,然後讓你的所有類都實現它。然後給所有你用到的Java 內建類編寫包裝類。這裡的思想是確保你程式裡的每個物件都實現這個介面。最後,編寫所有的方法,讓它們的引數和返回型別都是這個 WrittenByMe。這樣就幾乎不可能搞清楚某個方法的功能是什麼,並且所有型別都需要好玩的造型方法。更出格的玩法是,讓每個團隊成員編寫它們自己的介面(例如 WrittenByJoe),程式設計師用到的任何類都要實現他自己的介面。這樣你就可以在大量無意義介面中隨便找一個來引用物件了。
巨型監聽器
- 永遠不要為每個元件建立分開的監聽器。對所有按鈕總是用同一個監聽器,只要用大量的if…else 來判斷是哪一個按鈕被點選就行了。
好事成堆TM
- 狂野地使用封裝和OO思想。例如
1 2 3 4 5 |
myPanel.add( getMyButton() ); private JButton getMyButton() { return myButton; } |
- 這段很可能看起來不怎麼好笑。別擔心,只是時候未到而已。
友好的朋友
- 在C++ 裡儘量多使用friend宣告。再把建立類的指標傳遞給已建立類。現在你不用浪費時間去考慮介面了。另外,你應該用上關鍵字private 和 protected 來表明你的類封裝得很好。
使用三維陣列
- 大量使用它們。用扭曲的方式在陣列之間移動資料,比如,用arrayA裡的行去填充arrayB的列。這麼做的時候,不管三七二十一再加上1的偏移值,這樣很靈。讓維護程式碼的程式設計師抓狂去吧。
混合與匹配
- 存取方法和公共變數神馬的都要給他用上。這樣的話,你無需呼叫存取器的開銷就可以修改一個物件的變數,還能宣稱這個類是個”Java Bean”。對於那些試圖新增日誌函式來找出改變值的源頭的維護程式碼的程式設計師,用這一招來迷惑他尤其有效。
沒有祕密!
- 把每個方法和變數都宣告為 public。畢竟某個人某天可能會需要用到它。一旦方法被宣告為public 了,就很難縮回去。對不?這樣任何它覆蓋到的程式碼都很難修改了。它還有個令人愉快的副作用,就是讓你看不清類的作用是什麼。如果老闆質問你是不是瘋了,你就告訴他你遵循的是經典的透明介面原則。
全堆一塊
- 把你所有的沒用的和過時的方法和變數都留在程式碼裡。畢竟說起來,既然你在1976年用過一次,誰知道你啥時候會需要再用到呢?當然程式是改了,但它也可能會改回來嘛,你”不想要重新發明輪子”(領導們都會喜歡這樣的口氣)。如果你還原封不動地留著這些方法和變數的註釋,而且註釋寫得又高深莫測,甭管維護程式碼的是誰,恐怕都不敢對它輕舉妄動。
就是 Final
- 把你所有的葉子類都宣告為 final。畢竟說起來,你在專案裡的活兒都幹完了,顯然不會有其他人會通過擴充套件你的類來改進你的程式碼。這種情況甚至可能有安全漏洞。 java.lang.String 被定義成 final 也許就是這個原因吧?如果專案組其他程式設計師有意見,告訴他們這樣做能夠提高執行速度。
避免佈局
- 永遠不要用到佈局。當維護程式碼的程式設計師想增加一個欄位,他必須手工調整螢幕上顯示所有內容的絕對座標值。如果老闆強迫你使用佈局,那就寫一個巨型的 GridBagLayout 並在裡面用絕對座標進行硬編碼。
全域性變數,怎麼強調都不過分
- 如果上帝不願意我們使用全域性變數,他就不會發明出這個東西。不要讓上帝失望,儘量多使用全域性變數。每個函式最起碼都要使用和設定其中的兩個,即使沒有理由也要這麼做。畢竟,任何優秀的維護程式碼的程式設計師都會很快搞清楚這是一種偵探工作測試,有利於讓他們從笨蛋中脫穎而出。
再一次說說全域性變數
- 全域性變數讓你可以省去在函式裡描述引數的麻煩。充分利用這一點。在全域性變數中選那麼幾個來表示對其他全域性變數進行操作的型別。
區域性變數
- 永遠不要用區域性變數。在你感覺想要用的時候,把它改成一個例項或者靜態變數,並無私地和其他方法分享它。這樣做的好處是,你以後在其他方法裡寫類似宣告的時候會節省時間。C++程式設計師可以百尺竿頭更進一步,把所有變數都弄成全域性的。
配置檔案
- 配置檔案通常是以 關鍵字 = 值 的形式出現。在載入時這些值被放入 Java 變數中。最明顯的迷惑技術就是把有細微差別的名字用於關鍵字和Java 變數.甚至可以在配置檔案裡定義執行時根本不會改變的常量。引數檔案變數和簡單變數比,維護它的程式碼量起碼是後者的5倍。
子類
- 對於編寫無法維護程式碼的任務來說,物件導向程式設計的思想簡直是天賜之寶。如果你有一個類,裡邊有10個屬性(成員/方法),可以考慮寫一個基類,裡面只有一個屬性,然後產生9層的子類,每層增加一個屬性。等你訪問到最終的子類時,你才能得到全部10個屬性。如果可能,把每個類的宣告都放在不同的檔案裡。
編碼迷局
迷惑 C
- 從網際網路上的各種混亂C 語言競賽中學習,追隨大師們的腳步。
追求極致
- 總是追求用最迷惑的方式來做普通的任務。例如,要用陣列來把整數轉換為相應的字串,可以這麼做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
char *p; switch (n) { case 1: p = "one"; if (0) case 2: p = "two"; if (0) case 3: p = "three"; printf("%s", p); break; } |
一致性的小淘氣
- 當你需要一個字元常量的時候,可以用多種不同格式: ‘ ‘, 32, 0x20, 040。在C或Java裡10和010是不同的數(0開頭的表示8進位制),你也可以充分利用這個特性。
造型
- 把所有資料都以 void * 形式傳遞,然後再造型為合適的結構。不用結構而是通過位移位元組數來造型也很好玩。
巢狀 Switch
- Switch 裡邊還有 Switch,這種巢狀方式是人類大腦難以破解的。
利用隱式轉化
- 牢記程式語言中所有的隱式轉化細節。充分利用它們。陣列的索引要用浮點變數,迴圈計數器用字元,對數字執行字串函式呼叫。不管怎麼說,所有這些操作都是合法的,它們無非是讓原始碼更簡潔而已。任何嘗試理解它們的維護者都會對你感激不盡,因為他們必須閱讀和學習整個關於隱式資料型別轉化的章節,而這個章節很可能是他們來維護你的程式碼之前完全忽略了的。
分號!
- 在所有語法允許的地方都加上分號,例如:
1 2 3 4 5 6 7 |
if(a); else; { int d; d = c; } ; |
使用八進位制數
- 把八進位制數混到十進位制數列表裡,就像這樣:
1 2 3 4 5 6 7 |
array = new int [] { 111, 120, 013, 121, }; |
巢狀
- 儘可能深地巢狀。優秀的程式設計師能在一行程式碼裡寫10層(),在一個方法裡寫20層{}。
C陣列
- C編譯器會把 myArray[i] 轉換成 *(myArray + i),它等同於 *(i + myArray) 也等同於 i[myArray]。 高手都知道怎麼用好這個招。可以用下面的函式來產生索引,這樣就把程式碼搞亂了:
1 2 3 |
int myfunc(int q, int p) { return p%q; } ... myfunc(6291, 8)[Array]; |
遺憾的是,這一招只能在本地C類裡用,Java 還不行。
放長線釣大魚
- 一行程式碼裡堆的東西越多越好。這樣可以省下臨時變數的開銷,去掉換行和空格還可以縮短原始檔大小。記住,要去掉運算子兩邊的空格。優秀的程式設計師總是能突破某些編輯器對於255個字元行寬的限制。
異常
- 在這裡我要向你傳授一個程式設計領域裡鮮為人知的祕訣。異常是個討厭的東西。良好的程式碼永遠不會出錯,所以異常實際上是不必要的。不要把時間浪費在這上面。子類異常是給那些知道自己程式碼會出錯的低能兒用的。在整個應用裡,你只用在main()裡放一個try/catch,裡邊直接呼叫 System.exit()就行了。在每個方法頭要貼上標準的丟擲集合定義,至於會不會丟擲異常你就甭管了。
使用異常的時機
- 在非異常條件下才要使用異常。比如終止迴圈就可以用 ArrayIndexOutOfBoundsException。還可以從異常裡的方法返回標準的結果。
狂熱奔放地使用執行緒
- 如題。
測試
在程式裡留些bug,讓後繼的維護程式碼的程式設計師能做點有意思的事。精心設計的bug是無跡可尋的,而且誰也不知道它啥時候會冒出來。要做到這一點,最簡單的辦法的就是不要測試程式碼。
永不測試
- 永遠不要測試負責處理錯誤、當機或作業系統故障的任何程式碼。反正這些程式碼永遠也不會執行,只會拖累你的測試。還有,你怎麼可能測試處理磁碟錯誤、檔案讀取錯誤、作業系統崩潰這些型別的事件呢?為啥你要用特別不穩定的計算機或者用測試腳手架來模擬這樣的環境?現代化的硬體永遠不會崩潰,誰還願意寫一些僅僅用於測試的程式碼?這一點也不好玩。萬一將來出了事使用者抱怨,你就怪到作業系統或者硬體頭上。他們永遠不會知道真相的。
永遠不要做效能測試
- 嘿,如果軟體執行不夠快,只要告訴客戶買個更快的機器就行了。如果你真的做了效能測試,你可能會發現一個瓶頸,這會導致修改演算法,然後導致整個產品要重新設計。誰想要這種結果?而且,在客戶那邊發現效能問題意味著你可以免費到外地旅遊。你只要備好護照和最新照片就行了。
永遠不要寫任何測試用例
- 永遠不要做程式碼覆蓋率或路徑覆蓋率測試。自動化測試是給那些窩囊廢用的。搞清楚哪些特性佔到你的例程使用率的90%,然後把90%的測試用在這些路徑上。畢竟說起來,這種方法可能只測試到了大約你程式碼的60%,這樣你就節省了40%的測試工作。這能幫助你趕上專案後端的進度。等到有人發現所有這些漂亮的“市場特性”不能正常工作的時候,你早就跑路了。一些有名的大軟體公司就是這樣測試程式碼的,所以你也應該這樣做。如果因為某種原因你還沒走,那就接著看下一節。
測試是給懦夫用的
- 勇敢的程式設計師會跳過這個步驟。太多程式設計師害怕他們的老闆,害怕丟掉工作,害怕客戶的投訴郵件,害怕遭到起訴。這種恐懼心理麻痺了行動,降低了生產率。有科學研究成果表明,取消測試階段意味著經理有把握能提前確定交付時間,這對於規劃流程顯然是有利的。消除了恐懼心理,創新和實驗之花就隨之綻放。程式設計師的角色是生產程式碼,除錯工作完全可以由技術支援和遺留程式碼維護組通力合作來進行。
如果我們對自己的程式設計能力有充分信心,那麼測試就沒有必要了。如果我們邏輯地看待這個問題,隨便一個傻瓜都能認識到測試根本都不是為了解決技術問題,相反,它是一種感性的信心問題。針對這種缺乏信心的問題,更有效的解決辦法就是完全取消測試,送我們的程式設計師去參加自信心培訓課程。畢竟說起來,如果我們選擇做測試,那麼我們就要測試每個程式的變更,但其實我們只需要送程式設計師去一次建立自信的培訓課就行了。很顯然這麼做的成本收益是相當可觀的。
程式語言的選擇
計算機語言正在逐步進化,變得更加傻瓜化。使用最新的語言算什麼好漢?儘可能堅持使用你會用的最老的語言,先考慮用穿孔紙帶,不行就用匯編,再不行用FORTRAN 或者 COBOL,再不行就用C 還有 BASIC,實在不行再用 C++。
FORTRAN
- 用 FORTRAN 寫所有的程式碼。如果老闆問你為啥,你可以回答說它有很多非常有用的庫,你用它可以節約時間。不過,用 FORTRAN 寫出可維護程式碼的概率是 0,所以,要達到不可維護程式碼程式設計指南里的要求就容易多了。
用 ASM
- 把所有的通用工具函式都轉成彙編程式。
用 QBASIC
- 所有重要的庫函式都要用 QBASIC 寫,然後再寫個彙編的封包程式來處理 large 到 medium 的記憶體模型對映。
內聯彙編
- 在你的程式碼裡混雜一些內聯的彙編程式,這樣很好玩。這年頭幾乎沒人懂彙編程式了。只要放幾行彙編程式碼就能讓維護程式碼的程式設計師望而卻步。
巨集彙編呼叫C
- 如果你有個彙編模組被C呼叫,那就儘可能經常從彙編模組再去呼叫C,即使只是出於微不足道的用途,另外要充分利用 goto, bcc 和其他炫目的彙編祕籍。
與他人共事之道
老闆才是真行家
- 如果你的老闆認為他20年的 FORTRAN 程式設計經驗對於現代軟體開發具有很高的指導價值,你務必嚴格採納他的所有建議。投桃報李,你的老闆也會信任你。這會對你的職業發展有利。你還會從他那裡學到很多搞亂程式程式碼的新方法。
顛覆技術支援
- 確保程式碼中到處是bug的有效方法是永遠不要讓維護程式碼的程式設計師知道它們。這需要顛覆技術支援工作。永遠不接電話。使用自動語音答覆“感謝撥打技術支援熱線。需要人工服務請按1,或在嘀聲後留言。”,請求幫助的電子郵件必須忽略,不要給它分配服務追蹤號。對任何問題的標準答覆是“我估計你的賬戶被鎖定了,有許可權幫你恢復的人現在不在。”
沉默是金
- 永遠不要對下一個危機保持警覺。如果你預見到某個問題可能會在一個固定時間爆發,摧毀西半球的全部生命,不要公開討論它。不要告訴朋友、同事或其他你認識的有本事的人。在任何情況下都不要發表任何可能暗示到這種新的威脅的內容。只傳送一篇正常優先順序的、語焉不詳的備忘錄給管理層,保護自己免遭秋後算賬。如果可能的話,把這篇稀裡糊塗的資訊作為另外一個更緊急的業務問題的附件。這樣就可以心安理得地休息了,你知道將來你被強制提前退休之後一段時間,他們又會求著你回來,並給你對數級增長的時薪!
每月一書俱樂部
- 加入一個計算機每月一書俱樂部。選擇那些看上去忙著寫書不可能有時間真的去寫程式碼的作者。去書店裡找一些有很多圖表但是沒有程式碼例子的書。瀏覽一下這些書,從中學會一些迂腐拗口的術語,用它們就能唬住那些自以為是的維護程式碼的程式設計師。你的程式碼肯定會給他留下深刻印象。如果人們連你寫的術語都理解不了,他們一定會認為你非常聰明,你的演算法非常深奧。不要在你的演算法說明裡作任何樸素的類比。
自立門戶
你一直想寫系統級的程式碼。現在機會來了。忽略標準庫, 編寫你自己的標準,這將會是你簡歷中的一大亮點。
推出你自己的 BNF 正規化
- 總是用你自創的、獨一無二的、無文件的BNF正規化記錄你的命令語法。永遠不要提供一套帶註解的例子(合法命令和非法命令之類)來解釋你的語法體系。那樣會顯得完全缺乏學術嚴謹性。確保沒有明顯的方式來區分終結符和中間符號。永遠不要用字型、顏色、大小寫和其他任何視覺提示幫助讀者分辨它們。在你的 BNF 正規化用和命令語言本身完全一樣的標點符號,這樣讀者就永遠無法分清一段 (…), […], {…} 或 “…” 到底是你在命令列裡真正輸入的,還是想提示在你的BNF 正規化裡哪個語法元素是必需的、可重複的、或可選的。不管怎麼樣,如果他們太笨,搞不清你的BNF 正規化的變化,就沒資格使用你的程式。
推出你自己的記憶體分配
- 地球人兒都知道,除錯動態儲存是複雜和費時的。與其逐個類去確認它沒有記憶體溢位,還不如自創一套儲存分配機制呢。其實它無非是從一大片記憶體中 malloc 一塊空間而已。用不著釋放記憶體,讓使用者定期重啟動系統,這樣不就清除了堆麼。重啟之後系統需要追蹤的就那麼一點東西,比起解決所有的記憶體洩露簡單得不知道到哪裡去了!而且,只要使用者記得定期重啟系統,他們也永遠不會遇到堆空間不足的問題。一旦系統被部署,你很難想象他們還能改變這個策略。
其他雜七雜八的招
如果你給某人一段程式,你會讓他困惑一天;如果你教他們如何程式設計,你會讓他困惑一輩子。 — Anonymous
不要重編譯
讓我們從一條可能是有史以來最友好的技巧開始:把程式碼編譯成可執行檔案。如果它能用,就在原始碼裡做一兩個微小的改動 — 每個模組都照此辦理。但是不要費勁巴拉地再編譯一次了。 你可以留著等以後有空而且需要除錯的時候再說。多年以後,等可憐的維護程式碼的程式設計師更改了程式碼之後發現出錯了,他會有一種錯覺,覺得這些肯定是他自己最近修改的。這樣你就能讓他毫無頭緒地忙碌很長時間。
挫敗除錯工具
對於試圖用行除錯工具追蹤來看懂你的程式碼的人,簡單的一招就能讓他狼狽不堪,那就是把每一行程式碼都寫得很長。特別要把 then 語句 和 if 語句放在同一行裡。他們無法設定斷點。他們也無法分清在看的分支是哪個 if 裡的。
公制和美製
在工程方面有兩種編碼方式。一種是把所有輸入都轉換為公制(米制)計量單位,然後在輸出的時候自己換算回各種民用計量單位。另一種是從頭到尾都保持各種計量單位混合在一起。總是選擇第二種方式,這就是美國之道!
持續改進
要持續不懈地改進。要常常對你的程式碼做出“改進”,並強迫使用者經常升級 — 畢竟沒人願意用一個過時的版本嘛。即便他們覺得他們對現有的程式滿意了,想想看,如果他們看到你又“完善“了它,他們會多麼開心啊!不要告訴任何人版本之間的差別,除非你被逼無奈 — 畢竟,為什麼要告訴他們本來永遠也不會注意到的一些bug呢?
“關於”
”關於“一欄應該只包含程式名、程式設計師姓名和一份用法律用語寫的版權宣告。理想情況下,它還應該連結到幾 MB 的程式碼,產生有趣的動畫效果。但是,裡邊永遠不要包含程式用途的描述、它的版本號、或最新程式碼修改日期、或獲取更新的網站地址、或作者的email地址等。這樣,所有的使用者很快就會執行在各種不同的版本上,在安裝N+1版之前就試圖安裝N+2版。
變更
在兩個版本之間,你能做的變更自然是多多益善。你不會希望使用者年復一年地面對同一套老的介面或使用者介面,這樣會很無聊。最後,如果你能在使用者不注意的情況下做出這些變更,那就更好了 — 這會讓他們保持警惕,戒驕戒躁。
無需技能
寫無法維護程式碼不需要多高的技術水平。喊破嗓子不如甩開膀子,不管三七二十一開始寫程式碼就行了。記住,管理層還在按程式碼行數考核生產率,即使以後這些程式碼裡的大部分都得刪掉。
只帶一把錘子
一招鮮吃遍天,會幹什麼就吆喝什麼,輕裝前進。如果你手頭只有一把錘子,那麼所有的問題都是釘子。
規範體系
有可能的話,忽略當前你的專案所用語言和環境中被普羅大眾所接受的程式設計規範。比如,編寫基於MFC 的應用時,就堅持使用STL 編碼風格。
翻轉通常的 True False 慣例
把常用的 true 和 false 的定義反過來用。這一招聽起來平淡無奇,但是往往收穫奇效。你可以先藏好下面的定義:
1 2 |
#define TRUE 0 #define FALSE 1 |
把這個定義深深地藏在程式碼中某個沒人會再去看的檔案裡不易被發現的地方,然後讓程式做下面這樣的比較
1 2 |
if ( var == TRUE ) if ( var != FALSE ) |
某些人肯定會迫不及待地跳出來“修正”這種明顯的冗餘,並且在其他地方照著常規去使用變數var:
1 |
if ( var ) |
還有一招是為 TRUE 和 FALSE賦予相同的值,雖然大部分人可能會看穿這種騙局。給它們分別賦值 1 和 2 或者 -1 和 0 是讓他們瞎忙乎的方式裡更精巧的,而且這樣做看起來也不失對他們的尊重。你在Java 裡也可以用這一招,定義一個叫 TRUE 的靜態常量。在這種情況下,其他程式設計師更有可能懷疑你乾的不是好事,因為Java裡已經有了內建的識別符號 true。
第三方庫
在你的專案裡引入功能強大的第三方庫,然後不要用它們。潛規則就是這樣,雖然你對這些工具仍然一無所知,卻可以在你簡歷的“其他工具”一節中寫上這些沒用過的庫。
不要用庫
假裝不知道有些庫已經直接在你的開發工具中引入了。如果你用VC++程式設計,忽略MFC 或 STL 的存在,手工編寫所有字串和陣列的實現;這樣有助於保持你玩指標技術的高水平,並自動阻止任何擴充套件程式碼功能的企圖。
建立一套Build順序
把這套順序規則做得非常晦澀,讓維護者根本無法編譯任何他的修改程式碼。祕密保留 SmartJ ,它會讓 make指令碼形同廢物。類似地,偷偷地定義一個 javac 類,讓它和編譯程式同名。說到大招,那就是編寫和維護一個定製的小程式,在程式裡找到需要編譯的檔案,然後通過直接呼叫 sun.tools.javac.Main 編譯類來進行編譯。
Make 的更多玩法
用一個 makefile-generated-batch-file 批處理檔案從多個目錄複製原始檔,檔案之間的覆蓋規則在文件中是沒有的。這樣,無需任何炫酷的原始碼控制系統,就能實現程式碼分支,並阻止你的後繼者弄清哪個版本的 DoUsefulWork() 才是他需要修改的那個。
蒐集編碼規範
儘可能蒐集所有關於編寫可維護程式碼的建議,例如 SquareBox 的建議 ,然後明目張膽地違反它們。
規避公司的編碼規則
某些公司有嚴格的規定,不允許使用數字識別符號,你必須使用預先命名的常量。要挫敗這種規定背後的意圖太容易了。比如,一位聰明的 C++ 程式設計師是這麼寫的:
1 2 3 |
#define K_ONE 1 #define K_TWO 2 #define K_THOUSAND 999 |
編譯器警告
一定要保留一些編譯器警告。在 make 裡使用 “-” 字首強制執行,忽視任何編譯器報告的錯誤。這樣,即使維護程式碼的程式設計師不小心在你的原始碼裡造成了一個語法錯誤,make 工具還是會重新把整個包build 一遍,甚至可能會成功!而任何程式設計師要是手工編譯你的程式碼,看到螢幕上冒出一堆其實無關緊要的警告,他們肯定會覺得是自己搞壞了程式碼。同樣,他們一定會感謝你讓他們有找錯的機會。學有餘力的同學可以做點手腳讓編譯器在開啟編譯錯誤診斷工具時就沒法編譯你的程式。當然了,編譯器也許能做一些指令碼邊界檢查,但是真正的程式設計師是不用這些特性的,所以你也不該用。既然你用自己的寶貴時間就能找到這些精巧的bug,何必還多此一舉讓編譯器來檢查錯誤呢?
把 bug 修復和升級混在一起
永遠不要釋出什麼“bug 修復”版本。一定要把 bug 修復和資料庫結構變更、複雜的使用者介面修改,還有管理介面重寫等混在一起。那樣的話,升級就變成一件非常困難的事情,人們會慢慢習慣 bug 的存在並開始稱他們為特性。那些真心希望改變這些”特性“的人們就會有動力升級到新版本。這樣從長期來說可以節省你的維護工作量,並從你的客戶那裡獲得更多收入。
在你的產品釋出每個新版本的時候都改變檔案結構
沒錯,你的客戶會要求向上相容,那就去做吧。不過一定要確保向下是不相容的。這樣可以阻止客戶從新版本回退,再配合一套合理的 bug 修復規則(見上一條),就可以確保每次新版本釋出後,客戶都會留在新版本。學有餘力的話,還可以想辦法讓舊版本壓根無法識別新版本產生的檔案。那樣的話,老版本系統不但無法讀取新檔案,甚至會否認這些檔案是自己的應用系統產生的!溫馨提示:PC 上的 Word 文書處理軟體就典型地精於此道。
抵消 Bug
不用費勁去程式碼裡找 bug 的根源。只要在更高階的例程里加入一些抵銷它的程式碼就行了。這是一種很棒的智力測驗,類似於玩3D棋,而且能讓將來的程式碼維護者忙乎很長時間都想不明白問題到底出在哪裡:是產生資料的低層例程,還是莫名其妙改了一堆東西的高層程式碼。這一招對天生需要多回合執行的編譯器也很好用。你可以在較早的回合完全避免修復問題,讓較晚的回合變得更加複雜。如果運氣好,你永遠都不用和編譯器前端打交道。學有餘力的話,在後端做點手腳,一旦前端產生的是正確的資料,就讓後端報錯。
使用旋轉鎖
不要用真正的同步原語,多種多樣的旋轉鎖更好 — 反覆休眠然後測試一個(non-volatile的) 全域性變數,直到它符合你的條件為止。相比系統物件,旋轉鎖使用簡便,”通用“性強,”靈活“多變,實為居家旅行必備。
隨意安插 sync 程式碼
把某些系統同步原語安插到一些用不著它們的地方。本人曾經在一段不可能會有第二個執行緒的程式碼中看到一個臨界區(critical section)程式碼。本人當時就質問寫這段程式碼的程式設計師,他居然理直氣壯地說這麼寫是為了表明這段程式碼是很”關鍵“(單詞也是critical)的!
優雅降級
如果你的系統包含了一套 NT 裝置驅動,就讓應用程式負責給驅動分配 I/O 緩衝區,然後在任何事務過程中對記憶體中的驅動加鎖,並在事務完成後釋放或解鎖。這樣一旦應用非正常終止,I/O快取又沒有被解鎖,NT伺服器就會當機。但是在客戶現場不太可能會有人知道怎麼弄好裝置驅動,所以他們就沒有選擇(只能請你去免費旅遊了)。
定製指令碼語言
在你的 C/S 應用裡嵌入一個在執行時按位元組編譯的指令碼命令語言。
依賴於編譯器的程式碼
如果你發現在你的編譯器或直譯器裡有個bug,一定要確保這個bug的存在對於你的程式碼正常工作是至關重要的。畢竟你又不會使用其他的編譯器,其他任何人也不允許!
一個貨真價實的例子
下面是一位大師編寫的真例項子。讓我們來瞻仰一下他在這樣短短几行 C 函式裡展示的高超技巧。
1 2 3 4 5 6 7 8 9 10 11 |
void* Realocate(void*buf, int os, int ns) { void*temp; temp = malloc(os); memcpy((void*)temp, (void*)buf, os); free(buf); buf = malloc(ns); memset(buf, 0, ns); memcpy((void*)buf, (void*)temp, ns); return buf; } |
- 重新發明了標準庫裡已有的簡單函式。
- Realocate 這個單詞拼寫錯誤。所以說,永遠不要低估創造性拼寫的威力。
- 無緣無故地給輸入緩衝區產生一個臨時的副本。
- 無緣無故地造型。 memcpy() 裡有 (void*),這樣即使我們的指標已經是 (void*) 了也要再造型一次。另外,這樣做可以傳遞任何東西作為引數,加10分。
- 永遠不必費力去釋放臨時記憶體空間。這樣會導致緩慢的記憶體洩露,一開始看不出來,要程式執行一段時間才行。
- 把用不著的東西也從緩衝區裡拷貝出來,以防萬一。這樣只會在Unix上產生core dump,Windows 就不會。
- 很顯然,os 和 ns 的含義分別是”old size” 和 “new size”。
- 給 buf 分配記憶體之後,memset 初始化它為 0。不要使用 calloc(),因為某些人會重寫 ANSI 規範,這樣將來保不齊 calloc() 往 buf 裡填的就不是 0 了。(雖然我們複製過去的資料量和 buf 的大小是一樣的,不需要初始化,不過這也無所謂啦)
如何修復 “unused variable” 錯誤
如果你的編譯器冒出了 “unused local variable” 警告,不要去掉那個變數。相反,要找個聰明的辦法把它用起來。我最喜歡的方法是:
1 |
i = i; |
大小很關鍵
差點忘了說了,函式是越大越好。跳轉和 GOTO 語句越多越好。那樣的話,想做任何修改都需要分析很多場景。這會讓維護程式碼的程式設計師陷入千頭萬緒之中。如果函式真的體型龐大的話,對於維護程式碼的程式設計師就是哥斯拉怪獸了,它會在他搞清楚情況之前就殘酷無情地將他踩翻在地。
一張圖片頂1000句話,一個函式就是1000行
把每個方法體寫的儘可能的長 — 最好是你寫的任何一個方法或函式都不會少於1000行程式碼,而且裡邊是深度巢狀,這是必須的。
少個檔案
一定要保證一個或多個關鍵檔案無法找到。利用includes 裡邊再 includes 就能做到這一點。例如,在你的 main 模組裡,你寫上:
1 |
#include <stdcode.h> |
Stdcode.h 是有的。但是在 stdcode.h 裡,還有個引用:
1 |
#include "a:\\refcode.h" |
然後,refcode.h 就沒地方能找到了。
(【譯者-老碼農-注】為啥找不到呢?仔細看看,現在還有人知道 a:\ 是什麼嗎?A盤!傳說中的軟盤…)
到處都寫,無處會讀
至少要把一個變數弄成這樣:到處被設定,但是幾乎沒有哪裡用到它。不幸的是,現代編譯器通常會阻止你做相反的事:到處讀,沒處寫。不過你在C 或 C++ 裡還是可以這樣做的。
【譯註】:原文在後面還有一些內容,翻譯時略有刪減。刪節的內容主要是:
- 我看不懂的部分;
- 我覺得不怎麼好笑的部分(其實很可能是因為沒看懂所以找不到笑點);
- 不容易引起現代程式猿共鳴的老舊內容。
本人水平有限,時間匆忙,難免有誤,請讀者不吝指出。謝謝!
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式