世界級編程大師 Bob 大叔為「乾淨代碼」辯護遭質疑

infoq 發佈 2024-03-17T13:48:18.329238+00:00

世界級編程大師 Bob 大叔為「乾淨代碼」辯護遭質疑:時代變了,別用 Clean Code 那套要求我們了作者 | 褚杏娟、核子可樂不久前,遊戲引擎資深研發 Casey Muratori 發表文章 「乾淨」的代碼,賊差的性能後,引發了大量開發者討論。

世界級編程大師 Bob 大叔為「乾淨代碼」辯護遭質疑:時代變了,別用 Clean Code 那套要求我們了

作者 | 褚杏娟、核子可樂

不久前,遊戲引擎資深研發 Casey Muratori 發表文章 「乾淨」的代碼,賊差的性能後,引發了大量開發者討論。 Casey 還發布了一個 20 多分鐘的視頻:

https://www.youtube.com/watch?v=tD5NrevFtbU


隨後,經典的《代碼整潔之道》一書的作者 Robert C. Martin(20 世紀 70 年代初成為職業程式設計師,世界級編程大師,設計模式和敏捷開發先驅,後輩程式設計師親切地稱之為「Bob 大叔」)也加入了這場「乾淨代碼」與性能之間的論戰中。他在推特上發文稱:


最近有人將 Clean Code 等同於過度工程。這當然是一種矛盾修飾法。根據定義,過度設計的代碼就是不乾淨的。這不禁讓人懷疑,那些大聲抱怨的人是否真的研究過他們抱怨的對象。


對此,有網友提出:將一個 150 行的函數分解成一堆僅由該函數調用的小方法是否被認為是過度工程?Bob 大叔回應道,「這完全取決於工程師的目標。如果是為了可讀性和表現力,那這樣的分解也是可選的。但如果是為了性能,那這種分解可能不是最優的。」


Bob 大叔隨後還發表了一系列觀點,然後補充說他忘了 @ Casey ,兩人因此有來有回地展開了一次「對話」。看過他們對話的網友表示,「幾乎所有涉及 Casey Muratori 和 Jonathan Blow (註:業內知名的獨立遊戲開發者)的話題都可以歸結為:


  • Casey 和 Jon Blow 在遊戲專業領域工作。他們都擅長這些事情。
  • 他們認為他們自己領域內的真理是普遍的。
  • 在 http 伺服器領域的編程技術,不同於遊戲引擎或遊戲渲染領域,這樣對比討論沒有意義,他們都認為對方的技術是「錯誤的」。
  • http 伺服器編程大多是「無狀態」的,遊戲領域的這些技術被推送到他們面前,這讓大家開始失去理智。雙方在某種意義上都是正確的,但雙方都認為對方是錯誤的。雙方都認為他們在談論同一個話題,而實際上他們在談論不同的話題。
  • 無限重複。


最後,這位網友還加了一句讓 Bob 感到扎心的話:「不過,這並不是專門為 Clean Code 辯護,那本書非常糟糕。」


隨後有網友也開始「歪樓」對 Bob 大叔的《代碼整潔之道》評價起來:我不是父母輩兒的,但我讀過這本書,並有一個建議:避免去讀它。「嚴格地說,這本書並不全是壞的。書的大部分內容相當合理,據我回憶,其中一些建議實際上很好。問題是,唯一能區分好壞的是那些壓根就不需要這本書的人。其餘的人註定只能看到表面價值,最終我們成了盲目遵循原則的、堅定的狂熱者,這些原則使他們的程序比原本做的大 3~5 倍(甚至沒有誇張)。」


下面是他們兩人圍繞 Clean Code 的「對話」,用講理的方式表達了對自己認為的「乾淨代碼」應該是什麼樣的,其中充滿了對對方的不服。

第一回合


Casey :我們都不在一個頻道上。

Bob 大叔:我沒感覺到。你說的不準確,但不重要了。


Casey :在回應之前,讓我們先做一點澄清。你提到的關於清潔代碼的大部分解釋,我在視頻里也都有提到——比如更傾向於繼承層次結構,而不是 if/switch 語句;不公開對象內部(也就是「迪米特法則」)之類。但好像你對我的說法很意外,所以在正式討論類型設計之前,能不能先解釋一下這個問題?這樣我才能明白為什麼咱們老是對不上頻道。


Bob 大叔:對不上頻道嗎?我倒沒這種感覺。我只看了你視頻的前半部分,然後就感覺我已經看明白了。我發過一條回復,說你的分析基本是對的。我還說過你在描述「清潔代碼」時的措辭不太準確,但我已經不記得具體哪裡不準確了,反正也不重要。


