平庸程式設計師的各種跡象

柒柒發表於2015-07-01

二、平庸程式設計師的跡象

1.無法從集合的角度思考

從指令式程式設計過度到函式式和宣告式程式設計,會立刻要求你思考將資料集合當作原語來操作,而不是作為標量值。無論你在關係型資料庫(並且不是作為一個物件倉庫)中使用 SQL,還是設計規模會隨多處理器線性變化的程式,亦或是你寫的程式碼必須要在擁有 SIMD 能力的晶片(比如現代顯示卡和電子遊戲機)上執行,都需要這種過渡。

特徵

只有在帶有宣告式或函數語言程式設計特性(程式猿應該知道這些特性)的平臺上看到這些特徵時,下面列出的才算數。

  1. 在 for 或 foreach 的迴圈裡對集合中的每個元素執行原子操作。
  2. 寫的 Map 或 Reduce 函式裡包含自定義的迴圈,在迴圈裡逐一重複執行資料集。
  3. 從伺服器抓取大量資料集,並在客戶端上計算和,而不是在查詢裡使用聚集函式。
  4. 函式作用於一個集合中的每個元素,並在函式開頭通過執行一次新的資料庫查詢來抓取一個關聯記錄。
  5. 寫的業務邏輯函式,例如更新一個使用者介面或執行檔案 I/O,很不幸地為了某種折衷而伴隨有副作用。
  6. 在實體類開啟專屬的資料庫連線或檔案操作符,並且在每個物件的生命週期裡都保持連線狀態。

補救措施

非常有趣,想象著一個發牌的人通過手指在牌裡翻轉把一副牌切成兩堆交叉洗牌,就能讓大腦聯想到集合,以及如何成批地操作集合。激發人聯想的其他想象還有:

  • 高速公路上的車流通過一系列收費站(並行處理)。
  • 泉水匯聚成溪流,溪流又匯聚成小河,最後再匯聚成江河(並行分解/聚集函式)。
  • 一個報紙印刷機(協同程式、流水線)。
  • 夾克上的拉鍊頭把拉鍊齒拉上(簡單的聯結)。
  • 轉移 RNA 加上氨基酸,並在一個核蛋白中加入信使 RNA,就變成了蛋白質(多階段函式驅動聯結,詳見 animation)。
  • 在一棵橘子樹中,數以億計的細胞裡同時發生著上述的過程,不斷地將空氣、水和陽光轉換成橘子汁(大型分散式叢集上的 Map/Reduce)。

如果你正在寫一個處理集合的程式,思考一下所有的附加資料和記錄,你的函式需要操作它們的每一個元素。並且在 Reduce 函式應用到每對資料上之前,使用 Map 函式把它們成對地聯結在一起。

2.缺乏批判性思維

除非你能批判自己的思維並從中找出缺陷,否則你會錯過那些可以在敲程式碼之前就能解決的問題。如果你也無法評判自己曾經寫過的程式碼,那你只能在不斷摸索中以龜速學習。這個問題同時來源於思考怠惰和以自我為中心,因此,這個問題的特徵似乎也來自兩個不同的方向。

