「好程式」跟你想的不一樣!一文,我們知道重構是「提升程式碼的可讀性,寫出容易修改擴充的好程式」的有效手段,接下來,你得先知道什麼叫壞程式,才寫出好程式。

Kent Beck 用「壞味道』(Bad Smell,中譯本把它翻成程式異味,但我偏好最早聽 Ruddy 老師採用的翻譯 - 壞味道)傳神地形容該動手進行重構的時機。充滿壞味道的程式,通常不是好程式。當你在閱讀程式碼時,若感覺某段寫法令人不悅,彷彿飄出一股臭味,甚至使人作嘔,通常就意味著該思考是否要重構消毒除臭一番。你必須擁有敏銳嗅覺,才能精準揪出程式的壞味道進行重構,也因此重構一書用一整章介紹了「壞味道」。

註:用味道一詞來比喻還挺傳神的,味道其實也帶點主觀成分,無絕對對錯,團隊有共識即可。

  • 入鮑魚之肆,久而不聞其臭
    如果工作團隊已習慣某種寫法,並不影響開發效率,它就不是壞味道。
  • 海畔有逐臭之夫
    我的美味對你可能是臭味(掏出臭豆腐、榴槤跟瑞典鯡魚罐頭),別人看不慣的怪寫法,工作團隊用來得心應手,那又何妨?

以下整理書裡提到的壞味道:
(註:未依據書本原文,算是我自己的體會與詮釋,走黑大風。書本原文有順便提了對映重構手法,此處忽略不提,只聚焦其負面影響。)

Mysterious Name 神祕的命名

命名是寫程式最大的難題,如何為類別、變數、屬性、方式取個簡短易懂意義完整的名字,常讓許多程序員還沒動手寫 Code 就陷入困境。但看了不知所云甚至會錯意的命名絕對是坑。

Duplicated Code 重複的程式碼

這個不用多說,跟大便一樣,解放時很爽快,但之後可臭了。重複的程式碼來自 Copy & Paste,意味將來要調整遇輯得同時修改很多地方,萬一漏改爆炸還得擦屁股。

Long Function 又臭又長的函式

函式常因不斷擴充遇輯變得又臭又長,職責也開始龐雜;相形之下,拆成多個小函式,函式較易命名也方便重複利用組裝其他功能。「呼叫副程式(Subroutine)比直接執行多耗損效能」是過時想法不需再納入考量,但要追進子函式才看得到邏輯的副作用仍在,這點可靠可望文生義的良好函式命名彌補。
參數與暫時變數不利於提取函式,如此可以解釋前文裡為什麼要把 play 暫時變數改成直接呼叫 playFor(perf) 三次。
條件式與迴圈也是值得提取成函式的徵兆。switch 可以考慮每個條件式提成一個函式。
迴圈含內部的程式碼可提取成函式,但前題是迴圈內處理性質要單一,若包含兩種不同工作,建議先拆成兩個迴圈再提取函式。嘿,我們現在也知道前篇文章為什麼要計算總金額跑一次,計算紅利積點跑一次拆成兩個迴圈了吧?

Long Parameter List 冗長的參數項目

需要一堆參數的函式(例如:SendMessageg(msg, recipient, sender, senderIp, scheduledTime, channels, attachments, logContent))看起來就很難搞,呼叫時需注意數量、順序,修改調整參數時呼叫端需配合修改,工程也會變大。

Global Data 全域變數

這點也是惡名昭彰,不需多做解釋的壞味道。你無從掌握及追蹤全域資料在何時被誰改掉,爆炸時查 Bug 也常查到歸藍波火。

Mutable Data 可變資料

一個經典例子是函式接收的參數,被函式前段程式貪圖方便直接改掉內容,後方程式預期它是外界傳入的原始值,因而出錯。

Divergent Change 發散式修改

當系統有某個需求規格發生異動,最佳狀況是只跳到某段程式進行修改就搞定,如果需要修改多個地方,通常就帶著發散式修改的壞味道。舉個例子,換一種資料庫或增加一種金融產品,需要修改的地方愈多愈臭;至於前陣子示範過EF Core 換資料庫只要改一行,真香!

