《重構》讀書筆記
總覽
第一部分
第一章從實例程序出發,展示設計的缺陷,對其重構可以了解重構的過程和方法。
第二部分
第二章討論重構的一般性原則、定義和進行重構的原因。主要講述概念性的東西。
第三部分
第三章介紹如何嗅出代碼的“壞味道”以及如何用重構對其消除。
第四部分
第四章講述構建java的測試環境
第五部分
第五章到第十二章介紹作者整理下來重構的方法。
第六部分
第十三章重構技術在商業化應用中出現的問題
第1章 重構,第一個案例1
1.1 起點1
如果發現需要為程序添加新特性,而代碼結構使你無法很方便的達到目的,那就需要先進行重構,然后使新特性的添加容易進行,再添加新特性。
1.2 重構的第一步7
首先檢查自己是否有一套可靠的測試機制。這些測試必須有自我檢測的能力。因為重構有可能引入bug。
1.3 分解并重組statement()8
重構技術就是以 微小的步伐修改程序,如果發現錯誤,很容易可以發現它。
優秀的程序員應該寫出人類容易理解的代碼,而非僅僅是計算機能理解的代碼。
- 搬移金額計算代碼(Move Method)
- 去除不必要的臨時變量(Replace Temp with Query)
- 提煉常客積分計算(Move Method)
- Move Method 如果一個方法運用目標對象的屬性進行計算,那么請把這個方法抽象到目標對象的類中。
1.4 運用多態取代與價格相關的條件邏輯34
- 最好不要在另一個對象的屬性基礎上運用switch語句。如果不得不使用,也應該在對象自己的數據上使用,而不是別人的數據上。
- 當一個類需要運用多態的時候,但是假如其在生命周期內有可能變成另一個同胞類,那么就應該使用state設計模式來解決這個問題。
1.5 結語52
第2章 重構原則53
2.1 何謂重構53
- 名詞含義:對軟件內部結構的一種調整,目的是在不改變軟件可觀察行為的前提下,提高理解性和降低修改成本。
- 動詞含義
使用一系列重構手法,在不改變軟件可觀察行為的前提下,調整其結構。 - 重構讓軟件更容易理解和修改
- 重構不會改變軟件的可觀察行為,即使改變也只能是微小的影響,軟件功能一如既往。
- 兩頂 帽子(時間分配)
- 添加新功能:不應該修改已有代碼,只關注新功能。增加新測試,通過測試衡量工作進度
- 重構:只改變程序內部結構,不應該添加測試(存在遺漏),不修改測試(除非接口發生變化)
- 軟件開發在這兩者之間切換
2.2 為何重構55
-
改進軟件設計:
- 程序的設計在沒有重構的情況下逐漸腐敗變質,功能的增加或者修改可能使代碼越來越難以理解,就越難保護其中的設計
- 消除重復的代碼一方面是程序運行更快,一方面是方便未來的修改,只用在一處修改即可不用修改多處。
-
軟件更容易理解:
- 及時填補“想要它做什么”和“告訴它做什么”之間的縫隙。重構的核心就是要“準確說出我所要的”
- 重新閱讀代碼的人有可能是自己,他人。
- 通過重構可以把不熟悉的代碼的用途理一遍,加深對代碼的理解
幫助找出bug:這個是建立在代碼容易理解之上的
提高編程速度:重構達到良好的設計,而良好的設計更容易修改,增加功能,調試。
2.3 何時重構57
- 三次法則:第一次的時候做某事盡管去做。第二次的時候對它產生反感,還是繼續去做。第三次再做類似的時候,就應該重構了。
- 添加功能時重構:一方面可能是需要理解需要修改的代碼,另一方面是使增加新特性更加容易。
- 修補錯誤時重構:出現bug的時候,難以找出問題所在的時候,很有可能是代碼不清晰導致查找bug的困難。
- 復審代碼時重構:
- 復審代碼有助于知識的傳播,有利于代碼被編寫者之外的人理解。
- 重構是有利于增強復審代碼的能力,重構需要先閱讀代碼得到一定程度的理解,得到一些建議,然后動手實現。所以重構有利于知道合理的代碼應當是怎么樣的。
- 復審團隊需要精煉,就 一個審查者和一個原作者。較大的項目可以通過 UML圖去展示代碼的邏輯。
- 程序難以相與的原因:
- 難以閱讀的程序,難以修改
- 邏輯重復的程序,難以修改
- 添加新特性需要修改已有代碼的程序,難以修改
- 帶復雜邏輯判斷的程序,難以修改
- 對應的期望:
- 容易閱讀
- 所有邏輯都只有唯一地點指定
- 新的改動不會危及現有行為
- 盡可能簡單表達邏輯
2.4 怎么對經理說60
- 不要告訴經理:經理是進度驅動,就是要求開發者盡快完成任務。而對于我來說最快完成任務的方式就是先重構。
- 很多時候重構都為程序引入間接層。把大型對象拆分成小對象,把大型函數拆分為小型函數。
- 允許邏輯共享:一個函數在不同地點被調用。子類共享超類的方法。
- 分開解釋意圖和實現:通過類名和函數名解釋自己的意圖
- 隔離變化:在不同地方使用同一個對象,需要修改一處邏輯,那么可以做出子類,并在需要的時候修改這個子類。
- 封裝條件邏輯:運用多態。將條件邏輯轉化為消息模式。
- 減少間接層:當間接層只在一處使用,那么需要將其消除。
2.5 重構的難題62
- 數據庫:
- 程序與數據庫耦合在一起。另一方面是數據遷移,是向繁瑣的事項。
- 在非關系型數據庫,可以在數據庫和對象模型中插入一個分離層,隔離兩者之間的變化
- 修改接口
- 對于已經發布的接口需要可能需要維護舊接口和新接口,用deprecated修飾舊接口。
- 不發布新接口,在舊接口中調用新接口。
- 假如新接口拋出編譯時異常,那么可以在舊接口中調用新接口并將編譯時異常轉化為運行時異常。
- 何時不重構
- 重構之前,代碼必須能夠在大部分情況下 正常運行,不然就不應該重構,而應該是 重寫。
- 到了Deadline,應該避免重構。
2.6 重構與設計66
- 重構與設計是彼此互補的。
- 預先設計是必須,預先設計不可能做到完全正確,隨著對問題的逐漸深入,通過重構可以改善程序的質量。
- 重構減輕了設計的難度和壓力,在程序不斷修改的過程逐步完善程序的設計。
2.7 重構與性能69
- 重構是有可能導致程序運行變慢的。
- 除了對實時有嚴格要求的程序,編寫快速軟件的秘訣是:首先寫出可調的程序,然后調整它以達到足夠的速度。
- 經過分析大部分程序的大半部分時間是運行在一小半代碼上,所以對所有代碼一視同仁是錯誤的。
- 性能優化放在開發的后期,通過分析工具找出消耗大量時間空間的地方,然后集中精力優化這些地方。
2.8 重構起源何處71
第3章 代碼的壞味道75
3.1 DuplicatedCode(重復代碼)76
- 同個類兩個函數存在相同表達式:ExtractMethod(提煉函數)
- 互為兄弟類內存在相同表達式:
- ExtractMethod-》PullUpMethod(函數上移)
- 如果代碼只是相似:先運用ExtractMethod(提煉函數)分開再Form TemPlate Method(塑造模板函數)
- 兩個毫不相干的類存在重復代碼:ExtractClass(提煉類)
3.2 LongMethod(過長函數)76
- 原則:每當感覺需要以注釋來說明什么的時候,我就將需要說明的代碼放到一個獨立的函數里面
- 只要函數名稱能夠 解釋用途,我們就應該毫不猶豫地做。
- 關鍵不在函數的長度,而在于函數“做什么”和“如何做”之間的語義距離。
- 具體情況
- 函數有大量參數和臨時變量:ExtractMethod(提煉函數)
- 用ReplaceTempwithQuery(以查詢取代臨時變量)消除臨時變量
- 用IntroduceParameterObject(引入參數對象)或者PreserveWholeObject(保持對象完整)來將多長的參數列表變得簡潔一點。
- 如果按照上述步驟還存在太多變量和參數就需要用到ReplaceMethodwithMethodObject(以函數對象取代函數)
- 條件表達式可以用DecomposeConditional(分解條件表達式)解決
- 可以將循環內的代碼提煉為函數。
3.3 LargeClass(過大的類)78
- 有時候類并非在所有時刻都使用實例變量:使用ExtractMethod和ExtractSubclass(提煉子類)
- 類中有太多代碼:ExtractClass(提煉類)ExtractSubclass(提煉子類),甚至可以使用提煉接口的方式分解類的行為。
- 存在GUI的時候,可以DuplicateObservedData(復制“被監視數據”),分離數據和行為到領域模型中去。
3.4 LongParameterList(過長參數列)78
- 如果可以調用已有對象獲取的話可以使用ReplaceParameterwithMethods(以函數取代參數)
- 將來自同一對象的數據收集起來,以該對象替代:PreserveWholeObject(保持對象完整)
- 如果幾個參數總是同時出現,那么可以考慮IntroduceParameterObject(引入參數對象)
3.5 DivergentChange(發散式變化)79
- 一個類受多種變化影響:加上一個功能需要修改類中多個函數
- 目標是每個對象都可以只因一種變化而需要修改
- 方法:可以將提煉類來達到。
3.6 ShotgunSurgery(霰彈式修改)80
- 遇到某種變化,需要在許多不同類做小修改。
- 可以通過移動函數、移動字段、內聯類把一種變化一系列變化放到同一個類中。
- 對比:DivergentChange(發散式變化)是一個類受多個變化影響;ShotgunSurgery(霰彈式修改)是一個變化引起多個類相應修改。
3.7 FeatureEnvy(依戀情結)80
- 函數對某個類的興趣高過對自己類的興趣
- 通過移動函數放到該合適的位置。
3.8 DataClumps(數據泥團)81
- 數據項總是成群結隊出現
- 判斷方法:刪除眾多數據項的一項,這么做其他數據是否失去意義。如果不再有意義就需要提煉為參數對象。
3.9 PrimitiveObsession(基本類型偏執)81
- 有些字段可以用對象表示更準確ReplaceDataValuewithObject(以對象取代數據值)
- 對于不影響行為的類型碼可以ReplaceTypeCodewithClass(以類取代類型碼)
- 影響行為的類型碼可以ReplaceTypeCodewithSubclasses(以子類取代類型碼),類型碼在運行時會變化就用ReplaceTypeCodewithState/Strategy(以State/Strategy取代類型碼)
3.10 SwitchStatements(switch驚悚現身)82
- 使用ReplaceTypeCodewithSubclasses(以子類取代類型碼)或者ReplaceTypeCodewithState/Strategy(以State/Strategy取代類型碼)
- 輕量級的解決方法:ReplaceParameterwithExplicitMethods(以明確函數取代參數)
3.11 ParallelInheritanceHierarchies(平行繼承體系)83
- 每當為一個類增加子類必須也為另外一個類增加一個子類
- 策略是讓一個繼承體系的實例引用另一個繼承體系的實例。
3.12 LazyClass(冗贅類)83
- 內聯類或者Collapse Hierarchy(折疊繼承體系)來解決
3.13 SpeculativeGenerality(夸夸其談未來性)83
- 內聯類或者Collapse Hierarchy(折疊繼承體系)來解決
- 函數參數沒被用上RemoveParameter(移除參數)
- 函數名稱過于抽象RenameMethod(函數改名)
3.14 TemporaryField(令人迷惑的暫時字段)84
- 對象中某個字段僅為特定情況而設。
- 提煉類來解決
3.15 MessageChains(過度耦合的消息鏈)84
- 獲取一個對象,再通過該對象獲取另外一個對象進行操作:HideDelegate(隱藏“委托關系”)
3.16 MiddleMan(中間人)85
- 過度委托形成中間人:RemoveMiddleMan(移除中間人)
- 如果中間人還有其他行為,Replace Delegation with Inherited(以繼承取代委托)
3.17 InappropriateIntimacy(狎昵關系)85
- 兩個類過于親密,花費太多時間去探究彼此private成分
- 移動字段和移動方法減少狎昵
- ChangeBidirectionalAssociationtoUnidirectional(將雙向關聯改為單向關聯)
- 如果兩個類實在情投意合:可以使用ExtractClass(提煉類),讓他們使用新類進行交互。
3.18 AlternativeClasseswithDifferentInterfaces(異曲同工的類)85
- 兩個函數做了相同的事情卻有不同的簽名
3.19 IncompleteLibraryClass(不完美的庫類)86
- 庫函數不夠好,需要加入一些操作,其實類似于 適配IntroduceForeignMethod(引入外加函數)
- 如果需要加入大量的操作,IntroduceLocalExtension(引入本地擴展)
3.20 DataClass(純稚的數據類)86
- 類只有數據沒有行為,其他類存在對該類的數據進行取值設值操作
- 有public字段:EncapsulateField(封裝字段)
- 對于不該被其他類修改的字段:RemoveSettingMethod(移除設值函數)
3.21 RefusedBequest(被拒絕的遺贈)87
- 如果類不想得到另一個類全部東西,只對部分感興趣。
- 可以使用Replace inherited with Delegation(以委托取代繼承)來處理
3.22 Comments(過多的注釋)87
- 試試提煉方法來解決注釋過多問題
第4章 構筑測試體系89
4.1 自測試代碼的價值89
4.2 JUnit測試框架91
4.3 添加更多測試97
第5章 重構列表103
5.1 重構的記錄格式103
5.2 尋找引用點105
5.3 這些重構手法有多成熟106
第6章 重新組織函數109
6.1 ExtractMethod(提煉函數)110
- 無局部變量:直接抽取方法
- 含有局部變量
- 局部變量只在提煉代碼塊內被讀取值:將局部變量作為方法參數
- 局部變量在提煉代碼塊內被賦值:1只在提煉代碼內被使用->將局部變量提煉到新該方法內;2在提煉代碼塊后->使用就返回局部變量修改后的值
6.2 InlineMethod(內聯函數)117
- 當函數的名稱與其本體都一眼清晰明了,在函數調用點插入函數本體,移除該函數。
- 有一群不甚合理的函數,可以先內聯到大型函數然后再提煉出合理的小函數
6.3 InlineTemp(內聯臨時變量)119
- 當臨時變量只是被一個簡單表達式賦值一次,而它妨礙其他重構方法
- 方法:將所有對該變量的引用動作替代成對它賦值的表達式本身。
- 情形:
- InlineTemp多半是為ReplaceTempwithQuery(以查詢取代臨時變量)準備
- 臨時變量被一次賦值后,臨時變量作為函數的返回值。
6.4 ReplaceTempwithQuery(以查詢取代臨時變量)120
- 情況:你的程序以一個臨時變量保存一個表達式的計算結果
- 做法:將表達式提煉出獨立的函數,然后臨時變量的調用替換成新函數的調用。此后新函數也能被調用。
- 具體做法:
- 將提煉出來的函數用private修飾
- 如果獨立函數有副作用,那對它進行SeparateQueryfromModifier(將查詢函數和修改函數分離)
6.5 IntroduceExplainingVariable(引入解釋性變量)124
- 將復雜表達式的結果賦值給一個臨時變量,用臨時變量名稱來解釋表達式的用途
6.6 SplitTemporaryVariable(分解臨時變量)128
- 臨時變量被賦值超過一次,但是既不是 循環變量也不是被用于 收集計算結果
- 原因:一個變量應該承擔一個責任,如果被賦值多次很可能承擔了多個責任
- 做法:針對每次賦值,創建新的臨時變量
6.7 RemoveAssignmentstoParameters(移除對參數的賦值)131
- java是值傳遞,對參數的任何修改都不會再調用端造成影響,所以對于 用過引用傳遞的人可能會發生理解錯誤
- 參數應該僅表示“被傳遞過來的東西”
6.8 ReplaceMethodwithMethodObject(以函數對象取代函數)135
- 情形:在大型函數內,對局部變量的使用導致難以使用ExtractMethod(提煉函數)進行重構
- 做法:將這個函數放入一個對象里,局部變量變成對象成員變量,然后可以在同一對象中將這個大型函數分解為多個小型函數。
- 原因:局部變量會增加分解函數的困難度
6.9 SubstituteAlgorithm(替換算法)139
- 把某個算法替換成更清晰的做法(算法)(有點廢話)。
第7章 在對象之間搬移特性141
7.1 MoveMethod(搬移函數)142
- 情形:程序中有個函數與所駐類之外的另一個類進行更多交流,調用后者或者后者調用該函數
- 做法:在該函數最常引用的類中定義相似行為的新接口,將舊函數變成委托函數或者將舊函數刪除。
- 具體做法:
- 檢查源類中被源函數使用的一切特性,如果特性被其他函數使用,考慮這些函數一起搬移
- 檢查源類的子類和超類,看看是否有該函數的聲明,如果出現,很可能不能搬移。
- 目標類需要使用源類的特性:1將該特性轉移到目標類;2建立目標類到源類之間引用。3將源類作為參數傳給目標類4將該特性作為參數傳給目標類
- 如果源函數包含 異常處理,需要考慮是在目標類還是源函數處理
7.2 MoveField(搬移字段)146
- 情形:程序中有個字段與所駐類之外被另一個類使用(包括設置取值函數的間接調用),后者調用該字段
- 做法:將該字段搬移到目標類
- 具體做法:建立從“舊類訪問新類”的連接關系,除非真正需要 不要建立從“新類到舊類”的關系
7.3 ExtractClass(提煉類)149
- 情形:一個類做了兩個類的事
- 做法:建立新類,將相應的字段和函數放到新類
7.4 InlineClass(將類內聯化)154
- 情形:某個類沒做太多的事情,與ExtractClass(提煉類)相反
- 做法:將這個類的所有特性搬移到另一類中,移除該類。
- 判斷依據:當一個類不再承擔足夠責任
7.5 HideDelegate(隱藏“委托關系”)157
- 情形:客戶端通過委托類來調用另一個對象
- 做法:在服務類上建立客戶端所需的函數,然后隱藏委托關系
- 依據:符合“封裝”的特性。當委托類發生變化不會對客戶端造成影響
7.6 RemoveMiddleMan(移除中間人)160
- 情形:某個類做了過多的委托動作
- 做法:讓客戶端直接調用委托類
- 依據:當原委托類的特性越來越多,服務類的委托函數將越來越長,需要讓客戶端直接調用,避免服務類淪為中間人。
7.7 IntroduceForeignMethod(引入外加函數)162
- 情形:需要為服務類某個函數增加功能,但是不能修改該類
- 做法:新建函數并將服務類的對象實例作為參數傳入。
- 具體情形:如果需要為服務類增加 大量的方法,請考慮使用IntroduceLocalExtension(引入本地擴展)
7.8 IntroduceLocalExtension(引入本地擴展)164
- 情形:需要為服務類某個函數增加函數,但是不能修改該類
- 做法:建立新類,使它包括這些額外函數,讓這個擴展類作為服務類的子類或者包裝類。
- 具體情況:如果需要對數據進行修改要波及服務類對象,那么使用包裝類的方式。如果不需要,使用子類化的方式
第8章 重新組織數據169
8.1 SelfEncapsulateField(自封裝字段)171
- 情形:直接訪問一個字段,但是字段之間的耦合關系逐漸變得笨拙。
- 做法:自封裝就是在對于類內部的字段也封裝一個設值取值的函數。
- 爭論:字段訪問方式是直接訪問還是間接訪問一致爭論不斷
- 間接訪問的好處
- 修改獲取數據的途徑;
- 支持更靈活的數據管理;如延遲加載(需要用到才加載)等。
- 直接訪問的好處
- 容易閱讀代碼,不會需要轉換一下這個函數是取值函數。
8.2 ReplaceDataValuewithObject(以對象取代數據值)175
- 情形:假如一個數據項需要與其他數據一起使用才有意義。
- 做法:將數據變成對象。
8.3 ChangeValuetoReference(將值對象改為引用對象)179
- 情形:從一個類衍生出彼此相似的對象的實例,希望把它們替換為同一個對象,ps:方便統一修改
- 做法:將值對象變成引用對象
- 區別:
- 引用對象每個都對應現實中一個對象(==)
- 值對象只關心其值是否相等。(重寫equals()和hashcode()方法)
- 具體做法:
- 需要使用工廠模式來創建對象
- 需要一個類(或者是自身)用字典或者靜態表來保存對象
- 決定對象是預先創建還是動態創建
8.4 ChangeReferencetoValue(將引用對象改為值對象)183
- 情形:有一個引用對象且 很小(創建太多值對象內存消耗大) 不可變(無需修改對象),那么應該將其轉換為值對象
- 具體做法:
- 查看是否是不可變對象或者可修改成不可變對象
- 重寫hashCode和equals()方法
- 取消使用工廠模式和將對象的構造函數設為public
8.5 ReplaceArraywithObject(以對象取代數組)186
- 情形:如果數據存儲了不相似的數據,元素代表不同的東西。
- 做法:將數組變成對象,數組的每個元素用字段表示
8.6 DuplicateObservedData(復制“被監視數據”)189
- 情形: 有領域數據置身于GUI控件中,而領域函數需要訪問這些數據
- 做法:將該數據復制到領域模型中。建立Observer模式,同步UI和領域模型的數據。
8.7 ChangeUnidirectionalAssociationtoBidirectional(將單向關聯改為雙向關聯)197
- 情形:被引用類需要得到引用類做一些處理
- 具體做法:
- 兩者是一對多關系,有單一引用承擔控制關聯關系責任
- 如果某個對象(Task)是另一個對象(Project)的組件,由后者負責控制。
- 如果兩者之間都是多對多關系,那么由誰負責都沒關系
8.8 ChangeBidirectionalAssociationtoUnidirectional(將雙向關聯改為單向關聯)200
- 情形:兩個類有雙向關聯,但是一個類不關心另一個類的特性
- 做法:去除雙向關聯
- 原因:
- 雙向關聯可能造成僵尸對象,不能被清除釋放內存。
- 使兩個類存在耦合關系,一個類的變化會導致另一類的變化。
8.9 ReplaceMagicNumberwithSymbolicConstant(以字面常量取代魔法數)204
- 情形:有一個字面常量(除了0和1之外)
- 做法:創建常量賦值以該字面常量,給予命名。
8.10 EncapsulateField(封裝字段)206
- 情形:一個類有public字段
- 將它聲明為private,并提供相應的訪問函數
8.11 EncapsulateCollection(封裝集合)208
- 情形:有函數返回集合
- 做法:讓該函數返回只讀副本,并在該類提供增加和刪除集合元素的函數
- 具體做法:不應該提供集合的設值函數
8.12 ReplaceRecordwithDataClass(以數據類取代記錄)217
- 情形:面對傳統編程環境的記錄數據
- 做法:為該記錄創建一個“啞”數據對象。
8.13 ReplaceTypeCodewithClass(以類取代類型碼)218
- 情形:類中有個數值型類型碼,不影響類的行為
- 做法:以一個新類替代類型碼
8.14 ReplaceTypeCodewithSubclasses(以子類取代類型碼)223
- 情形:有一個不可變的類型碼且影響類的行為
- 做法:以子類取代這個類型碼
8.15 ReplaceTypeCodewithState/Strategy(以State/Strategy取代類型碼)227
- 情形:有一個類型碼且影響類的行為,但是無法通過繼承消除(類型碼可變化)
- 做法:以狀態對象取代。
8.16 ReplaceSubclasswithFields(以字段取代子類)232
- 情形:各個子類唯一區別只在“返回常量的數據”的函數上
- 做法:修改這些函數使它們返回超類的某個(新增)字段,然后銷毀子類。
第9章 簡化條件表達式237
9.1 DecomposeConditional(分解條件表達式)238
- 情形:if-then-else語句,不同分支做不同事情形成大型函數,本身就難以閱讀,尤其在帶有復雜條件的邏輯中。
- 做法:
- 將if語句提煉為函數
- 將then和else段落提煉為函數
- 存在 嵌套,先判斷是否可以用ReplaceNestedConditionalwithGuardClauses(以衛語句取代嵌套條件表達式)消除。不行再分解每個條件
9.2 ConsolidateConditionalExpression(合并條件表達式)240
- 情形:有一系列條件判斷都返回相同結果
- 做法:將這些測試合并為同一個表達式并將這個表達式提煉為獨立函數
- 原因:
- 只是一次檢查,只是存在并列條件需要檢查而已
- 為ExtractMethod(提煉函數)做準備,通過 函數名 告知“為什么這么做”
- 特殊情形:條件表達式 存在副作用。
9.3 ConsolidateDuplicateConditionalFragments(合并重復的條件片段)243
- 情形:在條件表達式的分支上有相同的代碼
- 做法:將這段重復代碼搬移到條件表達式之外,多行代碼可以提煉為獨立函數。
- 當try和catch執行相同代碼,可以將代碼移到final區段。
9.4 RemoveControlFlag(移除控制標記)245
- 情形:在一系列布爾表達式中,某個變量存在控制標記(control flag)作用。
- 做法:以break或者continue代替
9.5 ReplaceNestedConditionalwithGuardClauses(以衛語句取代嵌套條件表達式)250
- 情形:函數中的條件邏輯使人難以看清正確的執行路徑。
- 做法:使用衛語句表現特殊情況
9.6 ReplaceConditionalwithPolymorphism(以多態取代條件表達式)255
- 情形:存在條件表達式根據對象的類型不同選擇不同的行為
- 做法:將表達式分支放進不同子類的重寫方法,將原始函數提煉為抽象函數。
9.7 IntroduceNullObject(引入Null對象)260
- 情形:需要再三檢查對象是否為null
- 做法:將null值替代為null對象
9.8 IntroduceAssertion(引入斷言)267
- 情形:某段代碼需要對程序狀態做出某種假設
- 做法:以斷言明確表現這種假設
- 具體做法:
- 斷言在 發布的時候統統 被去除
- 斷言應該檢查 ***一定必須為真 *** 的條件
第10章 簡化函數調用271
10.1 RenameMethod(函數改名)273
- 情形:命名不好
- 做法:
- 增加函數,將舊函數代碼復制到新函數
- 修改舊函數,讓其轉發調用新函數,如果舊函數引用點少可跳過
- 編譯測試
- 找出舊函數引用,調用新函數
- 編譯測試
- 刪除舊函數
10.2 AddParameter(添加參數)275
- 情形:某個函數需要調用端更多的信息
- 做法:為此函數添加對象參數,讓該對象帶進函數所需信息。
- 其他考慮:
- 現有參數是否提供足夠的信息?
- 這個函數是否應該移動到擁有該信息的對象中?
- 加入新參數是否合適,是否需要使用IntroduceParameterObject(引入參數對象)
10.3 RemoveParameter(移除參數)277
- 情形:函數不需要某個參數
- 做法:將該參數移除
- 具體做法同10.1
10.4 SeparateQueryfromModifier(將查詢函數和修改函數分離)279
- 情形:某個函數既返回對象狀態值,又修改對象狀態
- 做法:建立兩個不同的函數,其中一個負責查詢,另一個負責修改。
- 原則:任何一個有返回值的函數都不應該有副作用。
- 多線程:將修改和查詢函數封裝在一個同步函數中分開調用。
10.5 ParameterizeMethod(令函數攜帶參數)283
- 情形:若干個函數做了類似的工作,但在函數本體中卻包含了不同的值。
- 做法:建立單一函數,以參數表達那些不同的值。
- 要點: 可將少量數值視為參數
10.6 ReplaceParameterwithExplicitMethods(以明確函數取代參數)285
- 情形:有個函數完全有數值不同采取不同的行為
- 做法:針對該參數的每個可能值,建立獨立函數。
- 對比:與ParameterizeMethod(令函數攜帶參數)相反
- 目的:提供清晰的入口。
- 如果參數值對函數行為影響不大,不應該采用此方法。
10.7 PreserveWholeObject(保持對象完整)288
- 情形:從某個對象取若干值,把他們作為參數傳給函數
- 做法:改為調用整個對象
- 目的:避免過長參數
- 不使用該方法:
- 如果函數只依賴那些值不依賴對象,那么不能采用此方法,會導致耦合
- 有時候函數使用了很多來自對象的數據,那么應該考慮使用(Move Method)
10.8 ReplaceParameterwithMethods(以函數取代參數)292
- 情形:對象調用某個函數,并將所得結果作為參數傳遞給另一個函數,而接受該參數的函數本身也能夠調用前一個函數
- 做法:讓參數接受者去除該項參數,并直接調用前一個函數
10.9 IntroduceParameterObject(引入參數對象)295
- 情形:有些參數總是自然地同時出現
- 做法:以一個對象取代這些參數
- 目的:縮短參數長度,函數具有一致性,降低理解和修改代碼的難度
10.10 RemoveSettingMethod(移除設值函數)300
- 情形:類的某個字段應該對象創建的時候被設置,然后不再改變
- 做法:去掉該字段的設置函數
10.11 HideMethod(隱藏函數)303
- 情形:有一個函數,從來沒有被任何類調用
- 做法:將該函數設為private
10.12 ReplaceConstructorwithFactoryMethod(以工廠函數取代構造函數)304
- 情形:創建對象時不僅僅是做簡單的構建動作
- 做法:將構造函數替換為工廠模式
10.13 EncapsulateDowncast(封裝向下轉型)308
- 情形:某個函數返回的對象,需要由函數調用者執行向下轉型()downcast
- 做法:將向下轉型移到函數中
10.14 ReplaceErrorCodewithException(以異常取代錯誤碼)310
- 情形:某個函數返回一個特定的代碼,表示某個錯誤的情況
- 做法:改用異常
10.15 ReplaceExceptionwithTest(以測試取代異常)315
- 情形:面對一個調用者可以預先檢查條件,你拋出了一個異常
- 做法:修改調用者,使它在調用函數之前檢查。
第11章 處理概括關系319
11.1 PullUpField(字段上移)320
- 情形:兩個子類擁有相同的字段
- 做法:將該字段移動到超類,去除重復數據聲明和關于數據的重復行為。并堆超類該字段使用-SelfEncapsulateField(自封裝字段)
11.2 PullUpMethod(函數上移)322
- 情形:有些函數,在各個子類產生相同的結果。
- 做法:將該函數移動到超類
11.3 PullUpConstructorBody(構造函數本體上移)325
- 情形:你在各個子類擁有一些構造函數,它們的本地幾乎完全一致
- 做法:在超類新建一個構造函數,并在子類構造函數中調用它。
- 具體做法:
- 將共同代碼復制到超類構造函數中
- 將共同代碼放在子類構造函數起始處,然后再復制到超類構造函數中。
- 將子類構造函數中共同代碼刪除,改用調用新建的超類構造函數。
11.4 PushDownMethod(函數下移)328
- 情形:超類中的某個函數只與部分而非全部子類有關
- 做法:將這個函數移到相關的子類去。
11.5 PushDownField(字段下移)329
- 情形:超類中的某個字段只被部分而非全部子類使用
- 做法:將這個字段移到需要它的那些子類去。
11.6 ExtractSubclass(提煉子類)330
- 情形:類中的某些特性只被某些而非全部實例用到。
- 做法:新建一個子類,將上面所說的那一部分特性移到子類中。
- 具體情況:
- 并不是出現類型碼就表示需要用到子類,可以在委托和繼承之間做選擇。
- 為子類新建構造函數,如果需要 隱藏子類,可使用ReplaceConstructorwithFactoryMethod(以工廠函數取代構造函數)
- 找出超類調用點,如超類構造函數與子類不同,通過rename method方法可以解決。
- 如果不需要超類實例,可以將超類聲明為抽象類。
- 逐一使用字段下移、函數下移將源類的特性移動到子類。
11.7 ExtractSuperclass(提煉超類)336
- 情形:兩個類有相似特性。
- 做法:為兩個類建立一個超類,將相同特性移至超類。
11.8 Extract Interface(提煉接口)341
- 情形:某組用戶只使用類責任區中一個特定子集或者兩個類的接口有部分相同。
- 做法:將相同子集提煉到獨立的接口中。
- 區別:提煉超類是提煉共同代碼,提煉接口時提煉共同接口。
- 具體情形:如果某個類在不同環境下扮演截然不同的角色,使用接口就是個好主意。
11.9 Collapse Hierarchy(折疊繼承體系)344
- 情形:超類和子類之間區別不大。
- 做法:將它們合為一體。
11.10 Form TemPlate Method(塑造模板函數)344
- 情形:你有一些子類,其中相應的函數以相同順序執行類似的操作,但各個操作的細節有所不同。
- 做法:將這些小操作分別放進獨立函數中,并保持它們都有相同的簽名,于是原函數也變得相同了。然后將原函數上移至超類,運用多態來避免重復代碼。
- 原因:雖然使用了繼承,但是函數重復應盡量避免。
11.11 Replace inherited with Delegation(以委托取代繼承)352
- 情形:某個子類只使用超類接口中一部分,或是根本不需要繼承而來的數據
- 做法:在子類中新建一個字段用以保存超類,調整子類函數,令它委托超類,然后去掉兩者之間的繼承關系。
11.12 Replace Delegation with Inherited(以繼承取代委托)352
- 情形:在兩個類之間使用委托關系,并經常為整個接口編寫許多極簡單的委托函數,
- 做法:讓委托類繼承受托類。
- 告誡:
- 如果并沒有使用受托類的所有函數,那么就不要使用這個方法。因為子類應該總是遵循超類的接口,如果委托過多可以通過移除“中間人”方法讓客戶端調用受托函數,或者“提煉超類”,讓兩個類的接口提煉到超類中。類似的還可以使用“提煉接口”方法。
- 如果受托對象被不止一個其他對象共享,而且受托對象是可變的時候,那么這濕乎乎不能講委托關系替換為繼承關系。
……