使用空对象模式来避免空检查?

6

最近我了解到Null Object设计模式,我的同事说它可以用来消除代码中遇到的空指针检查。

例如,假设一个DAO类返回有关客户的信息(在名为CustomerVO的值对象中)。我的主类应该提取firstName和emailId,并向客户发送电子邮件。

...
CustomerVO custVO = CustomerDAO.getCustomer(customerID);
if(custVO != null) { // imp, otherwise we may get null ptr exception in next line
     sendEmail(custVO.getFirstName(), custVO.getEmailID());
}
...

这是一个非常简单的例子,但是基于值对象的复杂性,这样的空值检查可能会快速在代码中传播。
我有两个关于空值检查的问题: - 它们会让代码变得丑陋且难以阅读 - 经验不足的开发人员会在不必要的情况下进行空值检查,实际上应该抛出异常。例如,在上面的代码中,最好从getCustomer()本身抛出异常,因为如果无法找到给定CustID的客户信息,则表示CustID无效。
好的,回到空对象模式,我们可以使用“null” CustomerVO对象来隐藏空值检查吗?
CustomerVO {
   String firstName = "";
   String emailID = ""; 
}

不要这样做,这没有意义。你认为呢?

您在应用程序中遵循哪些方法来最小化空值检查?

5个回答

4
虽然空对象模式有其用处,但您仍需要在此处进行检查,否则您将尝试向一个空字符串的电子邮件地址发送电子邮件(或者您可以将检查推入sendEmail()中,它同样可以检查null)。如果CustomerVO类实现了sendEmail()方法,则空对象模式将非常有用。这样,您可以简单地链接调用,因为getCustomer()的契约将确保不会返回null引用:
CustomerDAO.getCustomer(customerID).sendEmail();

在这种情况下,sendEmail()方法会检查是否需要对特殊的“null对象”进行操作,并简单地不执行任何操作(或者执行适当的操作)。

1
+1,虽然我不希望我的客户有sendEmail方法。这只是给一个客户类太多的责任。这就像给每个类一个“print”方法,现在必须在各处实现打印逻辑或使每个类依赖于某个打印管理器,而它可以被限制为打印管理器和实际启动打印的表单/类给出打印管理器的类。空模式真正好的地方在于单元测试中,例如提供一个不做任何事情的日志类,这样您就不必修改正在测试的代码。 - Marjan Venema

2
在这种情况下,使用空对象可能不合适,因为默认值实际上可能会隐藏实际上是异常的内容。如果你发现自己必须检查是否有安全的空对象来执行其他活动,则空对象模式并没有给你带来任何好处。
正如你所说,许多新开发人员花费时间保护他们的代码免受比停止程序更糟糕的异常情况的影响。

我会重新表述为“使用这种设计时,空对象可能不适用”。我担心这是设计贫血对象的后果。 - xtofl

2
我发现这种代码实现方式有问题,这是一种常见模式。如果您分解实际操作,就不需要进行空值检验。在我看来,这里的问题在于您违反了单一职责原则。
方法CustomerVO custVO = CustomerDAO.getCustomer(customerID);执行了两个操作。首先,如果存在该客户,则返回客户;其次,如果没有此类客户,则返回null。这是两个不同的操作,应该分别编码。
更好的做法是:
bool customerExists = CustomerDAO.exists(customerID);

if (customerExists)
{
  CustomerVO custVO = CustomerDAO.getCustomer(customerID);
  sendEmail(custVO.getFirstName(), custVO.getEmailID());
}
else
{
   // Do whatever is appropriate if there is no such customer.
}

因此,将该方法分成两个部分,一个检查所请求的对象是否存在,另一个实际检索它。不需要任何异常,设计和语义非常清晰(在我的观点中,不存在返回null模式并不清晰)。此外,在这种方法中,如果所请求的客户不存在,则CustomerDAO.getCustomer(customerID)方法会抛出ArgumentException。毕竟,你要求一个Customer,但没有一个。

此外,在我看来,没有任何方法应该返回nullnull明确表示“我不知道正确答案是什么,我没有值可返回”。null不是一种含义,而是一种缺乏含义。问问自己,“为什么要返回我知道不应该发生的事情?你的方法GetCustomer显然应该返回一个Customer。如果你返回null,你只是将责任推回调用链,并破坏了合同。如果有有意义的默认值,请使用它,但在这里抛出异常可能会让你更加深入地思考正确的设计。


请确保在此处设计线程安全。 exists() 的初始调用可能会返回true,但是如果其他代码在调用之间删除了客户,则对getCustomer()的后续调用仍可能失败。例如,TryGet()这样的模式尝试减轻这种情况。请参见https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2?view=netframework-4.7.2,以获取此想法的示例。 - Adam Naylor

1

如果找不到客户,您的getCustomer方法应该抛出异常而不是返回null吗?

当然,答案取决于:几乎从不会出现客户ID不存在的情况吗?换句话说,这是一个特殊情况吗?如果是,那么抛出异常是合适的。

然而,在数据访问层中,某些东西不存在通常是很正常的。在这种情况下,最好不要抛出异常,因为它不是意外的、特殊的情况。

“返回一个具有空字段的非空对象”可能并不比返回null更好。如果没有添加一些检查代码,你怎么知道返回的对象是否“有效”,而且这些检查代码可能比null检查还糟糕?

因此,如果可能是正常状态,某些被获取的内容不存在,则null检查模式可能是最好的选择。如果是意外情况,则让数据访问方法抛出NotFound异常可能更好。


如果某些东西可能不存在,那么可以编写一个检查其是否存在的方法。这样,您只需要在异常情况下抛出异常。请看我的答案。 - nicodemus13

0

这个模式叫做NULL对象设计模式,而不是NULL检查设计模式。Null对象表示对象的缺失。当您有多个对象协同工作时,应该使用它。在您的情况下,您正在检查对象的存在,因此NULL检查应该是可以的。

NULL设计模式并不意味着要取代NULL异常处理。这只是NULL设计模式的一个附带好处,其目的是提供默认行为。

不应该用NULL设计模式对象替换NULL检查,因为这可能会导致应用程序中的静默缺陷。

请参阅下面的文章,其中详细介绍了NULL设计模式的DO's和Donts。

http://www.codeproject.com/Articles/1042674/NULL-Object-Design-Pattern


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接