Shotgun Surgery 霰彈槍式修改

類似發散式修改,更棘手。每次修改時,要在眾多類別裡進行一大堆小規模修改,找出所有要修改的地方是場惡夢,漏改爆炸則如鬼壓床。

Feature Envy 依戀情節

指模組 A 的函式經常與模組 B 的函式互動,甚至比存取模組 A 內函式的頻率還高。如此模組 B 在修改時得顧及模組 A 會不會受影響,違反觀注點分離原則。最簡單的改善法則是「將永遠一起變化的東西放在一起」。

Data Clumps 資料泥團

幾個資料項目總是成群結隊一起在很多地方出現,例如類別 A、類別 B,函式 C、函式 D 都出現 LoginId, ClientIp, Channel 三個屬性或參數。因要修改時需要改多處,故可考量將其包成物件。有個測試指標是試著移除其中一個資料項目,若會導致資訊不完整,代表你該把它們轉成物件。

Primitive Obsession 基本型別偏執

許多程式設計師有「要儘量使用程式語言內建型別(int, string, DateTime...),不要發明自訂型別」的莫名偏執(呵,我就有)。這有時會導致一些副作用,例如:金額當成純數字處理時忽略幣別差異、長度比較時忽略單位(公尺、公分、吋)、儲存電話號碼時將區碼/電話/分機存成單一字串。為金額加幣別、長度加單位、區碼/電話/分機設計專屬型別。
【不同觀點】引進自訂型別會增加複雜度,違反 KISS 原則,我會選擇確認能帶來好處再動手。

Repeated Switches 重複 switch 邏輯

物件導向狂熱分子主張所有 switch 都該用多型(Polymorphism)取代,但作者認為當代語言的 switch 已支援精密控制,不致罪大惡極,故只聚焦在「重複出現相同條件組合的 switch 或 if/else」建議用多型取代之,以避免增加一個 case 條件要修多處。

Loops 迴圈

建議用篩選器(JavaScript filter、LINQ Where)、對映函式(JavaScript map、LINQ Select)取代 for 迴圈,語意上更清楚。

Lazy Element 冗員元素

為了預留未來變動擴充或重複使用而加入的類別或函式,類別裡面只有一個函式或函式內只有一行跟函式命名一模一樣的程式碼,顯現其多餘性。
【不同觀點】若確有擴充或修改的可能性,且並未造成困擾,我認為保留無妨。

Speculative Generality 畫大餅

為了預防將來某天會發用到而加入的奇巧設計或特例處理,導致系統不易理解與維護,例如:未使用的抽象類別、用不到的函式參數。如果某個方法唯一的呼叫來源只有測試程式,就是項徵兆。
【不同觀點】應視其將來用到機率與其負面影響決定去留,移除後將來真的用到要加回是項工程也是浪費,如何取捨是藝術,端賴看設計者的經驗、直覺與運勢。(命中帶賽例子:重構刪掉放了十年沒派上用場的預留抽象類別,下個月遇到新需求需要用到,啊幹)

Temporary Field 暫時欄位

某個欄位只在特定情況下會有值,造成程式不易被理解,開發者覺得它是個沒用的欄位,或是未料到它沒有值而存取出錯。

Message Chains 過度耦合的訊息鏈

呼叫物件 A 方法取得物件 B、再呼叫物件 B 方法取得物件 C,造成呼叫者與過程中涉及的結構過度相關,一旦 A、B、C 要修改,都會影響呼叫端。

Middle Man 中間人

封裝通常會伴隨著委託(Delegate)。例如:Manager.IsAvailable(DateTime someday) 封裝了內部去查詢行事曆或詢問祕書的動作,但這個 IsAvailable() 方法內部只是轉達 Calendar.IsAvailable(someday) 的執行結果。有時因封裝造成類別的大半方法都是委託給其他類別執行,會讓類別的中間人角色嫌得多餘,可以考慮讓呼叫端直接存取受委託的其他類別。

