當前位置: 妍妍網 > 碼農

軟體開發中的抽象泄露法則

2024-04-28碼農

在這篇 2002 的文章中,Joel Spolsky,StackOverflow 的聯合創始人,探討了抽象泄漏的法則。他指出,許多開發工具致力於透過抽象化簡化我們的工作流程,意在隱藏背後的復雜性。

然而,盡管抽象旨在遮掩底層的復雜性,實際套用中經常會暴露這些復雜性。這主要是因為抽象本身固有的復雜性以及在具體實施過程中遇到的多樣問題。Spolsky 強調,雖然抽象可以節省我們的工作時間,但它並不減少必須投入的學習時間。

01

互聯網工程中的抽象

互聯網工程中有一部份神奇的機制,你每天都在依賴它。這種機制發生在 TCP (Transmission Control Protocol) 協定 中,這是構建互聯網的基礎之一。

TCP 提供了一種可靠的數據傳輸方式。這意味著:如果你透過網路使用 TCP 發送訊息,它將被準確無誤地送達。我們依靠 TCP 來完成許多工,比如獲取網頁和發送電子信件。正是因為 TCP 的可靠性,每封電子信件都能完美無缺地到達,即便它只是一些無聊的垃圾信件。

相較之下,還有一種叫做 IP (Internet Protocol) 的數據傳輸方法,它是不可靠的。沒有人保證你的數據一定會到達,它可能在途中被擾亂。如果你透過 IP 發送一系列訊息,不要驚訝於只有一半能到達,有些可能順序錯亂,有些可能被替換成其他內容,可能包含可愛的小猩猩圖片的訊息,或者更可能是一堆類似你收到的外文垃圾信件的不可讀內容。

這裏有一個神奇的地方:TCP 是建立在 IP 之上的。也就是說,TCP 需要依賴一個本質上不可靠的工具來實作可靠的數據傳輸。

想象我們有一種將演員從百老匯送到好萊塢的方法,透過把他們放在汽車裏駕駛穿越美國。有些汽車事故導致了演員的不幸身亡。有時演員們路上喝醉酒,剃光頭或在鼻子上紋身,變得無法在好萊塢工作。而且,因為他們選擇了不同的路線,演員們常常不按原計劃的順序到達。

現在想象有一個名為 好萊塢快車 的新服務,它承諾送達演員時,他們會:(a)安全抵達(b)保持正確的順序(c)狀態完好。

神奇之處在於,好萊塢快車沒有比用汽車駕駛橫穿全國更可靠的傳送方法。而是透過確認每位演員都完好無失真地到達,如果有問題,就聯系總部派出演員的同卵雙胞胎。

如果演員順序錯亂,好萊塢快車將重新排序。如果一個巨大的 UFO 在內華達的某條高速公路上墜毀,使該路不通,所有經過那裏的演員都會改道透過亞利桑那,而好萊塢快車不會告訴加州的電影導演這些變故。對導演來說,演員似乎只是到達得比平時慢一些,他們甚至永遠不會知道 UFO 墜毀的事情。

這就是 TCP 的神奇之處。它是電腦科學家所稱的「抽象」(abstraction):一種簡化復雜底層操作的方式。

02

抽象的漏洞

實際上,許多電腦編程工作都涉及到抽象的構建。什麽是字串庫 (string library)?它讓電腦處理字串像處理數位一樣簡單。檔案系統 (file system) 又是什麽呢?它讓我們覺得硬碟不是一個儲存位元的旋轉磁盤堆積,而是一個層次分明的資料夾系統,其中的每個檔都由一個或多個字節串構成。

回到 TCP 的話題。我之前為了簡化問題而說了一個小謊,可能已經讓一些人感到不快。我說 TCP 能保證訊息的送達,但實際上並非如此。如果你的寵物蛇咬斷了電腦的網路線,導致 IP (Internet Protocol) 封包無法傳送,那麽 TCP 也無法奏效,你的資訊將無法送達。

如果你對公司的系統管理員態度不友好,他們可能會透過將你連線到一個負載過重的集線器來進行懲罰,這樣你的 IP 封包只有一部份能夠傳輸成功,TCP 雖然仍然會嘗試工作,但一切都會變得異常緩慢。