總之,我想說的是,你展示的結構並不是那種能擠出每一納秒極限的最佳性能設計方式。實際上,這些結構可能會浪費掉很多納秒,或者說執行效率根本就沒到納秒那個層次。在當初那個每一點提速空間都很重要的時代,我們會非常精心地規劃函數調用開銷和間接成本。如果可以,我們甚至會解開循環,特別是在嵌入式實時環境當中。


但如今,這種環境已經非常少見了。絕大多數的軟體系統所消耗的性能還不足現代處理器的 1%。更重要的是,處理器既便宜又容易獲取。這些事實,改變了開發工作的基本思路——關注重點從程序性能轉向開發效率,以及開發者構建系統並保持系統穩定運行的能力。正是這些需求,讓「清潔代碼」這個概念全面起飛。


對大多數組織來說,幫程式設計師節約時間比幫計算機節約 CPU 周期更有經濟價值。所以如果非要說咱們之間「對不上頻道」,那可能就是在優先級判斷上。如果你是想儘量榨取每一納秒的提速空間,那清潔代碼確實沒用;但如果你是想儘量提升開發團隊每個工時所對應的生產力,那清潔代碼往往就是達成目標的有效策略。


Casey 顯然還沒有被說服:能不能講得更具體點,免得咱們再有什麼誤會。可以列舉幾個具體的軟體示例嗎?比如,假設我們都熟悉的 Visual Studio 和 CLANG/LLVM,這些也符合你之前提到的、絕大多數軟體所占用的資源不足現代處理器 1%的情況嗎?


Bob 大叔:那不是,我覺得 IDE 是一類非常專業的軟體系統,屬於極少數情況。


IDE 這東西非常有趣,因為它涵蓋的範圍太大、涉及的情況太多。其中有些部分需要摳到幾個納秒,但也有些部分根本就不在乎性能波動。現代 IDE 必須能在用戶敲擊鍵盤的同時解析大量代碼,這個解析的過程就很挑性能,從而保證跟得上開發者的輸入速度。但另一方面,配置對話框部分的代碼就不怎麼講究效率。


順帶一提,IDE 編譯引擎所強調的效率,更多在於算法效率而非循環精益。循環精益代碼雖然能把效率提高一個數量級,但選擇正確的算法能直接把效率提高好幾個數量級。


另外,我說的那種資源占用不足現代處理器 1%的軟體,其實是程式設計師們經常用到的常規系統。比如一個網站、一個日曆應用、一個流程控制儀錶板(管理簡單流程)等。實際上,幾乎任何 Rails 應用、Python 或者 Ruby 應用,甚至是大部分 Java 應用,都屬於這類。而且它們全都不符合把性能推向極限的要求。


我目前的首選語言是 Clojure,它的速度只有等效 Java 程序的 1/30,沒準只有同等 C 程序的 1/60。但我不在乎,畢竟我可以在必要時隨時轉去用 Java。另外,對很多應用程式來說,換個更強的處理器反而是最便宜、最簡單的辦法。總之,我覺得幫程式設計師節省時間才是目前最主要的降本方向。


但千萬別誤會我的意思。我也是上世紀 70、80 年代成長起來的老彙編玩家和 C 用戶了。在必要時,我也會認真計算微秒級別的差異(納秒這個太誇張了,人類幾乎把握不住)。所以我知道循環精益代碼的重要性。但今天的處理器比我們當時用的設備快上萬倍,所以對於現在的大多數軟體系統,我們更傾向於「浪費」一點 CPU 周期,來換取程式設計師的幸福生活。


第二回合


Casey:那你的意思是 XXX ?

Bob 大叔:我不完全同意。


Casey:如果我理解得沒錯,你的意思是說軟體可以分兩大類,具體情況要歸類之後再分析。從這個角度看,我日常用的大多數軟體其實都屬於「每一納秒都很重要」的類別,比如 Visual Studio、LLVM、GCC、Microsoft Word、PowerPoint、Excel、Firefox、Chrome、fmmpeg、TensorFlow、Linux 、Windows、MacOS、OpenSSL 等。你同意嗎,就是說這些軟體都需要高度關注性能?


Bob 大叔:不完全同意。相反,我的經驗是,大多數軟體之內還是要再細分來看。某些模塊需要在納秒級周期內執行,其他模塊的響應時間則可以容忍微秒、毫秒甚至更長。是的,有些模塊甚至在響應時間不超過 1 秒的情況下都是沒問題的。


