Goodbye Clean Code,這是對代碼編寫與重構的新感悟
- 2020 年 2 月 14 日
- 筆記
機器之心報道
參與:思
乾淨的代碼是我們的目標嗎?不,可能冗餘一點的代碼才更好讀。

Clean Code,顧名思義就是整潔的代碼,或者說清晰、漂亮的代碼,相信大多數開發者都希望自己能寫出這種類型的代碼。
那麼我們為什麼需要 Clean Code 呢?這裡需要明確的是,寫代碼並不只是用來跑一跑或實現某些功能,寫代碼更重要的是便於維護。所以從這一點出發,寫代碼就像寫一篇文章,閱讀體驗應該非常流程,邏輯承接應該非常順滑。
現在有兩種文章會讓我們閱讀體驗不難么好,一種是過度冗餘,有一些簡單的概念經常出現,讓我們會覺得文章不夠精鍊。另一種是太精鍊了,每一部分之間的邏輯跳躍比較大,或者說我們很難跟上作者的敘述邏輯,那麼這樣的文章看起來會顯得生澀難懂。
代碼也一樣,有些重複使用的方法可以編入相同的函數,同類函數之間的關係可以編入類與對象。這樣代碼整體能顯得更加「乾淨」。但需要注意的是,並不是說最緊湊的代碼就是最好的,例如類的繼承,如果說讀懂當前類需要往上翻好幾個類,這種體驗並不友好,似乎代碼的邏輯跳躍讓人很難跟着走。
所以 Clean Code 到底好不好?

