9. Primitive Obsession(基本型別偏執)
大多數編程環境都有兩種資料:結構型別允許你將資料群組織成有意義的形式;基本型別則是構成結構型別的積木塊。結構總是會帶來一定的額外開銷。它們有點像資料庫中的表格,或是那些得不償失的東西。
對象的一個極具價值的東西早到:它們模糊了橫亙於基本資料和體積較大的classes之間的界限。你可以輕鬆編寫出一些與語言內建型別無異的小型classes。例如Java就以基本型別表示數值,而心class表示字串和日期——這兩個型別在其它許多編程環境中都以基本型別表現。
對象技術的新手通常在小任務上運用小對象——像是結合數值和幣別的money class、含一個起始值和一個結束值的range class、電話號碼或郵遞區號等等的特殊strings。你可以運用Replace Data Value with Object將原本單獨存在的資料值替換為對象,從而走出傳統的洞窟,進入炙手可熱的對象世界。如果欲替換之資料值是type code,而它並不影響行為,你可以運用Replace Type Code with Class將它換掉。如果你有相依於此type code的條件式,可運用Replace Type Code with Subclass或Replace Type Code with State/Strategy加以處理。
如果你有一組應該總是被放在一起的範圍,可運用Extract Class。如果你在參數列中看到基本型資料,不妨試試Introduce Parameter Object。如果你發現自己正從array中挑選資料,可運用Replace Array with Object。
10. Switch Statements(switch驚悚現身)
物件導向程式的一個最明顯特徵就是:少用switch(或case)語句。從本質上說,switch語句的問題在於重複。你常會發現同樣的switch語句散佈於不同的地點。如果要為它添加一個新的case子句,你必須找到所有switch語句並修改它們。面向的多態概念可為此帶來優雅的解決辦法。
大多數時候,一看到switch語句你就應該考慮以多態來替換它。問題是多態該出現在哪兒?switch語句常常根據type code進行選擇,你要的是[與該type code相關的函數或class]。所以你應該使用Extract Method將switch語句提煉到一個獨立函數中,再以Move Method將它搬移到需要多態性的那個class裡頭。此時你必須決定是否使用Replace Type Code with Subclasses或Replace Type Code with State/Strategy。一旦這樣完成繼承結構之後,你就可以運用Replace Conditional with Polymorphism了。
如果你只是在單一函數中髭選擇案例,而你並不想改動它們,那麼[多態]就有點殺雞用牛刀了。這種情況下Replace Parameter with Explicit Methods是個不錯的選擇。如果你的選擇條件之一是null,可以試試Introduce Null Object。
11. Parallel Inheritance Hierarchies(平等繼承體系)
Parallel Inheritance Hierarchies其實是Shotgun Surgery的特殊情況。在這種情況下,每當你為某個class增加一個subclass,必須也為另一個class相應增加一個subclass。如果你發現某個繼承體系的class名稱首碼和另一個繼承體系的class名稱首碼完全相同,便是聞到了這種壞味道。
消除這種重複性的一般策略是:讓一個繼承體系的實體指涉另一個繼承體系的實體。如果再接再厲運用Move Method和Move Field,就可以將指涉端的繼承體系消弭於無形。
12. Lazy Class(冗贅類)
你所建立的每一個class,都得有人去理解它、維護它,這些工作都是要花錢的。如果一個class的所得不值其身份,它就應該消失。項目中經常會出現這樣的情況:某個class原本對得起自己的身份,但重簷使它身形縮水,不再做那麼多工作;或開發人員事前規划了某些變化,並添加一個class來就會這些變化,但變化實際上沒有發生。不論上述哪一種原因,請讓這個class莊嚴赴義吧。如果某些subclass沒有做滿足夠工作,試試Collapse Hierarchy。對於幾乎沒用的組件,你應該以Inline Class對付它們。
13. Speculative Generality(夸夸其談未來性)
這個令我們十分敏感的壞味道,命名者是Brian Foote。當有人說“噢,我想我們總有一天需要做這事”並因而企圖以各式各樣的掛勾和特殊情況來處理一些非必要的事情,這種壞味道就出現了。那麼做的結果往往造成系統更難理解和維護。如果所有裝置都會被用到,那就值得那麼做;如果用不到,就不值得。用不上的裝置只會擋你的路,所以,把它搬弄吧。
如果你的某個abstract class其實沒有太大作用,請運用Collapse Hierarchy。非必要之delegation可運用Inline Class除掉。如果函數的某些參數示被用上,可對它實施Rename Method讓它現實一些。
如果函數或class的惟一使用者是test cases,這就飄出了壞味道Speculative Generality。如果你發現這樣的函數或class,請把它們連同其test cases都刪掉。但如果它們的用途是協助test cases檢測正當功能,當然必須刀下留人。
14. Temporary Field(令人迷惑的暫時範圍)
有時你會看到這樣的對象:其內某個instance 變數僅為某種特定情勢而設。這樣的代碼讓人不易理解,因為你通常認為對象在所有時候都需要它的所有變數。在變數未被使用的情況下猜測當初其設定目的,會讓你發瘋。
請使用Extract Class給這個可憐的孤獨創造一個家,然後把所有和這個變數相關的代碼都放進這個新家。也許你還可以使用Introduce Null Object在[變數不合法]的情況下建立一個Null對象,從而避免寫出[條件式代碼]。
如果class中有一個複雜演算法,需要好幾個變數,往往就可能導致壞味道Temporary Field的出現。由於實現者不希望傳遞一長串參數,所以他把這些參數都放進範圍中。但是這些範圍只在使用該演算法時才有效,其它情況下只會讓人迷惑。這時候你可以利用Extract Class把這些變數和其相關函數提煉到一個獨立class中。提煉後的新對象將是一個method object。
15. Message Chains(過度耦合的訊息鏈)
如果你看到使用者向一個對象索求另一個對象,然後再向後者索求另一個對象,然後再索求另一個對象……這就是Message Chain。實際代碼中你看到的可能是一長串getThis()或一長串臨時變數。採取這種方式,意味客戶將與尋找過程中的航行結構緊密耦合。一旦對象間的關係發生任何變化,用戶端就不得不做出相應修改。
這時候你應該使用Hide Delegate。你可以在Message Chain的不同位置進行這種重構手法。理論上你可以重構Message Chain上的任何一個對象,但這麼做往往會把所有中介對象都變成Middle Man。通常更好的選擇是:先觀察Message Chain最終得到的對象是用來幹什麼的,看看能否以Extract Method把使用該對象的代碼提煉到一個獨立函數中,再運用Move Method把這個函數推入Message Chain。如果這條鏈上的某個對象有多位客戶打算航行此航線的剩餘部分,就加一個函數來做這件事。
有些人把任何函數鏈都視為壞東西,我們不這樣想。呵呵,我們的總代表鎮定是出了名的,起碼在這件事情上是這樣。
16. Middle Man(中間轉手人)
對象的基本特徵之一就是封裝——對外部世界隱藏其內部細節。封裝往往伴隨delegation。比如說你問主管是否有時間參加一個會議,他就把這個訊息委託給他的記事簿,然後才能回答你。很好,你沒必要知道這位主管到底使用傳統記事簿或電子記事簿抑或秘書來記錄自己的約會。
但是人們可能過度運用delegation。你也許會看到某個class介面有一半的函數都委託給其它class,這樣就是過度運用。這裡你應該使用Remove Middle Man,直接和負責對象打交道。如果這樣[不幹實事]的函數只有少數幾個,可以運用Inline Method把它們”inlining”,放進調用端。如果這些Middle Man還有其它行為內銷可以運用Replace Delegation with Inheritance把它變成負責對象的subclass,這樣你既可以擴充原對象的行為,又不必負擔那麼多的委託動作。
17. Inappropriate Intimacy(狎昵關係)
有時候你會看到兩個classes過於親密,花費太多時間去探究彼此的private成分。如果這發生在兩個[人]之間,我們不必做衛道之士;但對於classes,我們希望它們嚴守清規。
就像古代戀人一樣,過份狎昵的classes必須拆散。你可以採用Move Method和Move Field幫它們劃清界線,從而減少狎昵行徑。你也可以看看是否運用Change Bidirectional Association to Unidirectional讓其中一個class對另一個斬斷情絲。如果兩個classes實在情投意合,可以運用Extract Class把兩者共同點提煉到一個安全地點,讓它們坦蕩地使用這個新class。或者也可以嘗試運用Hide Delegate讓另一個class來為它們傳遞相思情。
繼承往往造成過度親密,因為subclass對superclass的瞭解總是超過superclass的主觀願望。如果你覺得該讓這個孩子獨自生活了,請運用Replace Inheritance with Delegation讓它離開繼承體系。
18. Alternative Classes with Different Interfaces(異曲同工的類)
如果兩個函數做同一件事,卻有著不同的簽名式,請運用Rename Method根據它們的用途重新命名。但這往往不夠,請反覆運用Move Method將某些行為移入classes,直到兩者的協議一致為止。如果你必須重複而贅餘地移入代碼才能完成這些,或許可運用Extract Superclass為自己贖點罪。
19. Incomplete Library Class(不完美的程式庫類)
複用常被視為對象的終極目的。我們認為這實在是過度估計了。但是無可否認,許多編程技術都建立在library classes的基礎上,沒人敢說是不是我們都把排序演算法忘得一乾二淨了。
Library classes構築者沒有未蔔Crowdsourced Security Testing的能力,我們不能因此責怪他們。畢竟我們自己也幾乎總是在系統快要構築完成的時候才能弄清楚它的設計,所以library構築者的任務真的很艱巨。麻煩的是library的形式往往不夠好,往往不可能讓我們修改其中的classes使它完成我們希望完成的工作。這是否意味那些經過實踐檢驗的戰術如Move Method等等,如今都派不上用場了?
幸好我們有兩個專門就會這種情況的工具。如果你只想修改library classes內的一兩個函數,可以運用Introduce Foreign Method;如果想要添加一大堆額外行為,就得運用Introduce Local Extension。
20. Data Class(純稚的資料類)
所謂Data Class是指:它們擁有一些範圍,以及用於訪問這些範圍的函數,除此之外一無長物。這樣的classes只是一種[不會說話的資料容器],它們幾乎一定被其它classes過份細瑣地操控著。這些classes早期可能擁有public範圍,果真如此你應該在別人注意到它們之前,立刻運用Encapsulate Field將它們封裝起來。如果這些classes內含容器類的範圍,你應該檢查它們是不是得到了恰當的封裝;如果沒有,就運用Encapsulate Collection把它們封裝起來。對於那些不該被其它classes修改的範圍,請運用Remove Setting Method。
然後,找出這些[取值/設值]函數被其它classes運用的地點。嘗試以Move Method把那些調用行為搬移到Data Class來。如果無法搬移整個函數,就運用Extract Method產生一個可被搬移的函數。不久之後你就可以運用Hide Method把這些[取值/設值]函數隱藏起來了。
Data Class就像小孩子。作為一個起點很好,但若要讓它們像[成年]的對象那樣參與整個系統的工作,它們就必須承擔一定責任。
21. Refused Bequest(被拒絕的遺贈)
Subclasses應該繼承superclass的函數和資料。但如果它們不想或不需要繼承,又該怎麼辦呢?它們得到所有禮物,卻只從中挑選幾樣來玩!
按傳統說法,這就意味繼承體系設計錯誤。你需要為這個subclass建立一個兄弟,再運用Push Down Method和Push Down Field把所有用不到的函數下推給那兄弟。這樣一來superclass就只持有所有subclasses共用的東西。常常你會聽到這樣的建議:所有superclasses都應該是抽象的。
既然使用[傳統說法]這個略帶貶義的詞,你就可以猜到,我們不建議你這麼做,起碼不建議你每次都這麼做。我們經常利用subclassing手法來複用一些行為,並發現這可以很好地應用於日常工作。這也是一種壞味道,我們不否認,但氣味通常並不強烈。所以我們說:如果Refused Bequest引起困惑和問題,請遵循傳統忠告。但不必認為你每次都得那麼做。十有八九這種壞味道很淡,不值得理睬。
如果subclass複用了superclass的行為(實現),卻又不願意支援superclass的介面,Refused Bequest的壞味道就會變得濃烈。拒絕繼承superclass的實現,這一點我們不介意;但如果拒絕繼承superclass的介面,我們不以為然。不過即使你不願意繼承介面,也不要胡亂修改繼承系,你應該運用Replace Inheritance with Delegation來達到目的。
22. Comments(過多的注釋)
別擔心,我們並不是說你不該寫注釋。從嗅覺上說,Comments不是一種壞味道;事實上它們還是一種香味呢。我們之所以要在這裡提到Comments,因為人們常把它當作除臭劑來使用。常常會有這樣的情況:你看到一段代碼有著長長的注釋,然後發現,這些注釋之所以存在乃是因為代碼很糟糕。這種情況的發生次數之多,實在令人吃驚。
Comments可以帶我們找到本章先前提到的各種壞味道。找到壞味道後,我們首先應該以各種重構手法把壞味道去除。完成之後我們常常會發現:注釋已經變得多餘了,因為代碼已經清楚說明了一切。
如果你需要注釋來解釋一塊代碼做了什麼,試試Extract Method;如果你需要注釋說明某些系統的需求規格,試試Introduce Assertion。
如果你不知道該做什麼,這才是注釋的良好運用時機。除了用來記述將來的打算之外,注釋還可以用來標記你並無十足把握的地區。你可以在注釋裡寫下自己[為什麼做某某事]。這類資訊可以協助將來的修改者,尤其是那些健忘的傢伙。