是時候想想該怎麼刪代碼了

武林外傳里秀才懟上姬無命,來了一段關於「我是誰」的精彩逼問。

我是誰?我生從何來,死往何處,我為何要出現在這個世界上?我的出現對這個世界來說意味著什麼,是世界選擇了我還是我選擇了世界?!我和宇宙之間有必然的聯繫嗎?宇宙是否有盡頭,時間是否有長短,過去的時間在那裡消失,未來的時間又在何處停止,我在這一刻提出的問題還是還是你剛才聽到的問題嗎?

我們平時很少問自己這樣愚蠢的問題。很多事情,我們是如此地習以為常,以至於非但自己看不到這樣的問題,當別人問道時,反而純純地回一句:doesnt look like anything to me。

在很早前的一篇文章 技術債:the good, the bad, and the tao 里我問了幾個問題:

  • 我們為什麼要做 code review?可以不做 code review 么?不做會有很嚴重的問題么?

  • 在一個 team 裡面,我們為什麼要用同一種語言寫代碼?為什麼程序員不能隨意用自己想用的語言?

上面的問題,別說問了,想一想都是政治不正確。我們寫代碼遵循的很多流程,歸根到底,都是為了軟體能有更好的可維護性(maintainability) —— 畢竟,一段代碼寫下去,未來很久很久的時間,它們都是公司的資產:我們需要根據市場的需要,為其添加新功能,我們需要修改發現的 bug 和潛在的問題,我們還不得不不斷提升其效率和能力,以便調和人民群眾日益增長的需求和落後的生產力之間的矛盾 —— 這可是軟體公司初級階段的主要矛盾(高級階段的都搞 AI 了)。所以,我們必須保證軟體的可維護性 —— 而代碼審核,同事間互相備份,使用同種語言開發都是服務於這個目的而存在的手段而已。那麼問題來了:

為什麼我們要維護代碼?為什麼不能重寫代碼?或者說,為什麼我們要通過修改代碼來維護代碼,而不是通過刪除和重寫代碼來達到維護代碼的目的?

我們之所以不能,或者不願這麼做有很多原因:

  1. 添加一個新的功能同時保證不引入問題已經讓人頭大了 —— 要修改十几几十個文件,新增代碼即便只有幾百上千行,但涉及的代碼成千上萬行,都重寫,寫的過來么?風險太大

  2. 改動不算太大,沒有必要重寫啊

  3. 瘋了,就算這麼做是對的,老闆肯定不會讓我這麼干

  4. 重寫我自己的代碼也就算了,重寫別人的 —— 比我牛的我沒底氣;比我慫的我不忍心

  5. ...

這裡面,除了第二個原因成立,其他其實都不太成立。第一個是架構的問題;三四是文化或者說認知的問題。架構問題如果深究,也是一種認知的問題。

因為我們認定了我們所寫的代碼,都不是可以隨意拋棄(throwable)的代碼,而是需要維護(maintainable)的代碼;我們從不把代碼看做阻礙我們前進的負債,而是沉澱下來的寶貴的資產。所以我們願意日復一日地堆上千萬行的代碼,從不考慮有計劃地刪除。

同樣的認知問題也發生在 imperative / functional programming 上。變數之所以被稱之變數,是因為它能夠被賦值,被修改。很多程序員無法想像如果變數只能被綁定,無法被修改,日子該如何過 —— 因為他們甚至不知道該如何處理最基礎的控制流程:循環。你告訴他 high ordered function,tail recursion,他會一臉懵逼:it doesnt look like anything to me。

你也許會說 functional programming 和代碼可隨意拋棄是兩種完全不同的認知,前者理性且有學術界的支撐,後者瘋狂,近乎無稽之談。讓我們先拋開這個爭論,假設確實有個瘋子要我們設計一種架構,在這個架構下,我們引入的任何新的代碼,都能夠在未來的某個時刻被完全扔掉重寫,而不會影響系統的其他功能。 我們該怎麼做?

義大利麵條式的架構肯定不行。別說把某個功能摘出來扔掉不影響功能了,光摘出來可能就已經讓人竭盡全力了。

所以我們必須要模塊化。每個模塊各司其職,上帝的歸上帝,凱撒的歸凱撒。這樣我們即便把凱撒拋棄了,上帝也不會活不下去。

光有模塊化也是不足夠的。我們還得考慮分層。原子核和電子組成原子,若干原子組成分子,若干分子組成細胞,若干細胞組成組織,若干組織組成器官,若干器官組成生物體。每個層次各司其職,細胞不會過問原子中的電子是如何變態躍遷的。層次化能夠保證在變化的系統中能有不變的,穩定的部分。在 ISO/OSI 結構中,物理鏈路層的變化被傳輸層屏蔽,而傳輸層的變化又被應用層掩蓋地妥妥帖帖。程序員在發送一個 GET / 請求時,並不關心這個請求是經過 IPv4 還是 IPv6 傳輸的,更不需要迷失在 RJ45,fiber 這些多種多樣的介面形態中。