特徵

  1. 自制“業務規則引擎”。
  2. 靜態工具類很冗餘且龐大,或者多學科的函式庫只用一個名稱空間。
  3. 把各種應用糅合在一起,或給當前的應用附加不相關的特性來避免啟動新專案的開銷。
  4. 程式架構開始需要建立 epicycle 模型。(譯者: epicycle 模型是天文學上使用的模型,用來解釋天體在運動過程中出現的偏差等異常行為。)
  5. 為了很不相關的資料向表中新增欄位(比如:在通訊錄的表中放置“# cars owned”欄位)。(譯者:通訊錄的內容要記錄是否有車幹嘛,確實扯遠了。)
  6. 前後矛盾的命名規範。
  7. 處於“拿著錘子看什麼都是釘子”的心態,或者改變對問題的定義,這樣所有問題都能用某個特定的技術來解決。
  8. 編寫程式降低問題的複雜度。
  9. 從病理上冗餘地防禦式程式設計(“企業級程式碼”)。
  10. 用 XML 重新發明 LISP。

補救措施

從Paul 和 Elder 寫的《批判性思維 | Critical Thinking》這樣的書入手,控制自我意識,在向朋友或同事發表自己的想法以此尋求評論時,練習抵制為自己辯護的衝動。

一旦你習慣了別人來檢驗你的想法,你就會開始自我審視並練習想象這些想法的結果。另外,你也需要培養起區別輕重緩急的能力(能直覺知道對這種規模的問題,需要花費多少精力比較合適)、用實踐驗證假設的習慣(這樣你就不會高估問題的大小)和麵對失敗的健康心態(就算艾薩克.牛頓的地心引力說是錯的,但我們依然愛他並需要他去嘗試)。

最後,你必須自律。意識到計劃裡有缺陷不會讓你更高效,除非你有足夠的意志力去改正缺陷,並重建手中正在進行的工作。

3.彈球式程式設計

如果你把皮膚傾斜得剛剛好,把曲柄拉回到剛好的距離,並且以正確的順序擊中那些凸起的按鈕,那麼程式就會像彈球一樣執行無誤:隨著指令的執行流程,從條件語句返回,跳過未選中的指令,轉向下一次的狀態轉換。

特徵

  1. 用一個 try-catch 程式碼塊包圍 Main() 的整個函式體,並在 Catch 分句中重置整個程式(像彈球地溝,掉下去以後重新開始遊戲)。
  2. 在強型別的語言中,用字串或整型來儲存那些擁有(可以用)更合適封裝型別的值。
  3. 把複雜資料打包成帶分隔符的字串,然後在使用它的每個函式裡解析一遍。
  4. 對輸入有歧義的函式,不會用斷言(assertion)或方法協定(method contract)。
  5. 使用 Sleep() 來等待另一個執行緒完成任務。
  6. 對非列舉型別的值使用 switch 語句,而且分支語句中沒有“Otherwise”分句。
  7. 用 Automethods 或 Reflection 來呼叫在非法的使用者輸入中提到的方法。
  8. 在函式裡通過設定全域性變數來返回多個值。
  9. 類裡有一個方法和幾個欄位,通過設定欄位來為方法傳遞引數。
  10. 不用事務來更新多行資料庫內容。
  11. 孤注一擲(比如,試圖不用事務和 ROLLBACK 來恢復資料庫的狀態)。

補救措施

把程式的輸入想象成水。它即將流過每一個縫隙,灌滿每一個容器。那麼你要想一想,如果它流過的地方並沒有明確建立任何東西去呈接它的話,會造成什麼後果。

你要讓自己熟悉平臺的機制,這有助於寫出健壯且易擴充套件的程式。共有三種基礎機制:

  1. 當某種意外發生時,能在產生任何破壞之前停止程式,然後幫助你識別出是哪裡出錯了(型別體系、斷言、異常等)。
  2. 將程式的執行導向處理意外最佳的程式碼塊( try-catch 模組、多重分發、基於事件驅動程式設計等)。
  3. 暫停執行緒直到一切就緒(WaitUntil 命令、互斥鎖和訊號量、同步鎖等)。

還有第四條,單元測試,你可以在設計階段使用。

使用這些機制應該成為你的第二天性,就像在句子裡用逗號和句號一樣。為了做到這些,每次瀏覽一遍上面介紹的機制(括號裡提到的那些),並重構你的舊程式,把提到的這些機制塞到任何能塞的地方,就算最後發現這麼做並不合適(尤其是在它們看似不合適的時候,至少那時你也開始明白其中的緣由)。

4.不熟悉安全原則

如果要說下述特徵並不很嚴重,但它們幾乎是大部分程式都存在的整體質量問題。意思是說,這些特徵不會讓你成為一名很糟糕的程式猿,只是意味著你不應該從事網路程式或安全系統的工作,直到你已經在這方面做了一些功課。

特徵

  1. 以明文形式儲存可利用資訊(名字、卡號、密碼等)。
  2. 用低效的加密術儲存可利用資訊(將密碼編譯在程式中的對稱加密演算法;簡單密碼;任何“解碼環( decoder-ring )”、自創加密演算法、專有的或未驗證的加密演算法)。
  3. 在接受網路連線或解釋來自非置信源的輸入資訊之前,程式或裝置沒有限制它們的許可權。
  4. 不進行邊界檢查或輸入合法性驗證,尤其是在使用非託管類的語言時。
  5. 把不合法或非轉義的輸入串接到字串上來構建SQL查詢。
  6. 呼叫使用者輸入中指定的程式。
  7. 試圖通過搜尋已知漏洞的簽名(signature)來阻止漏洞被利用。
  8. 用不加鹽的雜湊值(unsalted hash)儲存信用卡卡號或密碼。

補救措施

下面只涵蓋了基本原則,但遵照這些原則會避免絕大多數臭名昭著的錯誤,那些錯誤可以讓整個系統大打折扣。對於任何處理或儲存有價值資訊的系統,無論是向你還是其使用者,或是控制一個貴重資源的系統,它們通常都有一個安全專家來審查系統的設計與實現。

從審查程式開始,找出用陣列或其他配置記憶體的容器來儲存輸入的程式碼,確保這部分程式碼檢查了輸入的大小不會超出分配給它的記憶體大小。沒有其他型別的 bug 能比緩衝區溢位更能導致可利用的安全漏洞。從某個層面來說,在寫網路通訊程式或任何安全第一的場合下,你應該認真考慮使用某種記憶體託管型的程式語言。

下一步,審查資料庫查詢操作。審查那些將未修改輸入串接到 SQL 查詢內容中的查詢操作,並且,如果平臺支援的話就切換為使用引數化查詢,如果不支援就對輸入進行過濾或轉義。這麼做是為了防止 SQL 注入攻擊。

在你清除了這兩類最臭名昭著的安全 bug 之後,你應該繼續將所有的的程式輸入視為完全不可靠,或是有潛在惡意。按有效的驗證規則來定義程式的輸入很重要,而且除非輸入能通過驗證,否則程式應該拒絕它,這樣你就能夠通過修復驗證方法並使其更加明確來修復可利用的漏洞,而不是通過掃描已知漏洞的簽名來修復漏洞。

進一步說,你應該總是在開始設計程式之前,思考程式需要執行的操作以及這些操作需要從 host 獲得什麼樣的許可權,因為這個時候是想出怎麼樣能儘可能使用最少許可權的最佳時機。這條建議背後的原則是,如果在你的程式碼中找到一個可利用的 bug ,限制這個bug可能對系統其他部分造成的損害。換言之:在你學會不信任輸入之後,你也應該學會不要信任自己寫的程式。

最後你要學會的是資料加密基礎,從《Kerckhoff’s principle》開始。這一點亦可表達為“安全第一”,從中還衍伸出了一些有趣之處。

原則一,永遠不要信任一個密碼或其他加密原語,除非它已經被公開發表,並且已經由更高階別的安全社群對其進行了全面的分析和測試。從密碼學的發展來看,模糊晦澀的加密法、專有的加密法或是新出現的加密法都毫無安全可言。即使是可信的加密原語,其實現中也會存在缺陷,因此,對於你不能確定其已經得到全面審查的加密演算法(包括自己實現的版本),要避免使用。所有的新型加密系統都要經過一系列的詳細審查,這個過程可能長達十年之久,或更長,而你只要關注那些最後經受住了審查並且所有已知錯誤都已修復的加密系統。

原則二,如果金鑰容易破解或儲存失當,那這和完全不加密一樣糟糕。如果程式要對資料加密,但不需要解密或很少需要解密,那就考慮只把對稱加密金鑰對的公鑰給它,並讓解密階段和私鑰分開執行,使用者必須每次輸入一個好的口令來確保金鑰的安全。

越是處於危險之中,你需要做的功課越多,並且必須在程式的設計階段投入更多精力。這都是因為一旦你的程式部署下去,就會有成堆、有時候可能是成千上萬的不速之客試圖去破壞它的安全性。

絕大部分可追溯到程式碼問題的安全故障都歸因於一些很愚蠢的錯誤,其中大部分錯誤可以通過篩選輸入、謹慎使用資源、利用常識、想清楚再寫程式碼等方式來避免。

5. 程式碼一塌糊塗

特徵

  1. 不遵循一貫的命名規範。
  2. 不使用縮排,或縮排不一致。
  3. 不使用空格,例如在方法之間不加空格(或表示式裡不加空格,看“ANDY=NO”)。
  4. 有一大堆被註釋掉的程式碼。

補救措施

程式猿在匆忙之下(或特殊情況下)犯了上述所有毛病的話,會在之後返回來清理,但一個糟糕的程式猿真的就只是粗心大意。有時,利用可通過快捷鍵來修復縮排和空格(“美觀的格式”)的 IDE 是很幫助的,但我發現程式猿總是把程式碼搞得一團糟,極大地違背 Visual Studio 對適當縮排的堅持。

相關文章