大多數應用程式都由多個模塊組成,不同的模塊對應不同的應用範圍。例如,Chrome 就必須快速完成渲染。在填充複雜的網頁時,每一微秒都很重要。另一方面,Chrome 中的首選項對話框就相對不強調性能,響應速度到毫秒級別也完全可以。


如果我們把特定應用程式中的各個模塊按響應時間列成一份直方圖,應該會看到某種非正態分布。部分應用程式可能包含大量的納秒級模塊和少量毫秒級模塊,其他應用程式的大部分模塊則可能都在毫秒級別,只有少數在納秒級別。


例如,我目前正在開發一款應用程式,其中絕大多數模塊有毫秒級的響應就很好了;但也有少數模塊的性能要求是其他模塊的 20 倍。我的策略就是用 Clojure 編寫毫秒級模塊,因為雖然速度不快,但它卻是種非常方便的語言。微秒模塊用 Java 來寫,速度更快但沒那麼方便。


有些語言和結構,實際上就是對裸機的抽象結果,能幫助程式設計師專注於解決問題本身。比如說,程式設計師不用分心去優化 L2 緩存的命中率,這時候他們編寫毫秒級代碼的效率就要高得多。相反,他們可以更多關注業務需求,特別是幾年之後其他人接手項目時能不能看懂代碼、接管維護。


也有一些語言和結構會直接跟裸機映射,這樣程式設計師就能更輕鬆地通過算法極限壓榨剩餘性能。這類結構的編寫、解釋和維護難度往往更高,但如果編寫的對象確實需要納秒級別的性能,那就必須承受這一切。


當然,這只是兩種極端情況,大部分軟體和語言其實介於二者之間。所以作為開發者,我們應該了解這些環境,明確知曉哪種環境最適合當前的實際問題。


大約十年前,我寫過一本書,名字就叫《Clean Code》。其中更多關注的是毫秒級別的問題,而非納秒級別的問題。在我看來,直到現在,程式設計師生產力都是相對更重要的問題。書里討論了多態和 switch 語句之間的優劣取捨,用了整整一個章節。我想再援引其中的總結性論述,「成熟的程式設計師知道,一切對於對象的理解都是不可靠的。有時候,我們真正需要的只是簡單的數據結構和能操作它們的過程。」


Casey:好吧,那我再調整一下自己的說法。Visual Studio、LLVM、GCC、Microsoft Word、PowerPoint、Excel、Firefox、Chrome、fmmpeg、TensorFlow、Linux、Windows、MacOS 和 OpenSSL,對於這些程序、至少是程序中的某些模塊,「毫秒級別的性能就夠了」,對嗎?


Bob 大叔:毫秒?當然是對的。我也承認這些程序里還有不少微秒級甚至是納秒級的模塊。但大多數情況下,毫秒級就夠了。


第三回合


Casey:行,那讓我們說點別的。

Bob 大叔:讓我給你點「溫馨提示」。


Casey:很好,看來我們已經在軟體類別方面達成了一致。我想描述一下你所提到的編碼實踐的特徵。因為你說的特徵也適用於 LLVM 之類的軟體,所以我就以它為代表。而且 LLVM 恰好是開源的,所以我們能明確知曉它的工作和構建原理(Visual Studio 就不行)。


我覺得你在回復、書中和講座里都在強調同一個觀點,就是說在對 LLVM 這樣的大型軟體進行編程時,程式設計師不用太關心性能。他們應該更多關注如何提升自己的開發生產力。如果說場景只限定在你之前提到的簡單日曆應用層面,那這麼說沒有問題。但在 LLVM 當中,確實存在著「納秒/微秒/毫秒很重要」的情況,所以程式設計師早晚都得認真思考性能優化,否則他們一定會發現程序運行起來太慢。


假定有人想用 LLVM 構建一款真正的大型程序,比如虛幻引擎或者 Chrome 等。在這種情況下,假設性能問題出現在代碼中的某些孤立部分(也就是你之前提到的「模塊」),那現在肯定應該將這些部分重寫為性能取向。


這就是我對你之前論述的理解,包括「如果我的 Clojure 代碼太慢,我隨時可以轉向 Java」,也就是說如果某個部分需要更高的性能,你就會用 Java 進行重寫。


我的理解準確嗎?


次日,Bob 大叔在回答這個問題之前先說道:


順帶一提,前幾天我回去看了你的完整視頻。我覺得既然咱們決定參與討論,那我確實該認真研究一下你的觀點。先說結論:我對你的某些言論難以苟同,接下來會用「溫馨提示」的方式做點補充。


Bob 大叔:首先,這些幾何形狀的面積都能用相同的基本公式(KxLxW)來計算,這個太妙了。應該只有程式設計師和數學家才能欣賞其中的美感。