根據列表推導式(first method)精簡代碼(second method)。
React 主要維護者 Dan Abramov 也在考慮這件事,其中 React 是 Facebook 維護的 JavaScript UI 庫。他在博客上寫了一篇對 Clean Code 的反思,這篇文章在 HackNews 上獲得了非常熱烈的反響。下面我們具體看看 Dan Abramov 眼中的代碼編寫準則。
精簡的魅力
已經到了深夜,我的同事正在檢查這一周寫的所有代碼。他們在做的東西可以理解為,通過拉拽圖形邊緣的小控件來變成矩形和橢圓形等形狀,代碼本身是沒有問題的。
但似乎代碼的重複性有點多,每一個形狀都有一組不同的控件,從不同方向拉拽每一個控件都會以不同的方式影響形狀的位置與大小。如果用戶按住了 Shift 鍵,那麼在改變形狀的同時還應該展示各種屬性。實現這些能力需要如下一系列數學計算:
let Rectangle = { resizeTopLeft(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeTopRight(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeBottomLeft(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeBottomRight(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, }; let Oval = { resizeLeft(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeRight(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeTop(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeBottom(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, }; let Header = { resizeLeft(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeRight(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, } let TextBlock = { resizeTopLeft(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeTopRight(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeBottomLeft(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeBottomRight(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, };
我們可以看到上面已經是簡化的代碼了,每一條計算語句下都有 10 條幾乎重複的計算過程。這樣的重複性計算代碼看起來就很冗餘,它也不是一種 Clean Code。
我們很容易發現,大多數重複代碼都有相似的屬性。例如 Oval.resizeLeft() 與 Oval 的其它方法都非常相似,因為它們都是橢圓的某些屬性或操作。其它 Rectangle、Header 和 TextBlock 也會有重複,因為文本框都是矩形的。
所以我們可能會想到,能不能通過組合與合併的方式將所有重複項都刪除掉?我們大概能得出下面的這種代碼:
let Directions = { top(...) { // 5 unique lines of math }, left(...) { // 5 unique lines of math }, bottom(...) { // 5 unique lines of math }, right(...) { // 5 unique lines of math }, }; let Shapes = { Oval(...) { // 5 unique lines of math }, Rectangle(...) { // 5 unique lines of math }, }
組合成各種行為特性:
let {top, bottom, left, right} = Directions; function createHandle(directions) { // 20 lines of code } let fourCorners = [ createHandle([top, left]), createHandle([top, right]), createHandle([bottom, left]), createHandle([bottom, right]), ]; let fourSides = [ createHandle([top]), createHandle([left]), createHandle([right]), createHandle([bottom]), ]; let twoSides = [ createHandle([left]), createHandle([right]), ]; function createBox(shape, handles) { // 20 lines of code } let Rectangle = createBox(Shapes.Rectangle, fourCorners); let Oval = createBox(Shapes.Oval, fourSides); let Header = createBox(Shapes.Rectangle, twoSides); let TextBox = createBox(Shapes.Rectangle, fourCorners);
上面這種方法只會有一半的代碼量,重複代碼基本都被刪除了,所以它是乾淨的。如果我們想改變圖形方向或形狀等特定屬性,我們只需要修改一段代碼,而不需要到處更新這個函數。
現在代碼過於重複這個問題解決了,我們可以開心地把它提交給代碼庫。並且因為寫了更簡潔的代碼,我們可以帶着成就感上床睡覺了。
事情並不那麼簡單
但是等等,到了第二天,你會發現事情並不簡單。可能老闆會找你談話,委婉地想要你撤回昨晚重構的乾淨代碼。但這出現了什麼問題?重構的代碼非常簡單,它比之前一堆亂麻的代碼漂亮很多。
當時你很可能心不甘情不願,但幾年後,你會發現老闆的觀點才是正確的。
痴迷於「Clean Code」並刪除重複代碼是我們都會經歷的一個過程。當我們對代碼感到不太自信時,我們很容易將自我價值與自信感聯繫到一些可衡量的標準。例如一組嚴格的代碼規則、一個確定的命名策略、一個明確的文件結構和沒有重複的「乾淨」代碼等。
在實踐中,我們很自然地想着刪除重複代碼。我們通常知道每一次修改代碼後的長短變化,因此移除重複代碼可以提升一些客觀的代碼度量標準。不過糟糕的是,這種現象擾亂了我們的認同感:「雖然難懂一些,但我現在是在寫一種乾淨的代碼。」
一旦我們學會了創建 Abstraction,就很容易對這種能力產生很高的期望,並且每當我們看到重複代碼就會想起一種「高效」的抽象方法。經過幾年的代碼經驗後,我們一眼就可以看到各種重複代碼,抽象就是我們新的能力。如果有人告訴我們抽象是一種美德,我們就會欣然接受它,同時也會因為別人不崇尚「清潔代碼」而對他們品頭論足。
現在,我們要考慮到,「重構」是一場災難,它主要體現在兩方面:
首先,我們並不能和寫代碼的人直接交流,我們只是重寫代碼,並簡要地檢查它。即使這是一種進步,那也是一種非常糟糕的方式。一個健康的工程團隊需要不斷建立信任,在沒有討論的情況下重寫同事的代碼是對協作的一個打擊。
其次,什麼都是有成本的,我們的代碼會在後續的修改需求與降低重複性上進行權衡。還是之前的例子,如果我們需要為不同形狀上的不同控件提供許多特殊能力。那麼我們的抽象需要複雜好幾倍才能完全囊括它們,而在最初「冗餘」的代碼中,添加新行為簡直就是小菜一碟。
這難道意味着我們需要寫「不幹凈」的代碼?並不是的,我們需要仔細思考到底「乾淨」和「不幹凈」指的都是什麼。
寫代碼就是一段旅程,我們需要考慮這段旅程到底需要走多遠,也需要考慮我們現在的位置又在哪。如果我們第一次通過函數或重構一個類來令代碼變得更簡單,那麼會獲得很多滿足感。如果我們對自己代碼感到比較滿意,那麼追求更乾淨的代碼是非常好的,我們可以在這個階段持續做一段時間。
但不要止步於此,不要做一個乾淨代碼的狂熱推崇者。乾淨的代碼並不是最終目標,只是我們在處理複雜系統的一個嘗試。我們可能並不知道這種修改最終對代碼庫有什麼樣的影響,但是乾淨的代碼會指引一條明路,至少這個方向是對的。
乾淨代碼可以指引我們,但熟悉後應該放鬆它的指引。代碼庫整體的邏輯與風格,整體的可讀性與修改便捷性,才是我們該追求的。
參考鏈接:
https://overreacted.io/goodbye-clean-code/ https://news.ycombinator.com/item?id=22022466
https://zhuanlan.zhihu.com/p/25541626
本文為機器之心報道,轉載請聯繫本公眾號獲得授權。
✄————————————————
加入機器之心(全職記者 / 實習生):[email protected]
投稿或尋求報道:[email protected]
廣告 & 商務合作:[email protected]