我們總是執著於讓程式碼持久執行。或許我們也應該執著於讓它優雅地退出。
有一句話在我腦海裡縈繞多年:
“編寫易於刪除、不易擴展的程式碼。”
— Tef,程式設計太糟糕了
我第一次讀到這段話時,就提出了異議。難道編寫程式碼的意義不就在於編寫能夠長期存在、可擴展、並且可以在此基礎上進行擴展的程式碼嗎?
然後我花了一個週末的時間,試圖從三年前的程式碼庫中移除一個日誌庫。它悄無聲息地擴展到了 40 個檔案。移除它就像給一個骨頭長在海綿裡的病人做手術一樣。
當我們編寫程式碼時,我們會給自己設定一個美好的願景:這段程式碼五年後依然會存在,所以我應該讓它健壯、可重複使用且可擴展。
但資料並不支持這種說法。大多數功能會在幾個月內發生變更,許多功能甚至會被徹底移除。平均而言,生產程式碼庫中存在著多年未曾修改的整個目錄——並非因為它們完美無缺,而是因為每個人都害怕刪除它們。
我們編寫程式碼時就好像它能承重一樣。但通常情況下,它並不能。
諷刺的是,我們越是試圖讓程式碼「永久化」——用抽象層包裹它、將其耦合到共享工具中、將其融入整個系統——它就越難修改。我們用適應性換取了持久性的假象。
這並不意味著編寫一次性程式碼。也不意味著跳過測試或忽略程式碼結構。
這意味著:設計時要考慮可逆性。
當你寫一個功能時,問問自己:如果這個功能明天就要被移除,那會是什麼樣子?
如果答案是“一個涉及 20 個檔案的 400 行 PR”,那麼問題出在設計階段,而不是刪除階段。
易於刪除的程式碼往往具有以下幾個共同特徵:
重複程式碼名聲不佳。 DRY 原則固然好,但如果走極端,就會導致程式碼高度糾纏。當同一個函數在八個不同的上下文中被重複使用時,你無法在不影響其他上下文的情況下修改其中一個上下文中的函數。
有時,少量重複是實現獨立性的代價。兩個都包含formatDate函數的模組可以各自發展或刪除,而不會產生任何影響。
最難刪除的程式碼是那些到處洩漏的程式碼。例如被匯入到 UI 元件中的資料庫客戶端,被層層傳遞的配置物件,以及淪為負載基礎架構的實用函數。
邊界是確保刪除安全的關鍵。一個隔離的模組、一個簡潔的介面、一個定義完善的 API 背後的服務……這些都可以放心地移除、替換或重寫,無需猶豫或深思熟慮。
易於刪除的程式碼往往是「無知」的,但這種「無知」恰恰是它的優勢所在。它對系統的其他部分一無所知。它接收輸入,執行自身任務,然後返回輸出。它不會去獲取全域狀態,也不會修改它未建立的內容。
無知的程式碼也是可測試的程式碼,這並非巧合(實際上,出於一些個人原因,我並不想加入這部分內容)。
功能開關、適配器層、介面抽象化——這些不僅僅是工程形式主義,它們還是刪除的利器。一個功能開關背後的功能可以在幾秒鐘內關閉。介面背後的程式碼可以在呼叫者毫無察覺的情況下被替換。
絞殺榕圖案的存在正是出於這個原因:先包裹住舊的東西,然後在上面建造新的東西,最後在舊的東西完全分離後將其刪除。接縫正是實現這一過程的關鍵。
我們常常為了避免重複而訴諸抽象。但抽象的最佳理由是將其隔離出來,或為它命名並歸類,這樣你就可以在不影響其他部分的情況下更改或移除它。
想想日誌記錄。你可以到處散佈console.log呼叫。這樣做雖然容易寫,但修改起來卻非常麻煩。或者,你可以將所有日誌記錄都路由到一個單獨的logger模組中。現在,如果你想切換日誌庫、新增上下文訊息,或完全關閉日誌——你只需要修改一個檔案。僅僅一個。
抽象層的存在並非因為日誌記錄很複雜,而是因為日誌記錄可能會改變或消失,而你希望這種變化或消失的過程盡可能簡單無阻。
抽象之處在於接縫處,而非中間部分。
我在程式碼審查中開始嘗試這樣做:不僅要問“這能行嗎?”,還要問“要移除這個功能需要什麼?”
它以一種有用的方式重新定義了事物。
一個加入新功能並修改 15 個檔案的 PR 是一個警示訊號——並非因為它本身有錯,而是因為它預示著未來變更的高昂成本。而一個透過單一、邊界清晰的模組加入相同功能的 PR 則留下了更簡潔的程式碼痕跡。
你可以將這種理念延伸到架構決策中。在新增新的依賴項之前,先問問自己:「兩年後移除這個依賴項會是什麼樣子?」有些依賴項沒問題,因為它們規模小、穩定或獨立存在。而另一些依賴項則像是引入了一種入侵物種,它們會滲透到各個系統,最終難以根除。
禪宗中有一個概念,有時被翻譯為「無常」 ——它指的是事物產生、存在一段時間後消逝。這並非悲觀,而只是對事物運作規律的準確描述。
軟體也是如此。功能會不斷更新迭代,產品會轉型升級,需求也會隨之改變。你今天寫的程式碼將來可能會被部分或全部替換。這並非失敗——這正是軟體的生命週期。
為短暫性而寫作意味著接受這一點,並據此進行設計。這意味著你的目標不是編寫永遠無法刪除的程式碼,而是編寫易於刪除的程式碼。
那些建構了至今仍在執行30年的系統的工程師們,並非透過將程式碼設計得難以修改來實現這一點。他們透過編寫易於理解、易於隔離,並且在需要時可以輕鬆地逐一替換的程式碼來實現這一點。
在做決定之前,不妨先快速地捫心自問:
我能否透過一個 PR 刪除這個功能?如果不行,為什麼?
這會牽涉到多少個文件?文件越多不一定越糟糕,但應該讓人感覺是刻意為之。
這個模組是否感知到了它不應該感知到的東西?例如導入語句、全域變數、副作用。
如果這個依賴項明天消失了,會有多糟?你能在一下午之內把它換掉嗎?
這種抽像是為了方便修改,還是只是為了避免重複?
這並不意味著要束手無策。你不需要把每個微服務都設計得好像隨時可能消失一樣。但是,就像培養對效能或可讀性的敏銳洞察力一樣,培養對刪除成本的敏銳洞察力,會悄悄讓你的程式碼庫更加健康。
最後還有一點值得一提:最容易刪除的程式碼就是你從未寫過的程式碼。
每個特性都是負擔。每個抽象層都增加了維護成本。每個依賴項都意味著你現在身處其中的關係。不存在的程式碼沒有缺陷,沒有耦合,也沒有刪除成本。
這並不意味著什麼都不做。而是意味著要深思熟慮。當你想要新增的抽象層,想要將只使用過一次的東西通用化,想要為可能永遠不會出現的用例建立功能時…請停下來。
或許正確的做法是等待。先寫出最簡潔的內容。留出刪除的空間。
因為能夠輕鬆變更的軟體才能存活下去。而實現易變更性的秘訣並非巧妙的架構或精妙的抽象。
關鍵在於明白,你今天建造的東西,明天可以優雅拆解。
原文出處:https://dev.to/adamthedeveloper/write-code-thats-easy-to-delete-the-art-of-impermanent-software-19l1