這正是我所描述的「抽象泄漏」(leaky abstraction)。TCP 試圖提供一個完全的抽象,掩蓋底層不可靠的網路;然而,有時網路的實際問題會穿透這層抽象,使你感受到那些抽象無法完全遮蔽的問題。這只是我稱之為「抽象泄漏法則」的眾多範例之一:

所有復雜的抽象,都在某種程度上存在漏洞。

抽象有時會出現問題。有的時候問題不大,有的時候問題很嚴重。這就是所謂的「泄漏」。在使用抽象的過程中,錯誤難免會發生。這裏有一些例項。

03

開發中常見的抽象泄露

一個看似簡單的操作,比如在一個大型二維陣列上進行遍歷,如果你選擇橫向而非縱向進行,效能差異可能會非常大。這就像木頭的紋理方向,不同的遍歷方向可能導致的系統錯誤(頁面錯誤)數量大不相同,而這些錯誤會讓程式執行變慢。即使是編寫底層程式程式碼的組合語言程式設計師,也通常假設他們可以操作一個大而平坦的記憶體空間。然而,由於虛擬記憶體的存在,這只是一個理論上的設想。當出現頁面錯誤時,這種抽象就會顯露出漏洞,某些記憶體存取會比其他存取消耗更多的時間。

SQL 語言的設計初衷是為了簡化資料庫查詢過程中所需的具體步驟,它允許你只定義想要的結果,而由資料庫自動確定如何具體執行。然而,在某些情況下,一些 SQL 查詢可能比其他邏輯上等效的查詢慢上千倍。一個廣為人知的例子是,在某些 SQL 伺服器上,如果你指定「where a=b and b=c and a=c」,查詢速度會顯著快於只指定「where a=b and b=c」的情況,即使最終的結果集相同。理論上,你不應需要關心具體的查詢過程,只需要關註所需的結果。但有時候,這種抽象層會出現問題,導致效能嚴重下降,這時你可能需要使用查詢計劃分析器 (query plan analyzer) 來檢查問題所在,並找出加快查詢速度的方法。

雖然像 NFS (Network File System) 和 SMB (Server Message Block) 這樣的網路庫能讓你把遠端機器上的檔當作本地檔來處理,但有時候網路連線可能變得非常慢或直接斷開,這使得檔無法像本地檔那樣操作。作為開發者,你需要編寫程式碼來應對這種情況。這種將「遠端檔視為本地檔」的抽象就出現了漏洞。這裏有一個具體的例子適用於 Unix 系統管理員。如果你把使用者的主目錄設定在透過 NFS 掛載的驅動器上(一種抽象),而使用者建立了 .forward 檔以將所有信件轉發到其他地方(另一種抽象),當 NFS 伺服器在新信件到達時出現故障,這些信件就不會被轉發,因為 .forward 檔無法被找到。這種抽象的漏洞實際上導致了一些信件的遺失。

C++ 字串類的設計初衷是讓字串處理變得簡單,好像字串是基本數據型別一樣。這些類嘗試隱藏字串處理的復雜性,讓操作字串像操作整數那樣簡單。幾乎所有的 C++ 字串類都多載了加號 (+) 運算子,使得你可以簡單地透過 s + 「bar」 來拼接字串。但有一個問題:無論這些類嘗試得多麽努力,目前還沒有任何一個 C++ 字串類能支持直接使用 「foo」 + 「bar」 來拼接字串,因為在 C++ 中,字串字面量實際上是 const char* 型別,而不是字串物件。這揭示了一個語言層面的漏洞,這個漏洞是語言設計本身所無法解決的。(有趣的是,C++ 的發展歷史在很大程度上可以看作是不斷嘗試修補這種字串抽象漏洞的過程。至於為什麽不直接在語言中增加一個原生的字串類,目前我還不清楚原因。)

即使你的汽車配備了雨刷、前燈、車頂和加熱器,這些裝置幫你忽略了下雨天氣的直接影響(它們試圖抽象掉天氣因素),但在下雨時,你仍然不能像晴天那樣快速駕駛。這是因為你需要考慮到車輛打滑的風險,而且有時雨勢太大,前方的視線非常有限,迫使你不得不減速。因此,天氣的實際影響永遠無法完全被忽略,這正是抽象泄漏法則所描述的現象。

04

抽象泄露的挑戰