總的來說,我認為你的視頻很好地解釋了程式設計師在環境資源受限時,需要如何找到出路。很明顯,在資源豐富的環境中,人們不會專門選擇 KxLxW 這種解法,畢竟大家不確定場景中會不會引入其他形狀。另外,就算是問題範圍就穩定在符合 KxLxW 的情況之內,使用更傳統的公式也能降低其他程式設計師的理解門檻。畢竟這是種很少見的算法,人們往往會感到困惑甚至重新做驗證。我承認,這個驗證的過程是美妙的、甚至堪稱「尤里卡」時刻,但上班都挺忙的,最好不要無謂地占用寶貴時間。


我不知道你有沒有讀過 Don Norman 的作品。很久之前,他寫過一本名叫《日常事物的設計》的書,相當值得一看。他在書中陳述了這樣一條經驗法則:「如果你覺得某種事物精巧而複雜,請當心——它可能是自我放縱的結果。」所以在資源豐富的環境裡,我覺得 KxLxW 的解法就屬於這種情況。


Bob 大叔:我是敏捷宣言的簽訂者之一,他們堅信前期架構和設計非常重要。


按你提到的這個案例,那我可能得再具體考慮一下,包括尋找可能出現性能問題的地方並更多關注這些模塊。比如,我可能已經開發了一個精簡版的模塊,再把它放進壓力測試里看行不行。當然,我最擔心的永遠是花了大量的時間和精力,但因為選擇的方法不對,所以最終沒能滿足客戶需求(這事在我自己身上也發生過)。


總而言之,對於這類複雜問題,揪住單一因素做分析永遠是不夠的。沒有唯一正確的方法,這一點我在《Clean Code》中也曾多次提出。


第四回合


Casey:說了半天都沒回答我的問題。

Bob 大叔:你說得有道理,不過我昨天就在課堂上講過這個問題了,謝謝。


Casey:一邊看你的回覆,我腦子裡已經跳出了很多問題。但你最後說得確實很好,所以就從這裡入手吧。


在對話中,你談到軟體架構中有幾個性能關注點:IDE 解析器更多關注「納秒級」,所以應該把「模塊」劃分成納秒/微秒/毫秒等幾個響應要求級別。你還建議程式設計師可以先創建一個「精簡版」模塊,再通過壓力測試分析其運行效果,最終編寫軟體以確保性能維持在可以接受的範圍。甚至可以根據性能需求選擇不同的語言,比如你在例子中說的 Clojure、Java 和 C 語言。總結來講,你的觀點就是「所以作為開發者,我們應該了解這些環境,明確知曉哪種環境最適合當前的實際問題。」


聊了這麼多,我想回歸最開始的問題:對於我把「清潔代碼」放在追求性能的對立面這件事,你為什麼會表現得很驚訝?你說了半天,完全沒有提到這方面的觀點。


當然,你的書里和博文里也肯定過性能的重要性。但從數量上看,你剛剛講的這一切,但你以往的表達里都占比很低。比如說,這是包含六個段落、總長好幾個小時的視頻合集,即《Clean Code》系列講座。在長達九個小時的內容中,你從來就沒提到過前面回復的這類內容:


https://www.youtube.com/playlist?list=PLmmYSbUCWJ4x1GO839azG_BBw8rkh-zOj


如果你對性能問題真像回覆中表現得這麼重視,那為什麼在九個小時的課程里都沒拿出哪怕一個小時,專門給聽眾們解釋一下優化性能、提前設計的現實意義?比如代碼可能會對性能產生影響,應該如此避免對性能有害的編程結構之類,包括你在回覆中提到的應該預先建立性能測試等等。


或者從另一個角度提問,你是不是覺得性能的意義是理所當然、無需贅述的,所以你沒有給予特別的強調。但你的聽眾對性能並不熟悉,這種偏廢難道不會妨礙他們在正確的時間考慮這個正確的問題嗎?畢竟你都開始做「作為開發者……」這樣的總結了,多少應該提一提吧?


Bob 大叔:坦率講,我認為你的批評有道理。而且巧的是,我昨天在課堂上,正好花了很多時間討論軟體開發學科中的性能成本和生產力收益問題。謝謝你的監督。


但要說「意外」,我覺得不準確。不過畢竟只是個語氣詞,沒必要糾結下去。


你問我是不是一直覺得性能永遠重要,根本沒必要強調。通過自我反省,我覺得可能真是這樣的心態。我不是性能方面的專家,我的專長是幫助軟體開發團隊高效構建和維護大型複雜軟體系統,為他們提供實踐、原則、設計思路和架構模式。正所謂「拿起錘子,看什麼都像釘子」,我們可能都習慣於從自己的專業角度出發看待問題。