分層還是不足夠的,因為它雖然解開了縱向的耦合而忽視了橫向的耦合。舉個最簡單的例子 —— 現在幾乎所有的 web app 都擁抱 MVC,但幾乎所有的經典 MVC framework 里做出來的 web app 都讓你陷入橫向耦合的陷阱,rails,django,phoenix,無一不是。如果讓你設計一個博客系統,你會自然而然地從 Model 起,設計 Blog / Post / Comment / User 等基本的 model。然而,Post 跟 Blog 本可以無關, Blog 只是一組滿足特定條件的 Post 的一個容器而已;Comment 也和 Post 無關,滿足特定條件的 Comments 聚合起來,恰巧構成了 Post 的某個屬性。因此,當你開始使用 rails generate model 的那一刻起,你的代碼已經註定了有很強的橫向耦合,難以將某個 Model 刪除重寫。有一天,當你發現 Post 需要用一個和 User 完全不同的 data store 存儲時,你會發現,這幾乎成了不可能完成的任務 —— 除非將整個系統重寫。

所以我們還需要將功能和功能解耦,也就是服務化,通過介面或者說協議來約束雙方的行為。拿剛才的博客系統來說,Post 應該對 User 而言完全是一個黑盒,User 無法觸及 Post 的內部狀態(使用什麼存儲方式),只能通過約定好的介面來獲取 Post 的信息。這樣的話,Post 和 User 可以在同一個資料庫中,也可以在不同的資料庫中。

服務化能夠部分地讓我們扔掉某個服務的代碼完全重寫,只要保證介面不變,就不會影響系統的其他功能。但它還不完全是最終的答案。我們從一個問題出發,走了這麼遠,已經可以心滿意足了。Stephen Covey 勸誡我們:begin with the end in mind。如果我們在開始寫代碼的時候能夠考慮到日後能被更加容易地刪除,那麼我們為此設計時會更加深思熟慮。我們會發現,寫一段能夠容易刪除和重寫的代碼要比寫一段容易維護的代碼要難上很多。

川普在他的百日新政中,提到了這麼一條:for every new federal regulation, two existing regulations must be eliminated. 且不管這有沒有實現的可能,它都是很大膽和有遠見卓識的想法。社會的效率之所以越來越低效,是跟法規(行政命令)只增不刪或者增加的速度大於刪除的速度,導致其臃腫繁複有極大的關係;軟體系統之所以越來越龐大不堪,難以維護,也是跟我們只是不斷添加和修改代碼,代碼的增加速度大於刪除的速度有關,偏巧,增加的又多以業務邏輯為主 —— 要知道,很多數年前的業務邏輯相關的代碼,可能已經和當下的商業環境大不相同,留著徒增煩惱,卻不帶來太多的價值。

對於這樣的問題,生物界給出來的答案是新陳代謝。所謂新陳代解,就是生物體與外界環境之間的物質和能量交換以及生物體內物質和能量的自我更新過程。新陳代謝包括合成代謝(同化作用)和分解代謝(異化作用)。

同化作用是指生物體把從外界環境中獲取的營養物質轉變成自身的組成物質,並且儲存能量的變化過程 —— 通過同化作用,程序員,軟體系統中的葉綠素,用靈巧的雙手把自己大腦中苦心孤詣鑽研出來的成果轉化成代碼,匯入系統中;異化作用是指生物體能夠把自身的一部分組成物質加以分解,釋放出其中的能量,並且把分解的終產物排出體外的變化過程 —— 對程序員來說,就是對系統去蕪存菁,定期不定期地刪除代碼中的渣滓,重寫那些光是維護著就已經讓人竭盡全力的代碼。。。然而,我們卻很少啟動這個異化過程。

寫這篇頗有爭議(且尚未深思熟慮)的短文,並非想說代碼維護不重要,重構不重要,只有重寫才是王道,而是想拋出一個問題:有沒有可能,我們在架構之初,就考慮到這個代碼有可能成為一種負債,因此在設計上考慮到如何能輕鬆地將其刪除或者替換?


推薦閱讀:

一次生產事故的優化經歷
英文版本的「弱者道之用」
詳解Serverless服務,它會顛覆你對雲的理解 | 硬創公開課
架構設計的0層邏輯

TAG:软件架构 | 软件设计 | 软件开发 |