抽象泄漏法則帶來的一個問題是,它並沒有像我們預期的那樣真正簡化我們的生活。比如,當我在教導新手成為 C++ 程式設計師時,我本希望他們無需了解復雜的 char* 和指標運算。我希望能直接教授他們如何使用 STL 的字串類。然而,當他們嘗試執行如「foo」 + 「bar」這樣的操作時,程式會出現意外的行為,這時我不得不解釋 char* 相關的所有復雜細節。或者,當他們需要呼叫一個 Windows API 函式,該函式的文件說明它有一個輸出型別為 LPTSTR 的參數,他們在理解 char*、指標、Unicode、wchar_t 以及 TCHAR 表頭檔等多個概念之前,是無法正確呼叫此函式的。這些底層的細節和概念就是所謂的「泄漏」,它們不斷浮出水面,使得抽象的本意——簡化編程——受到了挑戰。

在教授別人進行 COM 編程的過程中,如果我只需要向他們展示如何使用 Visual Studio 的精靈和各種程式碼生成工具就夠了,那該多好。然而,一旦出現任何問題,他們往往對發生了什麽、如何進行偵錯和修復都一頭霧水。因此,我不得不向他們詳細講解 IUnknown、CLSIDs(類識別元)、ProgIDs(程式識別元)等核心概念……哎,這真是對人性的一種折磨!

在教授 ASP.NET 編程時,最理想的情況是我只需要告訴學生們雙擊元素,然後編寫一些程式碼,這些程式碼會在使用者點選這些元素時在伺服器端執行。ASP.NET 實際上簡化了編寫處理超連結( <a> )點選和按鈕點選的 HTML 程式碼的差異。然而,問題在於:ASP.NET 的設計者們需要掩蓋一個事實,即在 HTML 中,超連結本身不能用來送出表單。為了解決這個問題,他們生成了一些 JavaScript 程式碼,並為超連結添加了一個點選事件處理器(onclick handler)。但是,這種抽象存在漏洞。如果終端使用者禁用了 JavaScript,那麽 ASP.NET 應用程式將無法正常工作。如果程式設計師不了解 ASP.NET 抽象化了哪些細節,他們將完全不知道出了什麽問題。

05

總結與思考

抽象泄漏法則表明,每當出現一種新的、被宣稱能大幅提高效率的程式碼生成工具時,你總會聽到很多人建議:「先學會手動操作,再用這些先進工具來節省時間。」這些試圖簡化任務的程式碼生成工具,就像所有抽象一樣,總是存在一些缺陷。唯一能夠有效處理這些缺陷的方法是,深入了解這些工具的工作原理和它們試圖簡化的具體內容。

所以,雖然這些工具可以減少我們的工作量,但它們並不能減少我們學習的時間。這一切表明了一個矛盾:盡管我們擁有更高級的編程工具和更精細的抽象方法,成為一名熟練的程式設計師的難度卻在不斷增加。

在我第一次在微軟的實習期間,我負責編寫用於麥金塔電腦的字串庫。一個典型的任務是編寫一個 strcat 函式的變體,該變體能返回指向新字串末尾的指標。這只需要幾行 C 程式碼。我所依據的全部知識都來自於一本簡單的書籍,由 Kernighan 和 Ritchie 編寫的【C Programming Language】。

今天,要在 CityDesk 上進行工作,我需要掌握一系列高級工具,包括 Visual Basic、COM、ATL、C++、InnoSetup、Internet Explorer 的內部機制、正規表式、DOM、HTML、CSS 和 XML。這些都是相對於舊的 K&R 材料來說的高級工具,但我仍然必須熟悉 K&R 的基礎內容,否則我就完蛋了。

十年前,我們可能以為新的編程範式會讓編程到今天變得更簡單。確實,我們多年來開發的抽象化技術讓我們能夠應對軟體開發中之前未曾遇到的復雜問題,如圖形化使用者介面(GUI)編程和網路編程。這些如現代物件導向的基於表單的語言等強大工具,使我們能夠迅速完成大量工作。然而,突然間,我們可能遇到一個因抽象漏洞而產生的問題,解決它可能需要兩周時間。此外,當你需要聘請一名主要進行 VB 編程的程式設計師時,僅有 VB 編程技能是不夠的,因為每當 VB 的抽象層出現問題時,他們可能會感到束手無策。

抽象泄漏法則正成為我們發展的一個拖累。

作者:JOEL SPOLSKY

原文地址:點選閱讀原文

最近文章列表:

[1]

[2]