Insider Trade 內線交易

模組間建立高牆減少資料交換可降低耗合性,資料交換無法完全避免,但要盡量減少或搬到檯面上,避免私下交換。如果兩個模組間有大量交談,可建立第三個模組來處理它們,讓這部分透明化且易於管理。另一種情境是父類別與子類別間有過度緊密相依性,可考慮加入委託類別將其隔離。
註:這部分不管原文或譯作都極其言簡意賅,我只能盡力推敲了。

Large Class 肥大的類別

企圖做太多事的類別往往有過多欄位,導致重複的程式碼。解決之道是將相關聯高的變數抽取出來變成子類別、用繼承關係提取出子類別、將部分功能抽取成獨立類別,讓每個類別的程式碼少一點。

Alternative Classes with Different Interfaces 異曲同工的類別

簡言之,兩個功能相似的類別介面有差異,導致不能替換使用。例如:類別 A 跟類別 B 有個方法功能相似,但名稱或參數不同,先將二者統一再定義一個介面 IBlah 讓兩個類別實作它。在需要用該方法的場合接受 IBlah 型別傳入物件,即可依需求傳入類別 A 或類別 B。

Data Class 無腦的資料類別

指資料類別只無腦地放入資料欄位,卻沒加入必要的管控或行為邏輯。必要時可加上唯讀設定,避免被不當修改;將特定資料行為邏輯寫在資料類別中,則有助於改善設計。

Refused Bequest 被拒絕的遺產

發現子類別並不想繼續父類別全部的方法或資料,只想挑選其中幾個。傳統上多半認定這個繼承關係是錯的,應該建立一個新的旁系類別,讓父類別只保留共用的東西。但新一代觀念是「為了簡便地重複使用一些行為,只要沒有造成混淆或問題,這種壞味道可以忍」,但如果子類別不是拒絕繼承父類別的程式碼,而是拒絕繼承父類別的介面,那麼就該儘速改善。

Comments 過多的註解

並非反對註解,而是反對用註解掩蓋壞味道。當某段程式傳出壞味道,請優先考慮重構改善它,而非加上註解說明就擱置不處理;另一方面,在重構完成後,註解也會變得多餘。

後記

  1. 真心覺得這本「重構」不是一本好讀的著作(至少不合我的口味),尤其是壞味道章節。Fowler 大師的用字言簡意賅(我覺得有「春秋」經文的 fu,我想我們需要一本「左傳」XD),常需從字句、援引的重構手法反覆推敲原意(因此以上如有理解錯誤之處,歡迎大家指正)。閱讀理解花費的時間比我預估多出一倍有餘,超累。
  2. 回到「味道帶有主觀性」的理論,書中所提觀念我們未必要一昩接受。有些壞味道間彼此矛盾的(例如:解決中間人問題可能造成訊息鏈耦合),有些壞味道則會因為時空演變而沒那麼臭(例如:switch 邏輯、被拒絕的遺產),甚至在某些特殊情境,它說不定是香的。
  3. 在了解這些「壞味道」後,不代表我們該看到黑影就開槍,打開程式碼大開殺戒大改一番;正確心態應是了解壞味道之所以是壞味道的理由,推敲其負面影響,再依針對所處情境環境評估對可維護性的傷害,用「大智慧」決定是否要處理... 聽起來超像幹話,但這是事實,我也沒辦法。
    程式開發是門藝術,每一個決策都是取捨,需要知識、經驗、直覺跟運氣,如果靠一本規範或 SOP 就能寫出完美程式,那輪得到我們吃飯的份?

My notes of Martin Fowler's book, Refactoring, this article list the bad smells.


Comments

# by 小孩

好文

# by WILSON

推! 用心分享

# by 路人

GOOD 好文章

# by 味道

壞味道是從印度瑜珈傳進來的,在印度瑜珈裡,大腦中思想、想法被稱為氣味、味道,因此壞味道白話來說就是指壞想法、壞思想。 參考文章:https://isha.sadhguru.org/yoga/yoga-articles-meditation/how-to-control-mind/

Post a comment