程式的複雜度將隨程式的演化而遞增,除非採取措施來保持或降低複雜度。 —— 雷曼的複雜度遞增定律。 p. x
好的設計人員熟悉設計解決方案,而偉大的設計人員明白設計中的問題 (臭味)、導致問題的原因,以及如何應用成熟合理的設計原則來解決問題。 p. x
可利用設計臭味來幫助理解軟體工程師在應用設計原則時所犯的錯誤。 p. xiv
從違反了那些底層設計原則的角度來看待每種臭味時,我們對該臭味將有更深入的認識。更重要的是,這也自然而然地指出了消除該臭味的潛在重構方法。 p. xiv
設計臭味是特定的設計結構,它們違反了基本設計原則,為設計品質帶來了負面影響。 p. 1
技術債是有意或無意地做出錯誤或非最佳的設計決策所引發的債務。 p. 2
在極端情況下,累積的技術債多到再也無力償還,只能放棄該軟體產品。這稱之為技術破產 (technical bankruptcy) p. 2
要求軟體開發人員以更快的速度時做出功能特性 (features)。在這種情況下,開發人員可能沒有機會或時間對設計決策的影響作出正確的評估。 p. 3
技術債影響軟體系統的內在品質,而這種內在品質不是終端用戶可以直接感知的。軟體公司很重視終端用戶,承擔不起失去終端用戶帶來的損失,因此缺陷獲得的關注最大,而與不可見的技術債相關的問題通常被拖延,甚至根本得不到解決。 p. 4
除技術難題外,技術債還會影響開發小組的士氣與積極性。隨著技術債不斷增加,軟體將難以修改,導致開發小組開始氣餒、煩悶。 p. 4
心理學家注意到犯錯會促使人去學習,這種動力源自於發現自己犯錯時的詫異。 p. 9
設計原則為設計人員提供了指南,得以實現有效和高品質的軟體設計方案。如果設計人員違反了設計原則,這種違反將以臭味的方式呈現出來。 p. 12
有時候,架構師和設計人員使用著名的解決方案來解決問題時,並沒有完全明白這些解決方案所帶來的影響。這些解決方案通常是以設計模式的方式呈現,架構師/設計人員急於應用這些模式,卻沒有充分認識到必須平衡各種作用力。 p. 13
軟體黏滯性指的是採用正確的方案解決問題時所必將遇到的「阻力 (即需要付出更多的時間和精力)」。 p. 14
環境黏滯性指的是遵循最佳實踐時,必須克服的軟體開發環境帶來的阻力。 p. 14
我們將 Booch 提出的物件模型中的這四個主要元素視為設計原則,並將它們統稱為 PHAME (Principles of Hierarchy, Abstraction, Modularization, and Encapsulation,層級結構、抽象、模組化和封裝原則)。本書使用的臭味分類方案是根據這些原則而來,因此每種設計臭味都對應於受其負面影響最大的一種設計原則。 p. 16
臭味的名稱都由兩部分組成,其中第一部分為形容詞,它修飾違反的設計原則名稱 (第二部分)。 p. 17
下面是我們透過實踐總結出來的一些重要實現手法,這些實現手法讓我們能夠在軟體設計中應用抽象原則。
使用一系列資料或編碼字串,而不建立類別或介面時,將引發這種臭味。 p. 24
通常缺乏抽象時,相關資料和行為將分散在其他抽象中,這將導致兩個問題:
要透過重構消除這種臭味,可建立在內部使用基本資料型別值或字串的抽象。 p. 26
要確定是否值得建立抽象,可檢查是否存在以下需求 (該清單只列出部分需求)。
每一節的標題即是一種設計臭味,以下都會類似上面這樣,僅摘錄臭味發生的原因、問題 (原理)、重構手法和現實考量。
這種臭味是將操作轉換為類別所引起的,它的表現是類別指定義了一個方法,有時候類別名稱和該方法名稱相同。... (略) ... 這種臭味另一個經常的表現形式為,方法操作的資料位於另一個類別之中。 p. 29
將函數或過程明確定義為類別 (其處理的資料位於其他地方) 是典型的結構化編寫思維,而不是物件導向編寫思維。 p. 29
要透過重構消除「命令式抽象」設計臭味,必須找到或建立一個合適的抽象,並將命令式抽象中的方法一道這個抽象中。你還必須在該抽象封裝這些方法所需的資料,以提高內聚性並降低耦合度。 p. 32
具體化 (reification) 指的是將不是物件的東西提升為物件。將行為具體化後,便可對其進行儲存、傳遞和轉換。具體化可提高系統的靈活性,但代價是增加系統的複雜度。 p. 35
很多設計模式都利用了具體化:
換句話說,為提高可重用性、靈活性和可擴展性 (及改善設計品質) 而有意識地將原本不是物件的東西提升為物件時,這不算臭味。 p. 35
抽象未支援所有互補或相關的方法時,將導致這種臭味。 p. 36
為解決抽象曝路的介面不完整的問題,客戶程式可能試圖直接存取抽象的內部實作細節;因此,未正確應用抽象原則所帶來的副作用式可能違反封裝原則。 p. 36
重構方法是在抽象中添加所有缺失的方法。 p. 39
前後兩個方法指的是完全不同的概念啊...
有時候,設計人員可能有意識地做出不提供對稱或配套方法的設計決策。... (略) ... 在這種情況下,抽象看似不完整,但不算臭味。 p. 41
抽象被賦予不指一項職責時,將導致這種臭味。 p. 42
設計元素的修改頻率與其缺陷數之間通常存在很強的正相關關係。這意味著多方面抽象存在的缺陷可能更多。 p. 42
具體地說,可建立新類別,並將存在「多方面抽象」臭味的類別中相關的方法和藍未一道新類別中。 p. 45
在軟體設計中引入實際上不需要 (即原本可以避免) 的抽象時,將導致這種臭味, p. 47
如果建立的抽象沒必要或只是為了方便,他們承擔的職責微不足道,甚至根本沒有承擔任何職責,這違反了抽象原則。 p. 47
介面是實作介面的類別必須支援的協議。將介面作為常數容器示範用抽象機制。 p. 49
我不否認自己會這麼用,因為不是所有的常數都適合用 enum 表示,也不是所有的常數都屬於實作細節,只能說有些語言對於常數確實不那麼友善。
有些設計模式 (如中介者模式、代理模式、門面模式和轉接器模式) 使用了委派,其中包含一個看似不必要的類別。... (略) ... 要判斷只執行委派的抽象是否多餘,必須以具體情況進行具體分析。 p. 51
建立的抽象未利用 (未被直接使用或繼承) 時,將導致這種臭味。 p. 52
設計應該服務於真實需求,而非憑空想像或推測的需求。未實作的抽象類別和介面是多餘或憑空想像出來的泛化,因此是不需要的。 p. 52
通常,開發人員不將就程式刪除的原因之一,他們不確定是否還有其他程式在使用它。 p. 53
最簡單的重構方法是,將未利用的抽象從程式中刪除。 p. 55
類別庫和框架通常以抽象類別或介面的方式提供擴展點,這些抽象類別或介面可能在函式庫或框架中未被使用,但他們是提供客戶程式使用的擴充點,因此不屬於未利用的抽象。 p. 56
兩個抽象的名稱、實作或兩者皆相同時,將導致這種臭味。 p. 57
對名稱相同的重複抽象,重構建議是將其中一個抽象改為不同的名稱。對於實作相同的重複抽象,如果實作完全相同,可將其中一個實作刪除。 p. 61
導致重複抽象的一個原因是,要同時支援同步與非同步變形。 p. 63
事實上,要對大型系統,建立完全統一的領域模型要嘛不可行要嘛不划算。領域驅動設計提供一個解決方案是,將大型系統分成多個「有界上下文 (Bounded Context)」。採用這種方法時,不同上下文的模型可能包含同名的型別。有鑑於有界上下文是幫助處理大型領域建模問題的模式之一,這種在不同上下文使用同名型別的做法是可以接受的。 p. 63
封裝原則倡導透過隱藏抽象的實作細節和隱藏變化等手法,來實作關注點分離和資訊隱藏。 p. 65
有兩種實作手法可有效地應用封裝原則:
透過隱藏變化,更容易在不給客戶程式帶來太大影響的情況下修改抽象的實作。 p. 66
對於抽象的一個或多個成員,宣告的存取全縣超過了實際需求時,將導致這種臭味。 p. 67
這種臭味的一種極端表現形式是,存在一些用全域變數、全域資料結構等表示的全域狀態,整個軟體系統的所有抽象都可以存取他們。 p. 67
如果抽象曝露了實作細節,將導致抽象和客戶程式緊密耦合。這不可取,因為每當需要修改抽象的實作細節時,都將影響客戶程式。 p. 67
對於公有資料成員,可採取重構手法「對欄位進行封裝」,將欄位宣告為私有的,並提供必要的存取方法。對於與實作類別相關的公有方法,必須將其宣告為私有或受保護的。 p. 74
但說真的,若沒經過考慮,就把 set 加上去,其實跟公有成員是一樣的。
然而,由於當今的編譯器和 JIT 編譯器會內聯 (inline) 存取方法,因此使用存取方法帶來的效能影響通常微不足道。 p. 77
抽象透過公有介面曝露或洩露實作細節時,將導致這種臭味。... (略) ... 即便抽象不吋在「不充分的封裝」臭味,其公有介面中的方法也可能洩露實作細節。 p. 77
重構建議是對介面進行修改,以免它曝露實作。例如,內部的演算法細節在公有介面上曝露,那麼就重構公有介面,以免它曝露演算法細節。 p. 81
向客戶程式返回指向內部資料結構的控制程式也不合適,因為這會讓客戶程式能透過該控制程式直接修改內部狀態。解決這種問題的方法有下列幾種:
參考 閒談軟體設計:Immutable Interface
沒有將實作變化封裝在抽象或 hierarchy 中,將導致這種臭味。 p. 85
將彼此獨立的各種關注點聚合在一個 hierarchy 中,而不是分開時,如果關注點發生變化,可能導致類別的數量呈爆炸性增長。 p. 87
重構建議是封裝變化。在實踐中,這通常是透過策略和橋街等模式來實現的。 p. 89
客戶程式使用顯式型別檢查 (使用一連串 if-else 語句或 switch 語句檢查物件的型別),而不利用 hierarchy 內以封裝的型別變化時,將導致這種臭味。 p. 93
顯式型別檢查讓客戶程式與具體型別緊密耦合,降低了設計的可維護性。例如,引入新型別後,必須修改客戶程式,在其中檢查新型別以執行相應操作的程式。 p. 93
參考 閒談軟體設計:Switch 壞味道
模組化原則倡導利用集中和分解等手法建立高內聚、低耦合的抽象。 p. 101
本書中,模組這個術語指的是類別級抽象,即具體類別、抽象類別和介面。... (略) ... 實現手法如下:
應集中在一個抽象中的資料和/或方法分散在多個抽象中時,將導致這種臭味。這種臭味的常見表現形式如下:
對於包含大量資料類別的程序型設計,可採用重構手法「將程序型設計轉換為物件導向」。在這類重構手法中,可以將資料成員以及與資料成員相關聯的行為移到同一個類別。 p. 107
為消除「拆散的模組化」臭味,將緊密相關的方法放到一個抽象之中,可能會導致「多方面抽象」或「不充分的模組化」臭味。有鑑於此,重構這種臭味時,需要特別小心。 p. 109
對於自動產生的程式,不推薦直接修改它們,因為這將導致模型與程式無法同步。有鑑於此,在自動產生的程式中包含資料類別可能是可以接受的。 p. 110
DTO 聚合資料,但不包含行為;這是有意為之的,只在方便同步。 p. 110
抽象分解得不徹底,且透過進一步分解,可以降低其規模和實作複雜度時,將導致這種臭味。這種臭味有以下兩種表現形式:
兩種臭味常常同時出現:龐大而複雜的抽象常常承擔多項職責。然而,它們式兩種不同的臭味:即便抽象只承擔了一項職責,也可能龐大而複雜;同樣的,很小的抽象也可能承擔了多項職責。 p. 111
如果類別包含多組為不同客戶程式提供服務的方法,可以考慮應用介面分離原則 (ISP),將原來的介面分成多個針對不同客戶程式的介面。 p. 115
如果方法的邏輯很複雜,可以引入私有的輔助方法,以簡化該方法的程式。 p. 115
對於循環複雜度極高的複雜方法實作,必須編寫大量的測試案例,對其所有執行路徑進行測試。 p. 116
關鍵類別抽象了系統最重要的概念,通常龐大而複雜,並與系統的眾多其他類別緊密耦合。在真實的系統中,這樣的類別難以避免,但必須考慮如何分解它們以簡化維護工作,這很重要。 p. 117
多個抽象直接或間接彼此依賴,導致抽象之間緊密耦合時,將引發這種臭味。 p. 117
抽象之間存在循環依賴時,可能必須同時理解、修改、使用、測試和重用這些抽象。 p. 117
實作回呼 (callback) 功能時,常常會在兩個類別之間引入不必要的循環依賴。 p. 119
重構這種臭味,需要段開依賴環路。對此有很多策略,其中一些重要的策略如下:
有專門負責避免架構腐蝕或設計退化的架構師,這很重要。 p. 127
但說真的,很少有公司有這角色,參考 閒談軟體設計:架構師難尋?
對依賴循環牽涉的一個抽象進行修改時,可能引發連鎖反應,波及依賴鏈牽涉的所有類別 (包括最初修該的抽象)。導致無論是要理解、分析和實作新功能特性,還是修改依賴循環牽涉的任何抽象等等,都很困難。 p. 127
這位翻譯難道不能挑個更簡單的名詞嗎?輪轂... 很冷僻的字耶,有人知道它原文是 Hub-like Modularization 嗎?
一個抽象與大量其他的抽象之間存在依賴關係時,將導致這種臭味。 p. 129
要重構這種臭味,可能需要採用下述一種或多種重構手法。
層次結構 (hierarchy) 原則倡導採用分類、合併、替換和排序等手法以層次方式組織抽象。 p. 135
在軟體設計中應用 hierarchy 的一些重要實現手法:
為了進行有意義的分類,在這個步驟中,應專注於型別行為 (而非資料) 的共同性和差異。 p. 136
確保 hierarchy 中的型別遵循里氏替換原則 (LSP),即可將超型別參照替換為子型別物件,而不會改變程式的行為。 p. 137
在繼承 hierarchy 中,子型別依賴於超型別很容易理解,但如果超型別依賴於子型別,設計就更難理解了,因為這違反了依賴順序原則。 p. 137
程式片段使用條件邏輯 (通常帶有標籤型態) 來顯式地管理行為變化,而原本可以建立一個 hierarchy 來封裝這些變化時,將導致這種臭味。 p. 139
當型別資訊被編碼,例如使用列舉、整數值或字串時,說明沒有正確地封裝行為變化,也未利用這些型別的共同性。 p. 139
可考慮使用以下手法來重構這種臭味:
如果應用程式需要與外部世界的實體互動 (例如以下的情形),要避免使用條件邏輯是很難的。
在這種情形下,不管設計中是否有對應於型別碼的 hierarchy,都可能必須使用以型別碼編寫的 switch 語句。 p. 146
當整個繼承 hierarchy 顯得多餘,亦即在特定的設計片段中使用了不必要的繼承時,將導致這種臭味。 p. 146
所謂進行「有意義」的分類,指的是捕捉行為而不是資料層面的共同性和差異。 p. 146
可考慮採用以下重構策略:
hierarchy 中的型別存在不必要的重複十,將導致這種臭味。這種臭味有兩種表現形式。
原因有三個:
重構方法是應用「合併」原則。
當繼承 hierarchy 太寬,顯示可能缺失中間型別時,將導致這種臭味。 p. 163
根據我們的經驗,只要任何型別的直接子型別超過 9 個,這樣的 hierarchy 就太寬了。 p. 163
個人是沒印象有設計過有 9 個的情況,但這確實是個很大的數字。
對於過寬的 hierarchy,檢查其中是否缺失中間抽象。如果是這樣,就採用「提取超類別」的重構手法,並引入中間抽象。 p. 165
根據憑空想像而非真實需求,在 hierarchy 中提供相應的型別時,將導致這種臭味。 p. 169
存在基於憑空想像的需求而建立的超型別時,可採取重構手法「緊縮 hierarchy」,將相關的超型別刪除。 p. 170
繼承 hierarchy 過深時,將導致這種臭味。 p. 172
hierarchy 過深時,要預測葉子型別 (leaf type) 的行為,難度將急遽增大,因為這種型別從其超類別那裏繼承了較多的方法。下面的範例說明了這種問題。
一種經驗法則是,如果 hierarchy 超過了 6 層,就可認為它太深了。 p. 172
太深的 hierarchy 包含不必要或憑空想像的中間抽象時,可採用重構方法「緊縮 hierarchy」。 p. 174
較深的繼承 hierarchy 可改善可重用性,很多使用廣泛的成熟框架和函式庫都包含很深的 hierarchy。因此,建立很深的 hierarchy 前,設計人員必須根據具體情況深利入考慮,權衡其利 (如提供可重用性) 弊 (如降低可理解性、可修改性、可擴展性、可靠性和效能)。 p. 178
子型別拒絕超型別提供的方法時,將導致這種臭味。 p. 178
子型別可修改從超型別那裡繼承的行為。改寫超型別的方法時,如果行為得到了改善 (即更具體),那麼這種修改是無害的。然而,如果改寫方法時限制或撤銷了超型別方法的行為,這種修改將是有害的。 p. 178
超型別的方法被有些子型別拒絕時,如果該方法只適用於部分子型別,可採用「移動方法 (move method)」的重構手法,將這些方法從超類別移到相關的子型別中。如果超型別的某個方法被所有子型別拒絕,可採用「刪除方法 (remove method)」的重構手法,將它從超型別中刪除。 p. 181
當超型別和子型別之間從概念上來說不存在 is-a 關係,而導致可替換性遭到破壞時,將導致這種臭味。 p. 189
這種臭味的一種極端形式是,「反轉」了超型別和子型別之間的繼承關係。換句話說,子型別應為超型別,超型別應為子型別。 p. 189
簡單說,就是亂七八糟隨便繼承...
對於支離破碎的 hierarchy,通常採用重構手法「以委派取代繼承」。 p. 195
對於「反轉的 hierarchy」臭味,一種顯而易見的重構方案是,將超型別和子型別之間的繼承關係倒過來。 p. 196
子型別同時直接和間接地繼承超型別,讓 hierarchy 包含不必要的繼承路徑實,將導致這種臭味。 p. 199
對於多路徑 hierarchy,重構建議是刪除 hierarchy 中不必要的繼承路徑。 p. 203
在 hierarchy 中,超型別依賴於子型別實,將導致這種臭味。這種依賴性有以下表現形式:
超型別的建構子主動使用了子型別實,將導致一種很不可取的循環 hierarchy。因為初始化超型別實,子型別還未初始化,這可能導致超型別的程式存取子型別未初始化的部分。 p. 206
對於循環 hierarchy,可考慮採用以下的重構方法。
我們發現了設計臭味的一些有趣特徵。如果設計一個由設計決策和臭味共同構成的生態系統,將發現臭味既影響該生態系統,也受該生態系統的影響。 p. 213
首先,臭味的存在很可能觸發消除它的新設計決策!其次,臭味可能影響或限制或旭設計決策,導致在設計生態系統中出現新的臭味。第三,臭味之間常常會互相影響。 p. 213
不知道有沒有像噬菌體這樣的臭味,可以修除別的臭味 XD
要確保整個設計的高品質,在做設計決策時必須對該決次會給整個設計帶來的利弊瞭如指掌。然而在實際工作中,軟體開發人員常常只關心設計決策的好處,對其弊端視而不見,進而在設計中引入了臭味。 p. 214
這真的蠻常見的,特別是想引入某個新技術時,推廣者會拼命說這個多好多好,卻從來不提引入後會帶來什麼缺點。
該採取什麼樣的重構解決方案呢?這記取決於存在的臭味,也取決於具體情況。重構時如果不考慮具體情況,很可能讓設計無謂地複雜化,乃至引入新的臭味。 p. 216
設計臭味可能強化其它夠味的影響。 p. 218
有些臭味常常與其它臭味同時出現。 p. 219
有些臭味常常是其他臭味的前導兵。 p. 220
在臭味之間存在因果關係時,透過是當的重構消除根臭味 (root smell) 後,根臭味導致的臭味將隨之消除。 p. 220
解決更深層的問題可能會給整體設計帶來極大的好處,因此我們強烈建議你不要滿足於找出設計中的臭味,而應該根據發現的臭味,找出設計中更深層的問題。 p. 222
修 bug 也是如此,參閱 書摘《程式設計守則》的守則 13 | 揪出引發雪崩的那顆小石頭。
要提出正確的修改建議並進行修改,都必須理解程式的內部結構,這是一個重要的前提條件。... (略) ... 為應對這種挑戰,可使用理解工具來產生視覺化輔助元素,如控制結構視覺化、資料傳輸流分析、呼叫序列分析和繼承圖,以幫助理解程式的內部結構。 p. 224
技術債很難精確地量化,其中主要原因有兩個。首先,對於那些因素將導致技術債,並沒有統一的看法;其次,沒有度量這些因素的標準方式。 p. 225
對於評估和重構發現工具產生的違規結果或建議,只能做為參考。必須根據具體情況對這些結果進行詳盡的分析,以排除假陽性警報。 p. 225
如果不進行單元測試,就難以確保軟體重構後的行為不變;而要編寫單元測試,就必須先讓程式/設計是可測試的。這種重構任務陷入了兩難,在遺留專案中尤其如此,因為它們通常缺乏單元測試,但又必須重構。 p. 226
制定小規模的重構計畫。採取循序漸進的方式。 p. 228
量化重構前後的設計品質,以便能夠透過比較設計品質突顯出重購的正面影響。 p. 228
在實際工作中,「大刀闊斧」的重構方法效果不佳。 p. 231
每次發布主要版本後,都留出一段時間來完成重構任務,確保技術債處於可控狀態,然後在進行下一個主要版本的開發工作。 p. 232
這本書的寫作方式蠻像學期期刊論文的寫法,也詳盡地列出文獻,本書大多數的內容都以 OO 的概念出發,詳列了許多設計的臭味道,也有大量的例子。個人雖然不會這樣寫程式,但仍是覺得受益良多,至少在 code review 時能更清楚知道該怎麼描述問題。但對於不是用 OO 概念寫程式的人來說,大多數章節可能都覺得無感吧!
最近幾年的觀察,不確定是不是我的錯覺,data + function 反而比 OO 來的受歡迎,但以我自己的感覺,程式的規模大到一定的程度,團隊成員成長到一定的規模,重複的 function 肯定會變多,邏輯四散的情況會越嚴重,但就是找不到合適的方式整理,因為對不是用 OO 的人來說,就是不會有把資料和行為封裝成類別的概念。
書摘只精簡地列出臭味和重構方法,但個人十分建議看書中的範例,特別是對 OO 不習慣的人,看範例比較能清楚知道什麼是臭味道。不過,即便不是用 OO 的概念,有些章節還是可以帶來一些想法,用 OO 概念寫程式的人更不該錯過這本好書。