Introduce Null Object (引入Null对象)

Summary:

你需要再三检查某对象是否为null。null值替换为null对象

Motivation: 

多态的最根本好处在于:你不必再向对象询问“你是什么类型”,而后根据得到的答案调用对象的某个行为—你只管调用该行为就是了,其他的一切多态机制会为你安排妥当。当某个字段内容是null时,多态可扮演另一个较不直观(亦较不为人所知)的用途。

Mechanics:

1.为源类建立一个子类,使其行为就像是源类的null版本。在源类和null子类中都加上isNull() 函数,前者的isNull() 应该返回false,后者的isNull() 应该返回true。

  • 下面这个办法也可能对你有所帮助:建立一个nullable接口,将isNull() 函数放在其中,让源类实现这个接口
  • 另外,你也可以创建一个测试接口,专门用来检查对象是否为null。

2.编译

3.找出所有“索求源对象却获得一个null”的地方。修改这些地方,使它们改而获得一个空对象。

4. 找出所有“将源对象与null作比较”的地方。修改这些地方,使它们调用isNull() 函数。

  • 你可以每次只处理一个源对象及其客户程序,编译并测试后,再处理另一个源对象
  • 你可以在“不该再出现null”的地方放上一些断言,确保null的确不再出现。这肯能对你有所帮助

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。这和“空对象的访问函数通常返回令一个空对象”是一样的道理






你可能感兴趣的:(Introduce Null Object (引入Null对象))