至於我很少強調性能意義,其實換個角度看,這又何嘗不是另一個錘子和釘子的故事呢?因為你是性能調優方面的專家,所以總喜歡從這個角度看待問題,都差不多的。


但我還是要承認,這段討論比我當初的預期更有助益,也讓我的觀點發生了變化——雖然變化不是特別大,我也沒覺得我的《Clean Code》系列教程真有那麼差。總之,如果你從頭到尾把這九個小時的內容看下來,就會發現我在其中多次提到過性能問題,而且至少有兩、三次達到了能讓你認可的強調程度。


因為就像你猜測的那樣,我確實認為性能問題很重要,需要進行預測和規劃。


第五回合


Casey:就這樣吧,我不想聊了。

Bob 大叔:你啟發了我單行過長引起的性能問題。


Casey:老實說,性能調優就是我的一切:)不開玩笑,就在 GitHub 上編輯這段回復的同時,我發現因為輸入的段落行數過多,頁面已經開始卡頓了。就幾百個字,但因為在系統里疊層太多,本該瞬時完成的操作就慢到影響使用。這也是我如此強調性能的原因之一——就在當下,即使是那些非常簡單的軟體功能,也經常會慢到無法使用。這絕不是我瞎編的,你可以看看我錄製的這段視頻,看我在輸入回復時頁面卡成了什麼樣子:


https://www.youtube.com/watch?v=gPgm28zXNEE


我用的可是 Zen2 晶片,速度快得很!所以我會抓住一切機會宣傳性能的意義,其中沒準就蘊藏著改善體驗的可能性。很多組織絕對不會考慮「納秒/微秒/毫秒/秒」之類的性能分級,但我想說,拜託你們考慮考慮吧。只要能把性能這個觀念植入他們的腦袋,並幫助他們獲得解決問題的能力,那將是對整個世界的重大改進。


所以我覺得要聊的主題到這裡就差不多了。如果你想繼續聊,那接下來可能就要延伸到架構領域了。那是比性能更寬泛的討論空間,如果你想要,我也很願意奉陪。


Bob 大叔:你這段視頻也太誇張了,cps 可能都沒有 25。我想問問你用的是什麼瀏覽器。我正在用的是 Vivaldi(Chrome 的一個 fork),雖然不像你那麼可怕,但輸入延遲也挺誇張的。所以我做了一些實驗,事實證明延遲跟文件大小無關,倒是跟段落長短有關。我在同一段落中鍵入的內容越多,其長度就越長,延遲也就越厲害。


那為什麼會這樣?首先,我想我們都在輸入相同的 JavaScript 代碼,畢竟沒人會繼續用瀏覽器里編寫的工具了。其次,我覺得這段代碼的作者從沒想過會有人把整個段落搞成單行形式(請注意左側的行號)。即便如此,在 25cps 的速率下,到 200 至 300 字符時延遲也會變得非常明顯。這是怎麼回事呢?


會不會是因為程式設計師用的是某個質量不佳的數據結構,每次延長都為其分配一個新的內存塊,之後把數據複製到新塊中?我記得舊 Rouge Wave C++庫就是這樣處理不斷增長的字符串的。總之,這延遲實在是太誇張了。


當然,這更像是個算法問題,而不是單純的性能問題。畢竟如果軟體運行得太慢,大家首先想要檢查的肯定是算法。但你的觀點確實有道理,寫這段代碼的程式設計師沒想到自己的功能會被用戶如何使用,所以在處理意外負載時表現很差。


所以,也許我應該從現在開始,每行結束都打個回車。


總之,你的回覆啟發我意識到了這個單行過長引起的性能問題。現在我會把單行字符限制在 80 個以內,這樣無論是多低端的晶片,應該都不會再卡頓了。


兩人對話到此結束了。不過在昨天的推特中,Casey 表示:


我有個壞消息要告訴大家。會有……更多的視頻。引起騷動的那篇文章只是我課程序言中被刪掉的內容。如果我們要全面討論乾淨的代碼和性能,那麼做好準備,因為我可以在接下里的一個月時間裡都做這個。


「不管我個人對 Bob 或 Casey 的感覺如何,我真的很喜歡這種形式,讓兩個意見不一致的人通過編輯共享的對話文件來協作進行對話。我現在真的很想自己試試這種方式。」有網友表示。


原文連結:

https://github.com/unclebob/cmuratori-discussion/blob/main/cleancodeqa.md


本文轉載來源:

https://www.infoq.cn/article/sDM66GVhWrrJ0vGa5Z19

關鍵字: