Summary:
你需要再三检查某对象是否为null。将null值替换为null对象。
Motivation:
多态的最根本好处在于:你不必再向对象询问“你是什么类型”,而后根据得到的答案调用对象的某个行为—你只管调用该行为就是了,其他的一切多态机制会为你安排妥当。当某个字段内容是null时,多态可扮演另一个较不直观(亦较不为人所知)的用途。
Mechanics:
1.为源类建立一个子类,使其行为就像是源类的null版本。在源类和null子类中都加上isNull() 函数,前者的isNull() 应该返回false,后者的isNull() 应该返回true。
2.编译
3.找出所有“索求源对象却获得一个null”的地方。修改这些地方,使它们改而获得一个空对象。
4. 找出所有“将源对象与null作比较”的地方。修改这些地方,使它们调用isNull() 函数。
5.编译,测试
6.找出这样的程序点:如果对象不是null,做A动作,否则做B动作
7.对于每一个上述地点在null类中覆写A动作,使其行为和B动作相同。
8.使用上述被覆写的动作,然后删除“对象是否等于null”的条件测试。编译并测试。
范例
一家公用事业公司的系统以site表示地点,庭院宅第(house)和集体公寓(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 getWeekDelinquentInLastYear()
上面各种取值函数允许客户取得各种数据。但有时候一个地点的顾客伴奏了,新顾客还没有搬进来,此时这个地点就没有顾客。由于这种情况可能发生,所以我们必须保证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,而这样的检查完全是重复的。看来是使用空对象的时候了。
首先新建一个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,可以建立一个新的测试接口。
如果你喜欢,也可以新建一个接口,昭告大家“这里使用了空对象”
interface Nullable{ boolean isNull(); } class Customer implements Nullable
还可以加入一个工厂函数,专门用来创建NullCustomer对象。这样一来,用户就不必知道空对象的存在了:
class Customer ... static Customer newNull(){ return new NullCustomer(); }
接下来的部分稍微有点麻烦。对于所有“返回null”的地方,都要将它改为“返回空对象”。此外,还要把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"; elsed customerName = customer.getName(); ... int weeksDelinquent; if(customer.isNull()) weeksDelinquent = 0; else weeksDelinquent = customer.getHistory().getWeeksDelinquentInLasterYear();
毫无疑问,这是本项重构中最需要技巧的部分。对于每一个需要替换的可能等于null的对象,都必须找到所有检查它等于null的地方,并逐一替换。如果这个对象被传播到很多地方,追踪起来就很困难。上述范例中,我们必须找出每一个类型为Customer的变量,以及它们被使用的地点。很难讲这个过程分成更小的步骤。
这个步骤完成,如果编译和测试都顺利通过,我们将进行下一步动作。到目前为止,使用isNull()函数尚未带来任何好处。只有把相关行为移到NullCustomer中并去除条件表达式之后,才能得到实际的好处。我们可以逐一将各种行为移过去。首先从“取得顾客名称”这个函数开始。
首先为NullCustomer加入一个合适的函数,通过这个函数来取得顾客名称:
class NullCustomer... public String getName(){ return "occupant"; }
现在可以去掉条件代码了:
String customerName = customer.getName();
接下来我们以相同手法处理其他函数,使它们对相应查询租出合适的响应。
请注意:只有当大多数客户代码都要求空对象作出相同相应时,这样的行为搬移才有意义。
范例2:测试接口
除了定义isNull() 之外,也可以建立一个用以检查“对象是否为null”的接口。使用这种办法,需要新建一个Null接口,其中不定义任何函数:
interface Null {}
然后,让空对象实现Null接口:
class NullCustomer extends Customer implements Null...然后就可以用instanceof操作符检查对象是否为null:
通常我们应当尽量避免使用instanceof操作符,但在这种情况下,使用它是没有问题的。而且这种做法还有另一个好处:不需要修改Customer。这么一来即使无法修改Customer源码,也可以使用空对象
其他特殊情况
使用本项重构时,你可以有几种不同的空间对象,例如你可以说“没有顾客”和“不知名顾客”,这两种情况是不同的。果真如此,你可以针对不同的情况建立不同的空对象类。有时候空对象也可以携带数据,例如不知名顾客的使用记录等,于是我们可以在查出顾客姓名之后将账单寄给他。
本质上来说,这是一个比Null Object模式更大的模式:Special case模式。所谓特例类,也就是某个类的特殊情况,有着特殊行为。因此表示“不知名顾客”的UnknownCustomer和表示“没有顾客”的NoCustomer都是Customer的特例。我们经常可以在表示数量的类中看到这样的“特例类”,例如Java浮点数有“正无穷大”、“负无穷大”和“非数量”(NaN)等特例。特例类的价值是:它们可以降低你的“错误处理”开销,例如浮点运算决不会抛出异常。如果你对NaN的浮点运算,结果也会是个NaN。这和“空对象的访问函数通常返回令一个空对象”是一样的道理