程式設計師應該有偏執心。
- “我仔細檢查了程式碼”
- “程式碼透過了測試”
- “審閱者批准了我的程式碼”
“那麼我的程式碼正確嗎?”
正確編寫程式碼很困難,而且驗證程式碼正確性是不可能的。以下是一些原因:
- 普遍性:即使你的程式碼一次執行正確,它在所有情況下、所有機器、所有時間都執行正確嗎?
- 誤報:測試失敗表明存在錯誤,但透過測試並不保證沒有錯誤。
- 缺乏確定性:您可以為程式碼的正確性編寫正式證明,但現在您一定想知道該證明是否正確。您需要證明該證明。這種驗證鏈永遠不會結束。
追求程式碼正確性的確定性是愚蠢的。錯誤可能隱藏在您永遠找不到的依賴項中。但我們不應該絕望。我們仍然可以透過更好的理解和盡職調查來降低錯誤的風險。
抽象
什麼是“更深的理解”?
讓我們集中討論一下程式設計師經常提到的理解的一個方面:抽象。
抽象是……
- 事物運作方式的心理模型
- 大腦中發生的資料壓縮(可能是有損的,也可能是無損的)的結果
- 在日常生活中隨處可見
“抽象”一詞有很多含義。
- 在程式設計中,它也可以指隱藏複雜性的程式碼層。
- 這篇文章只討論認知意義上的抽象。
抽象的例子:
- 我們將一群樹視為一片森林。
- 我們認為銀行存款就是銀行為我們儲存的錢。
- 事實上,銀行不只是儲存我們存入的錢。它借出/投資了人們存入的大部分錢。我們的錢不會閒置 在金庫裡。
- 我們的銀行餘額實際上只是一本記錄我們可以提取多少錢的賬簿。
- 時間膨脹會根據每個人/物體的速度和所受的重力大小稍微改變他們的時間流逝。
- 圍繞地球執行的 GPS 衛星必須每天調整其時鐘約 38 微秒,以適應時間膨脹(來源)。
形成抽象的一種方法是刪除不必要的細節。
例如,大多數開車的人對汽車的內部工作原理不太瞭解。他們對汽車的看法可以歸結為:
- 點火啟動汽車
- 加速器使汽車行駛
- 剎車使汽車停下來
- 車輪轉動汽車
- 汽車需要汽油/柴油
瞭解了上述抽象概念,就無需瞭解汽車發動機的內部工作原理。
大多數司機只具備汽車的這些工作知識,就可以開車去他們需要去的地方。
當我們使用程式語言時,它提供了抽象,使我們無需瞭解計算機的內部工作原理即可操作計算機。
- 基本語言特性(如迴圈、if 條件、函式、語句和表示式)都是抽象,它們隱藏了以下內容:
- 硬體級別的詳細資訊:CPU 指令、暫存器、標誌以及特定於 CPU 架構的詳細資訊……
- 作業系統級細節:呼叫堆疊管理、記憶體管理……
- 任何已編譯的 Java 程式(例如 jar 檔案)都應該能夠在任何具有 Java 執行時環境(即 JVM)的機器上執行。
- Python 指令碼應該能夠在任何具有 Python 直譯器的機器上執行。
- 如果機器有 C 編譯器,那麼C 程式應該能夠在任何機器上編譯並執行。
抽象洩露
不幸的是,抽象會失敗。
- 如果您關心程式碼效能,語言抽象是不夠的。要加快程式碼速度,您需要了解硬體級和作業系統級的詳細資訊。
- 移植具有外部依賴項(如動態庫或網路要求)的程式並不那麼簡單。它們不能簡單地移動到另一臺機器並執行。需要額外的設定和知識。
- 只知道最基本知識的車主最終可能會陷入汽車拋錨的境地。如果駕駛員不定期更換汽車的潤滑油/機油,則會縮短髮動機的使用壽命。
抽象在短期內執行良好,但從長期來看會失效。
Joel Spolsky 將這種失效的抽象描述為“洩露”,並提出了抽象洩露定律:
- 所有普通的抽象在某種程度上都是有漏洞的。
這與統計學中的格言類似:
- 所有模型都是錯誤的,但有些是有用的。
當我們編寫程式碼時,我們總是使用漏洞抽象。以下是一些隨機示例:
- 垃圾收集消除了擔心記憶體管理的負擔(除非我們關心延遲抖動)
- C++ 智慧指標使記憶體安全(只要你不儲存任何原始指標)
- 雜湊錶速度很快,因為它們具有 O(1)操作(但對於較小的陣列,速度更快)。
- 透過引用傳遞比透過值傳遞更快(除了複製省略的情況和適合 CPU 暫存器的值,如 int)
幸運的是,許多漏洞抽象在失敗時會導致程式碼崩潰,因此很容易解決。
然而,有些漏洞抽象可能只會產生未定義的行為或效能下降,這些行為更難識別和修復。
那麼,如果抽象可能會帶來問題,那麼我們是否應該嘗試在不考慮抽象的情況下理解一個主題(瞭解汽車的真正面貌)?
不。當你深入抽象時,你只會發現更多的抽象。
這就像烏龜在不斷下沉。
- 我們對汽車的抽象基礎在於對每個部件的用途的理解。
- 在這之下,燃燒化學和發動機機械工程
- 在這之下,是模擬宇宙力量的數學/物理學
這些抽象層不斷深入,直到我們觸及關於邏輯和現實的最基本公理。
作為程式設計師,我們應該把我們的知識看作是一個由漏洞百出的抽象和假設組成的紙牌屋。
我們應該對一切事物、任何人,包括我們自己,都保持適度的懷疑態度。
信任但要驗證
程式設計師應該有“信任,但要核實”的政策。
這裡有些例子:
- 相信人們告訴你的資訊,但要用檔案來驗證
- 透過嘗試反駁來檢驗你的信念。
- 您為程式碼更改編寫了測試,並且第一次嘗試就透過了。嘗試在沒有更改的情況下執行測試,看看它們是否仍能透過。它們可能存在導致測試總是透過的錯誤。
- 您已重構了本應為無操作的程式碼。所有測試仍透過。請檢查以確保確實有任何測試執行您重構的程式碼。
- 您最佳化了服務,並且看到了資源利用率的預期下降。請檢查以確保您的服務當前不只是處理的請求較少。
- 您已提交程式碼更改,第二天發現服務中沒有出現任何問題。請檢查以確保當天已推出並且您的程式碼已包含在內。
警惕未知的未知數
對於程式設計師來說,最可怕的認識論問題是“未知的未知”。
有…
- 你知道的事情(即“已知”)
- 你知道你不知道的事情(“已知的未知數”)
- 你甚至不知道你不知道的事情(“未知的未知數”)
這些未知的未知數是抽象失敗的根源(也是程式設計師永遠無法準確預測一個專案需要多長時間的原因)。
你可能從未聽說過……
- 淨化使用者輸入
- 如果您使用使用者提供的字串作為 SQL 查詢的一部分,您的服務可能會透過 SQL 注入受到駭客攻擊。
- 字元編碼
- 您的程式碼處理的任何文字資料都必須使用您的程式碼期望/支援的字元編碼(例如,ASCII,UTF-8,UTF-32等)。
- 根據字元編碼,隨機訪問文字緩衝區中的字元可能需要恆定時間(對於 ASCII)或線性時間(對於 UTF-8)。
- 如果您嘗試使用錯誤的字元編碼讀取文字資料,可能會輸出難以理解的字元。
- 您的程式可能會因堆記憶體不足而變慢。
- 如果您知道為 Java 程式配置更大的最大堆大小,則可以解決此問題。
如果您之前沒有聽說過這些主題,您甚至可能不知道自己已經陷入了它們的陷阱。
當未知未知因素出現時,沒有萬無一失的方法可以將其捕獲,但我們應該檢查至少一個抽象層來尋找它們。特別是當一個專案需要學習新東西時,你應該總是學習比你需要的更多的東西。這樣做可以降低因抽象失敗而感到驚訝的風險。
當學習/使用不熟悉的平臺/語言/工具/庫/技術時:
- 閱讀比最低限度要求更多的文件
- 看影片
- 在我看來,會議演講質量最高
- 閱讀部落格文章
- 閱讀原始碼
- 加深對必須處理的抽象概念的理解
- 瞭解您的程式語言最近新增的功能
- 通讀圖書館的所有公共功能,而不僅僅是你正在使用的功能
- 瀏覽 CLI 工具手冊頁中的所有標誌
- 瞭解編譯器的最佳化
- 如果你正在執行服務,請了解你的編排平臺(例如:kubernetes)
- 如果你使用 Java,請了解 JVM
- 如果您使用 Python,請了解 Python 直譯器。
概括:
以下是重點:
- 程式設計師應該採取“信任但要驗證”的方法,質疑假設,並從多個來源驗證資訊。
- 文章強調了測試和驗證的重要性,建議程式設計師應該:
- 執行不做任何更改的測試,以確保它們並非總是透過
- 使用適當的測試驗證程式碼重構
- 最佳化程式碼時衡量影響
- 確認程式碼部署及其包含在釋出中
- 閱讀大量文件
- 觀看會議演講
- 檢查原始碼
- 理解至少低於必要水平的一層抽象
結論
抽象是必要的,因為它能讓我們高效地思考,但抽象也是危險的,因為它可能會讓我們覺得自己知道的 "足夠多"。膚淺學習的程式設計師無法在沒有已知解決方案、涉及多個專業領域的高難度專案中取得成功。
也就是說,這篇博文提出的理想需要與現實保持平衡。顯然,我們不可能在匆忙中花時間學習每一件小事。此外,我們也不能指望初學者能做到如此透徹。理想應該與現實世界的考慮相平衡。