返回總目錄 7 Introduce Null Object(引入Null對象) 概要 你需要再三檢查某對象是否為null。 將null值替換為null對象。 動機 系統在使用對象的相關功能時,總要檢查對象是否為null,如果不為null,我們才會調用它的相關方法,完成某種邏輯。這樣的檢查在一個系統中 ...
7 Introduce Null Object(引入Null對象)
概要
你需要再三檢查某對象是否為null。
將null值替換為null對象。
動機
系統在使用對象的相關功能時,總要檢查對象是否為null,如果不為null,我們才會調用它的相關方法,完成某種邏輯。這樣的檢查在一個系統中出現很多次,相信任何一個設計者都不願意看到這樣的情況。為瞭解決這種問題,我們可以引入空對象,這樣,我們就可以擺脫大量程式化的代碼,對代碼的可讀性也是一個飛躍。
範例
以下代碼中,Site類表示地點。任何時候每個地點都擁有一個顧客,顧客信息以Customer表示:
public class Site { public Customer Customer { get; set; } } public class Customer { public string Name { get; set; } public BillingPlan Plan { get; set; } public PaymentHistory History { get; set; } } public class BillingPlan { public int BasicPay { get; set; } public BillingPlan() { BasicPay = 0; } public BillingPlan(int pay) { BasicPay = pay; } public static BillingPlan Basic() { return new BillingPlan(100); } } public class PaymentHistory { public int GetWeekDelinquentInLastYear() { return 100; } }
我們可能會這樣調用:
Site site = new Site(); Customer customer = site.Customer; BillingPlan plan; if (customer == null) { plan = BillingPlan.Basic(); } else { plan = customer.Plan; } string customerName; if (customer == null) { customerName = "occupant"; } else { customerName = customer.Name; } int weeksDelinquent; if (customer == null) { weeksDelinquent = 0; } else { weeksDelinquent = customer.History.GetWeekDelinquentInLastYear(); }
這個系統中可能會有許多地方使用Site和Customer對象,它們都必須檢查Customer對象是否等於null,而這樣的檢查完全是重覆的。下麵使用空對象進行重構。
首先新建一個NullCustomer,並修改Customer,使其支持“對象是否為null”的檢查:
public class Customer { public virtual bool IsNull() { return false; } }
class NullCustomer : Customer { public override bool IsNull() { return true; } }
接下來,在Customer中加入一個函數,專門用來創建NullCustomer對象。這樣一來,用戶就不必知道空對象的存在了:
public class Customer { public static Customer NewNull() { return new NullCustomer(); } }
對於所有提供Customer對象的地方,將他們都加以修改,使它們不能返回null,而返回一個NullCustomer對象。
public class Site { private Customer _customer; public Customer Customer { get => _customer ?? Customer.NewNull(); set => _customer = value; } }
另外,還要修改所有使用Customer對象的地方,讓它們以IsNull()函數進行檢查,不再使用==null的檢查方式。
Site site = new Site(); Customer customer = site.Customer; BillingPlan plan; if (customer.IsNull()) { plan = BillingPlan.Basic(); } else { plan = customer.Plan; } string customerName; if (customer.IsNull()) { customerName = "occupant"; } else { customerName = customer.Name; } int weeksDelinquent; if (customer.IsNull()) { weeksDelinquent = 0; } else { weeksDelinquent = customer.History.GetWeekDelinquentInLastYear(); }
但是到目前為止,使用IsNull()函數尚未帶來任何好處。下麵就把相關行為移到NullCustomer中並去除條件表達式。
首先為NullCustomer加入一個合適的函數,通過這個函數來取得顧客名稱,為此先將Customer中的屬性改為虛屬性,這樣方便在子類中重寫:
class NullCustomer : Customer { public override string Name => "occupant";
public override bool IsNull() { return true; } }
現在,調用的時候就可以去除條件代碼了,下麵的代碼:
if (customer.IsNull()) { customerName = "occupant"; } else { customerName = customer.Name; }
現在變成這樣調用:
string customerName=customer.Name;
接下來以相同的手法處理其他函數,使它們對相應的查詢做出最合適的響應。於是下麵這樣的客戶端程式:
BillingPlan plan; if (customer.IsNull()) { plan = BillingPlan.Basic(); } else { plan = customer.Plan; }
就變成了這樣:
BillingPlan plan = customer.Plan;
class NullCustomer : Customer { public override BillingPlan Plan => BillingPlan.Basic(); }
這裡註意一下:只有當大多數客戶代碼都要求使用空對象做出相同響應時,這樣的行為搬移才有意義。請註意,是“大多數”而不是“所有”。任何用戶如果需要空對象做出不同響應,仍然可以使用IsNull()函數來測試。只要大多數客戶端都要求空對象做出相同響應,就可以調用預設的Null行為。
上述例子中略有差異的是下麵這段代碼:
int weeksDelinquent; if (customer.IsNull()) { weeksDelinquent = 0; } else { weeksDelinquent = customer.History.GetWeekDelinquentInLastYear(); }
我們可以新建一個NullPaymentHistory類,用以處理這種情況:
class NullPaymentHistory : PaymentHistory { public override int GetWeekDelinquentInLastYear() { return 0; } }
public class PaymentHistory { public virtual int GetWeekDelinquentInLastYear() { return 100; } public static PaymentHistory NewNull() { return new NullPaymentHistory(); } }
現在來修改NullCustomer,讓其返回一個NullPaymentHistory對象:
class NullCustomer : Customer { public override string Name => "occupant"; public override BillingPlan Plan => BillingPlan.Basic(); public override PaymentHistory History => PaymentHistory.NewNull(); public override bool IsNull() { return true; } }
然後,就可以刪除這一行的條件代碼:
int weeksDelinquent = customer.History.GetWeekDelinquentInLastYear();
可以看到,使用Null對象後,代碼結構很清晰,代碼量大大減少。
小結
空對象通過isNull對==null的替換,顯得更加優雅,更加易懂;並不依靠Client來保證整個系統的穩定運行同時能夠實現對空對象情況的定製化的控制,能夠掌握處理空對象的主動權。
階段性小結
條件邏輯有可能十分複雜,因此重構手法之簡化條件表達式第1-7小節提供重構手法,專門用來簡化它們。其中一項核心重構就是Decompose Conditional,可將一個複雜的條件邏輯分成若幹小塊。這項重構很重要,因為它使得“分支邏輯”和“操作細節”分離。
如果代碼中有多次測試有相同結果,應該實施Consolidate Conditional Expression;如果條件代碼中有任何重覆,可以運用Consolidate Duplicate Conditional Fragments將重覆成分去掉。
如果程式開發者堅持“單一齣口”原則,那麼為讓條件表達式也遵循這一原則,他往往會在其中加入控制標記,可以使用Replace Nested Conditional with Guard Clauses標示出那些特殊情況,並使用Remove Control Flag去除那些討厭的控制標記。
在面向對象程式中如果出現switch語句,可以考慮運用Replace Conditional with Polymorphism將它替換為多態。
多態還有一種十分有用但鮮為人知的用途:通過Introduce Null Object去除對於null值的檢驗。
To Be Continued……