### Introduce Assertion(引入斷言)
某一段代碼需要對程序狀態(state)做出某種假設。
以assertion(斷言)明確表現這種假設。
~~~
double getExpenseLimit() {
// should have either expense limit or a primary project
return (_expenseLimit != NULL_EXPENSE) ?
_expenseLimit:
_primaryProject.getMemberExpenseLimit();
}
~~~
=>
~~~
double getExpenseLimit() {
Assert.isTrue (_expenseLimit != NULL_EXPENSE || _primaryProject != null);
return (_expenseLimit != NULL_EXPENSE) ?
_expenseLimit:
_primaryProject.getMemberExpenseLimit();
}
~~~
**動機(Motivation)**
常常會有這樣一段代碼:只有當某個條件為真時,該段代碼才能正常運行。例如「平方報計算」只對正值才能進行(譯注:這里沒考慮復數與虛數),又例如某個對象 可能假設其值域(fields)至少有一個不等于null。
這樣的假設通常并沒有在代碼中明確表現出來,你必須閱讀整個算法才能看出。有時程序員會以注釋寫出這樣的假設。而我要介紹的是一種更好的技術:使用assertion(斷言)明確標明這些假設。
assertion 是一個條件式,應該總是為真。如果它失敗,表示程序員犯了錯誤。因此assertion的失敗應該導致一個unchecked exception[7](不可控異常〕。Assertions 絕對不能被系統的其他部分使用。實際上程序最后成品往往將assertions 統統刪除。因此,標記「某些東西是個assertion」是很重要的。
[7]譯注:所謂unchecked exception 是指「未曾于函數簽名式(signature)中列出」的異常。
Assertions 可以作為交流與調試的輔助。在交流(溝通〕的角度上,assertions 可以幫助程序閱讀者理解代碼所做的假設;在調試的角度上,assertions 可以在距離「臭蟲」最近的地方抓住它們。當我編寫自我測試代碼的時候,我發現,assertions 在調試方面的幫助變得不那么重要了,但我仍然非常看重它們在交流方面的價值。
**作法(Mechanics)**
如果程序員不犯錯,assertions 就應該不會對系統運行造成任何影響,所以加入assertions 永遠不會影響程序的行為。
- 如果你發現代碼「假設某個條件始終(必須)為真],就加入一個assertion 明確說明這種情況。
- 你可以新建一個Assert class,用于處理各種情況下的assertions 。
注意,不要濫用assertions 。請不要使用它來檢查你「認為應該為真」的條件,請只使用它來檢查「一定必須為真」的條件。濫用assertions 可能會造成難以維護的重復邏輯。在一段邏輯中加入assertions 是有好處的,因為它迫使你重新考慮這段代 碼的約束條件。如果「不滿足這些約朿條件,程序也可以正常運行」,assertions 就不會帶給你任何幫助,只會把代碼變得混亂,并且有可能妨礙以后的修改。
你應該常常問自己:如果assertions 所指示的約束條件不能滿足,代碼是否仍能正常運行?如果可以,就把assertions 拿掉。
另外,還需要注意assertions 中的重復代碼。它們和其他任何地方的重復代碼一樣不好聞。你可以大膽使用Extract Method 去掉那些重復代碼。
**范例:(Example)**
下面是一個簡單例子:開支(經費)限制。后勤部門的員工每個月有固定的開支限額;業務部門的員工則按照項目的開支限額來控制自己的開支。一個員工可能沒有開支額度可用,也可能沒有參與項目,但兩者總得要有一個(否則就沒有經費可用 了)。在開支限額相關程序中,上述假設總是成立的,因此:
~~~
class Employee...
private static final double NULL_EXPENSE = -1.0;
private double _expenseLimit = NULL_EXPENSE;
private Project _primaryProject;
double getExpenseLimit() {
return (_expenseLimit != NULL_EXPENSE) ?
_expenseLimit:
_primaryProject.getMemberExpenseLimit();
}
boolean withinLimit (double expenseAmount) {
return (expenseAmount <= getExpenseLimit());
}
~~~
這段代碼包含了一個明顯假設:任何員工要不就參與某個項目,要不就有個人開支限額。我們可以使用assertion 在代碼中更明確地指出這一點:
~~~
double getExpenseLimit() {
Assert.isTrue (_expenseLimit != NULL_EXPENSE || _primaryProject != null);
return (_expenseLimit != NULL_EXPENSE) ?
_expenseLimit:
_primaryProject.getMemberExpenseLimit();
}
~~~
這條assertion 不會改變程序的任何行為。另一方面,如果assertion中的條件不為真,我就會收到一個運行期異常:也許是在withinLimit() 函數中拋出一個空指針(null pointer)異常,也許是在Assert.isTrue() 函數中拋出一個運行期異常。有時assertion 可以幫助程序員找到臭蟲,因為它離出錯地點很近。但是,更多時候,assertion 的價值在于:幫助程序員理解代碼正確運行的必要條件。
我常對assertion 中的條件式使用Extract Method ,也許是為了將若干地方的重復碼提煉到同一個函數中,也許只是為了更清楚說明條件式的用途。
在Java 中使用assertions 有點麻煩:沒有一種簡單機制可以協助我們插入這東西[8]。 assertions 可被輕松拿掉,所以它們不可能影響最終成品的性能。編寫一個輔助類(例如Assert class)當然有所幫助,可惜的是assertions 參數中的任何表達式不論什么情況都一定會被執行一遍。阻止它的惟一辦法就是使用類似下面的手法:
[8[譯注:J2SE1.4已經支持assert語句。
~~~
double getExpenseLimit() {
Assert.isTrue (Assert.ON &&
(_expenseLimit != NULL_EXPENSE || _primaryProject != null));
return (_expenseLimit != NULL_EXPENSE) ?
_expenseLimit:
_primaryProject.getMemberExpenseLimit();
}
~~~
或者是這種手法:
~~~
double getExpenseLimit() {
if (Assert.ON)
Assert.isTrue (_expenseLimit != NULL_EXPENSE || _primaryProject != null);
return (_expenseLimit != NULL_EXPENSE) ?
_expenseLimit:
_primaryProject.getMemberExpenseLimit();
}
~~~
如果Assert.ON 是個常量,編譯器(譯注:而非運行期間)就會對它進行檢查; 如果它等于false ,就不再執行條件式后半段代碼。但是,加上這條語句實在有點丑陋,所以很多程序員寧可僅僅使用Assert.isTrue() 函數,然后在項目結束前以過濾程序濾掉使用assertions 的每一行代碼(可以使用Perl 之類的語言來編寫這樣 的過濾程序)。
Assert class應該有多個函數,函數名稱應該幫助程序員理解其功用。除了isTrue() 之外,你還可以為它加上equals() 和shouldNeverReachHere() 等函數。
# 章節十 簡化函數調用
在對象技術中,最重要的概念莫過于「接口」(interface)。容易被理解和被使用的接口,是開發良好面向對象軟件的關鍵。本章將介紹「使接口變得更簡潔易用」 的重構手法。
最簡單也最重要的一件事就是修改函數名稱。「名稱」是程序寫作者與閱讀者交流的關鍵工具。只要你能理解一段程序的功能,就應該大膽地使用Rename Method 將你所知道的東西傳達給其他人。另外,你也可以(并且應該)在適當時機修改變量名稱和class 名稱。不過,總體來說,「修改名稱」只是相對比較簡單 的文本替換功夫,所以我沒有為它們提供單獨的重構項目。
函數參數在「接口」之中扮演十分重要的角色。 Add Parameter 和Remove Parameter 都是很常見的重構手法。初始接觸面向對象技術的程序員往往使用很長的參數列(parameter lists),這在其他開發環境中是很典型的方式。但是, 使用對象技術,你可以保持參數列的簡短,以下有一些相關的重構可以幫助你縮短參數列。如果來自同一對象的數個值被當作參數傳遞,你可以運用 Preserve Whole Object 將它們替換為單一對象,從而縮短參數列。如果此前并不存在這樣一個對象,你可以運用Introduce Parameter Object將它創建出來。如果函數參數來自該函數可取用的一個對象,則可以使用 Replace Parameter with Method 避免傳遞參數。如果某些參數被用來在條件式中做選擇依據,你可以實施 Replace Parameter with Explicit Methods。另外,你還可以使用Parameterize Method 為數個相似函數添加參數,將它們合并到一起。
關于縮減參數列的重構手法,Doug Lea 對我提出了一個警告:并發編程(con-current programming)往往需要使用較長的參數列,因為這樣你可以保證傳遞給函數的參數都是不可被修改的,就像內置型對象和value object 一定地不可變。通常,你可以使用不可變對象(immutable object)取代這樣的長參數列,但另一方面你也必須對此類重構保持謹慎。
多年來我一直堅守一個很有價值的習慣:明確地將「修改對象狀態」的函數(修改函數,modifiers)和「查詢對象狀態」的函數(查詢函數,queries)分開設計。不知道多少次,我因為將這兩種函數混在一起而麻煩纏身;不知道多少次,我看到別 人也因為同樣的原因而遇到同樣的麻煩。因此,如果我看到這兩種函數混在一起, 我就使用 Separate Query from Modifier 將它們分開。
良好的接口只向用戶展現必須展現的東西。如果一個接口暴露了過多細節,你可以將不必要暴露的東西隱藏起來,從而改進接口的質量。毫無疑問,所有數據都應該隱藏起來(希望你不需要我來告訴你這一點),同時,所有可以隱藏的函數都應該被隱藏起來。進行重構時,你往往需要暫時暴露某些東西,最后再以 Hide Method 和Remove Setting Method 將它們隱藏起來。
構造函數(constructors)是Java 和C++ 中特別麻煩的一個東西,因為它強迫你必須知道「待建對象」屬于哪一個class ,而你往往并不需要知道這一點。你可以使用Replace Constructor with Factory Method 避免了解這「被迫了解的一點」。
轉型(casting)是Java 程序員心中另一處永遠的痛。你應該盡量使用Encapsulate Downcast 將「向下轉型動作」封裝隱藏起來,避免讓class 用戶做那種動作。
和許多現代編程語言一樣,Java 也有異常處理(exception-handing)機制,這使得錯誤處理(error handling)相對容易一些。不習慣使用異常的程序員,往往會以錯誤代碼(error code)表示程序遇到的麻煩。你可以使用Replace Error Code with Exception 來運用這些嶄新的異常特性。但有時候異常也并不是最合適的選擇,你應該實施Replace Exception with Test 先測試一番。
- 譯序 by 侯捷
- 譯序 by 熊節
- 序言
- 前言
- 章節一 重構,第一個案例
- 起點
- 重構的第一步
- 分解并重組statement()
- 運用多態(Polymorphism)取代與價格相關的條件邏輯
- 結語
- 章節二 重構原則
- 何謂重構
- 為何重構
- 「重構」助你找到臭蟲(bugs)
- 何時重構
- 怎么對經理說?
- 重構的難題
- 重構與設計
- 重構與性能(Performance)
- 重構起源何處?
- 章節三 代碼的壞味道
- Duplicated Code(重復的代碼)
- Long Method(過長函數)
- Large Class(過大類)
- Long Parameter List(過長參數列)
- Divergent Change(發散式變化)
- Shotgun Surgery(散彈式修改)
- Feature Envy(依戀情結)
- Data Clumps(數據泥團)
- Primitive Obsession(基本型別偏執)
- Switch Statements(switch驚悚現身)
- Parallel Inheritance Hierarchies(平行繼承體系)
- Lazy Class(冗贅類)
- Speculative Generality(夸夸其談未來性)
- Temporary Field(令人迷惑的暫時值域)
- Message Chains(過度耦合的消息鏈)
- Middle Man(中間轉手人)
- Inappropriate Intimacy(狎昵關系)
- Alternative Classes with Different Interfaces(異曲同工的類)
- Incomplete Library Class(不完美的程序庫類)
- Data Class(純稚的數據類)
- Refused Bequest(被拒絕的遺贈)
- Comments(過多的注釋)
- 章節四 構筑測試體系
- 自我測試代碼的價值
- JUnit測試框架
- 添加更多測試
- 章節五 重構名錄
- 重構的記錄格式
- 尋找引用點
- 這些重構準則有多成熟
- 章節六 重新組織你的函數
- Extract Method(提煉函數)
- Inline Method(將函數內聯化)
- Inline Temp(將臨時變量內聯化)
- Replace Temp with Query(以查詢取代臨時變量)
- Introduce Explaining Variable(引入解釋性變量)
- Split Temporary Variable(剖解臨時變量)
- Remove Assignments to Parameters(移除對參數的賦值動作)
- Replace Method with Method Object(以函數對象取代函數)
- Substitute Algorithm(替換你的算法)
- 章節七 在對象之間搬移特性
- Move Method(搬移函數)
- Move Field(搬移值域)
- Extract Class(提煉類)
- Inline Class(將類內聯化)
- Hide Delegate(隱藏「委托關系」)
- Remove Middle Man(移除中間人)
- Introduce Foreign Method(引入外加函數)
- Introduce Local Extension(引入本地擴展)
- 章節八 重新組織數據
- Self Encapsulate Field(自封裝值域)
- Replace Data Value with Object(以對象取代數據值)
- Change Value to Reference(將實值對象改為引用對象)
- Replace Array with Object(以對象取代數組)
- Replace Array with Object(以對象取代數組)
- Duplicate Observed Data(復制「被監視數據」)
- Change Unidirectional Association to Bidirectional(將單向關聯改為雙向)
- Change Bidirectional Association to Unidirectional(將雙向關聯改為單向)
- Replace Magic Number with Symbolic Constant(以符號常量/字面常量取代魔法數)
- Encapsulate Field(封裝值域)
- Encapsulate Collection(封裝群集)
- Replace Record with Data Class(以數據類取代記錄)
- Replace Type Code with Class(以類取代型別碼)
- Replace Type Code with Subclasses(以子類取代型別碼)
- Replace Type Code with State/Strategy(以State/strategy 取代型別碼)
- Replace Subclass with Fields(以值域取代子類)
- 章節九 簡化條件表達式
- Decompose Conditional(分解條件式)
- Consolidate Conditional Expression(合并條件式)
- Consolidate Duplicate Conditional Fragments(合并重復的條件片段)
- Remove Control Flag(移除控制標記)
- Replace Nested Conditional with Guard Clauses(以衛語句取代嵌套條件式)
- Replace Conditional with Polymorphism(以多態取代條件式)
- Introduce Null Object(引入Null 對象)
- Introduce Assertion(引入斷言)
- 章節十一 處理概括關系
- Pull Up Field(值域上移)
- Pull Up Method(函數上移)
- Pull Up Constructor Body(構造函數本體上移)
- Push Down Method(函數下移)
- Push Down Field(值域下移)
- Extract Subclass(提煉子類)
- Extract Superclass(提煉超類)
- Extract Interface(提煉接口)
- Collapse Hierarchy(折疊繼承關系)
- Form Template Method(塑造模板函數)
- Replace Inheritance with Delegation(以委托取代繼承)
- Replace Delegation with Inheritance(以繼承取代委托)
- 章節十二 大型重構
- 這場游戲的本質
- Tease Apart Inheritance(梳理并分解繼承體系)
- Convert Procedural Design to Objects(將過程化設計轉化為對象設計)
- Separate Domain from Presentation(將領域和表述/顯示分離)
- Extract Hierarchy(提煉繼承體系)
- 章節十三 重構,復用與現實
- 現實的檢驗
- 為什么開發者不愿意重構他們的程序?
- 現實的檢驗(再論)
- 重構的資源和參考資料
- 從重構聯想到軟件復用和技術傳播
- 結語
- 參考文獻
- 章節十四 重構工具
- 使用工具進行重構
- 重構工具的技術標準(Technical Criteria )
- 重構工具的實用標準(Practical Criteria )
- 小結
- 章節十五 集成
- 參考書目