### Introduce Null Object(引入Null 對象)
你需要再三檢查「某物是否為null value」。
將null value (無效值)替換為null object(無效物)。
~~~
if (customer == null) plan = BillingPlan.basic();
else plan = customer.getPlan();
~~~
=>

**動機(Motivation)**
多態(polymorphism )的最根本好處在于:你不必再向對象詢問「你是什么型別」 而后根據得到的答案調用對象的某個行為——你只管調用該行為就是了,其他的一切多態機制會為你安排妥當。當你的某個值域內容是null value 時,多態可扮演另一個較不直觀(亦較不為人所知)的用途。讓我們先聽聽Ron Jeffries 的故事。
Ron Jeffries
我們第一次使用Null Object 模式,是因為Rih Garzaniti 發現,系統在對對象發送一個消息之前,總要檢査對象是否存在,這樣的檢査出現很多次。我們可能會向一個對象索求它所相關的Person 對象,然后再問那個對象是否為null 。如果對象的確存在,我們才能調用它的rate() 函數以查詢這個人的薪資級別。我們在好些地方都是這樣做的, 造成的重復代碼讓我們很煩心。
所以.我們編寫了一個MissingPerson class,讓它返回 '0' 薪資等級(我們把null objects 稱為missing object(虛構對象)。很快地MissingPerson 就有了很多函數,rate() 自然是其中之一。如今我們的系統有超過80個null object classes。
我們常常在顯示信息的時候使用null object。例如我們想要顯示一個Person 對象信息,它大約有20個instance 變量。如果這些變量可被設為null,那么打印一個Person 對象的工作將非常復雜。所以我們不讓instance 變量被設為null ,而是插入各式各樣的null objects ——它們都知道如何正常(正確地)顯示自己。這樣,我們就可以擺脫大量代碼。
我們對null object 的最聰明運用,就是拿它來表示不存在的Gemstone session。我們使用Gemstone 數據庫來保存成品(程序代碼),但我們更愿息在沒有數據庫的情況下進行開發,毎過一周左右再把新碼放進Gemstone 數據庫。然而在代碼的某些地方,我們必須登錄(log in)一個Gemstone session。當我們沒有Gemstone 數據庫時,我們就僅僅安插一個miss Gemstone session,其接口和真正的Gemstone session 一模一樣,使我們無需判斷數據庫是否存在,就可以進行開發和測試。
null object 的另一個用途是表現出「虛構的箱倉」(missing bin)。所謂「箱倉],這里是指群集(collection),用來保存某些薪資值,并常常謠要對各個薪資值進行加和或遍歷。如果某個箱倉不存在,我們就給出一個虛構的箱倉對象,其行為和一個空箱倉(empty bin)一樣;這個虛構箱倉知道自己其實不帶任何數據,總值為0。通過這種作法,我們就不必為上千位員工每人產生數十來個空箱(empty bins)對象了。
使用null objects 有個非常有趣的性質:好事絕對不會因為null objects 而「被破壞」。由于null objects 對所有外界請求的響應,都像real objects 的響應一樣,所以系統行為總是正常的。但這并非總是好事,有吋會造成問題的偵測和查找上的困難,因為從來沒有任何東西被破壞。當然,只要認真檢查一下,你就會發現null objects 有時出現在不該出現的地方。
請記住:null objects 一定是常量,它們的任何成分都不會發生變化。因此我們可以使用Singleton 模式[Gang of Four]來實現它們。例如不管任何時候,只要你索求一個MissingPerson 對象,你得到的一定是MissingPerson 的惟一實體。
關于Null Object 模式,你可以在Woolf [Woolf] 中找到更詳細的介紹。
**作法(Mechanics)**
- 為source class 建立一個subclass ,使其行為像source class 的null 版本。在source class 和null class 中都加上isNull() 函數,前者的isNull() 應該返回false,后者的isNull() 應該返回true。
- 下面這個辦法也可能對你有所幫助:建立一個nullable 接口,將isNull() 函數放在其中,讓source class 實現這個接口。
- 另外,你也可以創建一個testing 接口,專門用來檢查對象是否為null。
- 編譯。
- 找出所有「索求source object 卻獲得一個null 」的地方。修改這些地方,使它們改而獲得一個null object。
- 找出所有「將source object 與null 做比較」的地方。修改這些地方,使它們調用isNull() 函數。
- 你可以每次只處理一個source object 及其客戶程序,編譯并測試后, 再處理另一個source object 。
- 你可以在「不該再出現null value」的地方放上一些assertions(斷言), 確保null 的確不再出現。這可能對你有所幫助。
- 編譯,測試。
- 找出這樣的程序點:如果對象不是null ,做A動作,否則做B 動作。
- 對于每一個上述地點,在null class 中覆寫A動作,使其行為和B 動作相同。
- 使用上述的被覆寫動作(A),然后刪除「對象是否等于null」的條件測試。編譯并測試。
**范例:(Example)**
—家公用事業公司的系統以Site 表示地點(場所)。庭院宅等和集合公寓(apartment)都使用該公司的服務。任何時候每個地點都擁有(或說都對應于)一個顧客,顧客信息以Customer 表示:
~~~
class Site...
Customer getCustomer() {
return _customer;
}
Customer _customer;
~~~
Customer 有很多特性,我們只看其中三項:
~~~
class Customer...
public String getName() {...}
public BillingPlan getPlan() {...}
public PaymentHistory getHistory() {...}
~~~
本系統又以PaymentHistory 表示顧客的付款記錄,它也有它自己的特性:
~~~
public class PaymentHistory...
int getWeeksDelinquentInLastYear()
~~~
上面的各種取值函數(getter)允許客戶取得各種數據。但有時候一個地點的顧客搬走了,新顧客還沒搬進來,此時這個地點就沒有顧客。由于這種情況有可能發生,所以我們必須保證Customer 的所有用戶都能夠處理「Customer 對象等于null」的情況。下面是一些示例片段:
~~~
Customer customer = site.getCustomer();
BillingPlan plan;
if (customer == null) plan = BillingPlan.basic();
else plan = customer.getPlan();
...
String customerName;
if (customer == null) customerName = "occupant";
else customerName = customer.getName();
...
int weeksDelinquent;
if (customer == null) weeksDelinquent = 0;
else weeksDelinquent = customer.getHistory().getWeeksDelinquentInLastYear();
~~~
這個系統中可能使用許多Site 和Customer ,它們都必須檢查Customer 對象是否等于null ,而這樣的檢查完全是重復的。看來是使用null object 的時候了。
首先新建一個NullCustomer ,并修改Customer ,使其支持「對象是否為null」的檢查:
~~~
class NullCustomer extends Customer {
public boolean isNull() {
return true;
}
}
class Customer...
public boolean isNull() {
return false;
}
protected Customer() {} //needed by the NullCustomer
~~~
如果你無法修改Customer ,你可以建立一個新的testing 接口。
如果你喜歡,也可以新建一個接口,昭告大家「這里使用了null object 」:
~~~
interface Nullable {
boolean isNull();
}
class Customer implements Nullable
~~~
我還喜歡加入一個factory method,專門用來創建NullCustomer 對象。這樣一來,用戶就不必知道null class 的存在了:
~~~
class Customer...
static Customer newNull() {
return new NullCustomer();
}
~~~
接下來的部分稍微有點麻煩。對于所有「返回null」的地方,我都要將它改為「返回null object」,此外我還要把foo==null這樣的檢查替換成foo.isNull()。我發現下列辦法很有用:查找所有『索求Customer 對象」的地方,將它們都加以修改, 使它們不能返回null ,改而返回一個NullCustomer 對象。
~~~
class Site...
Customer getCustomer() {
return (_customer == null) ?
Customer.newNull():
_customer;
}
~~~
另外,我還要修改所有「使用Customer 對象」的地方,讓它們以isNull() 函數進行檢查,不再使用"== null"”檢查方式。
~~~
Customer customer = site.getCustomer();
BillingPlan plan;
if (customer.isNull()) plan = BillingPlan.basic();
else plan = customer.getPlan();
...
String customerName;
if (customer.isNull()) customerName = "occupant";
else customerName = customer.getName();
...
int weeksDelinquent;
if (customer.isNull()) weeksDelinquent = 0;
else weeksDelinquent = customer.getHistory().getWeeksDelinquentInLastYear();
~~~
毫無疑問,這是本項重構中最需要技巧的部分。對于每一個需要替換的「可能等于null」的對象,我都必須找到「它是否等于null」的所有檢查動作,并逐一替換。 如果這個對象被傳播到很多地方,追蹤起來就很困難。上述范例中,我必須找出每一個型別為Customer 的變量,以及它們被使用的地點。很難將這個過程分成更小的步驟。有時候我發現「可能等于null」的對象只在某幾處被用到,那么替換工作比較簡單。但是大多數時候我必須做大量替換工作。還好,撤銷這些替換并不困難,因為我可以不太困難地找出對isNull() 的調用動作,但這畢竟也是很零亂很惱人 的。
這個步驟完成之后,如果編譯和測試都順利通過,我就可以寬心地露出笑容了。接下來的動作比較有趣。到目前為止,使用isNull() 函數尚未帶來任何好處。只有當我把相關行為移到NullCustomer class 中并去除條件式之后,我才能得到切實的利益。我可以逐一將各種行為(函數)移過去。首先從「取得顧客名稱」這個函數開始。此時的客戶端代碼大約如下:
~~~
String customerName;
if (customer.isNull()) customerName = "occupant";
else customerName = customer.getName();
~~~
首先為NullCustomer 加入一個合適的函數,通過這個函數來取得顧客名稱:
~~~
class NullCustomer...
public String getName(){
return "occupant";
}
~~~
現在,我可以去掉條件代碼了:
~~~
String customerName = customer.getName();
~~~
接下來我以相同手法處理其他函數,使它們對相應查詢做出合適的響應。此外我還可以對「修改函數」(modifiers)做適當的處理。于是下面這樣的客戶端程序:
~~~
if (! customer.isNull())
customer.setPlan(BillingPlan.special());
~~~
就變成了這樣:
~~~
customer.setPlan(BillingPlan.special());
class NullCustomer...
public void setPlan (BillingPlan arg) {}
~~~
請記住:只有當大多數客戶代碼都要求null object 做出相同響應時,這樣的行為搬移才有意義。注意我說的是「大多數」而不是「所有」。任何用戶如果需要null object 作出不同響應,他仍然可以使用isNull() 函數來測試。只要大多數客戶端都要求null object 做出相同響應,他們就可以調用缺省的null 行為,而你也就受益匪淺了。
上述范例略帶差異的某種情況是,某些客戶端使用Customer 函數的運算結果:
~~~
if (customer.isNull()) weeksDelinquent = 0;
else weeksDelinquent = customer.getHistory().getWeeksDelinquentInLastYear();
~~~
我可以新建一個NullPaymentHistory class,用以處理這種情況:
~~~
class NullPaymentHistory extends PaymentHistory...
int getWeeksDelinquentInLastYear() {
return 0;
}
~~~
并修改NullCustomer,讓它返回一個NullPaymentHistory 對象:
~~~
class NullCustomer...
public PaymentHistory getHistory() {
return PaymentHistory.newNull();
}
~~~
然后,我同樣可以刪除這一行條件代碼:
~~~
int weeksDelinquent = customer.getHistory().getWeeksDelinquentInLastYear();
~~~
你常常可以看到這樣的情況:null objects 會返回其他null objects 。
**范例:另一種做法,Testing Interface**
除了定義isNull() 之外,你也可以建立一個用以檢查「對象是否為null」的接口。 使用這種辦法,必須新建一個Null 接口,其中不定義任何函數:
~~~
interface Null {}
~~~
然后,讓null object 實現Null 接口:
~~~
class NullCustomer extends Customer implements Null...
~~~
然后,我就可以用instanceof 操作符檢查對象是否為null :
~~~
aCustomer instanceof Null
~~~
通常我盡量避免使用instanceof 操作符,但在這種情況下,使用它是沒問題的。而且這種作法還有另一個好處:不需要修改Customer 。這么一來即使無法修改Customer 源碼,我也可以使用null object 。
其他特殊情況
使用本項重構時,你可以有數種不同的null objects ,例如你可以說「沒有顧客」(新建的房子和暫時沒人住的房子)和「不知名顧客」(有人住,但我們不知道是誰) 這兩種情況是不同的。果真如此,你可以針對不同的情況建立不同的null class。有時候null objects 也可以攜帶數據,例如不知名顧客的使用記錄等等,于是我們可以在查出顧客姓名之后將帳單寄給他。
本質上來說,這是一個比Null Object 模式更大的模式:Special Case 模式。所謂special case class(特例類)是某個class 的特殊情況,有著特殊的行為。因此表示「不知名顧客」的UnknowCustomer 和表示「沒有顧客」的NoCustomer 都是Customer 的特例。你經常可以在表示數量的classes 中看到這樣的「特例類」,例如Java 浮點數有「正無窮大」、「負無窮大」和「非數量」(NaN)等特例。special case class(特例類)的價值是:它們可以降低你的「錯誤處理」開銷。例如浮點運算決不會拋出異常。如果你對NaN做浮點運算,結果也會是個NaN。這和「null object 的訪問函數通常返回另一個null object 」是一樣的道理。
- 譯序 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 )
- 小結
- 章節十五 集成
- 參考書目