Fork me on GitHub

重构手法之简化条件表达式【4】

返回总目录

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……

posted @ 2017-11-29 08:59  NaYoung  阅读(771)  评论(1编辑  收藏  举报