Why NULL is Bad?

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

在Java中,NULL的一个简单示例是:

这个方法有什么问题?

它可能返回NULL而不是一个对象——这就是问题所在。在面向对象的范式中,NULL是一种糟糕的做法,应该尽量避免使用。已经有很多关于这个问题的观点发表过,包括Tony Hoare的Null References, The Billion Dollar Mistake演讲以及David West的整本Object Thinking书。

在这里,我将试图总结所有的论点,并展示如何避免使用NULL并替换为适当的面向对象构造。

基本上,有两种可能的替代方案来替代NULL

第一种是Null Object设计模式(最好将其设置为常量):

第二种可能的替代方案是在无法返回对象时通过抛出异常来快速失败。

现在,让我们来看看反对NULL的论点。

除了上面提到的Tony Hoare的演讲和David West的书之外,在撰写这篇文章之前,我阅读了以下出版物:《代码整洁之道》(Robert Martin著)、《代码大全》(Steve McConnell著)、《Say “No” to “Null”》(John Sonmez著)、Stack Overflow上的《返回null是否是糟糕的设计?》讨论。

每次您将对象作为输入时,您必须检查它是否为NULL或有效的对象引用。如果您忘记进行检查,可能会在运行时发生NullPointerException(NPE)导致执行中断。因此,您的逻辑会被多个检查和if/then/else分支所污染。

这是在C和其他命令式过程化语言中处理异常情况的方式。面向对象编程引入了异常处理,主要是为了摆脱这些临时错误处理块。在面向对象编程中,我们让异常一直向上冒泡,直到达到应用程序范围的错误处理程序,使我们的代码更加清晰和简短。

NULL引用视为过程化编程的继承,并使用1)Null对象或2)异常来替代。

为了明确传达其含义,函数getByName()必须被命名为getByNameOrNullIfNotFound()。对于每个返回对象或NULL的函数,都应该采取相同的做法。否则,代码读者将不可避免地面临歧义。因此,为了保持语义的明确性,您应该给函数起一个更长的名字。

为了消除这种歧义,总是返回一个真实的对象、一个空对象或抛出一个异常。

有人可能会争辩说,有时我们必须返回NULL,以提高性能。例如,Java中接口Map的方法get()在地图中没有这样的项时返回NULL

这段代码只搜索一次地图,这是由于在Map中使用了NULL。如果我们重构Map,使其方法get()在找不到任何内容时抛出异常,那么我们的代码将如下所示:

显然,这种方法比第一种方法慢两倍。该怎么办?

Map 接口(请原谅其作者)存在一个设计缺陷。它的 get() 方法应该返回一个 Iterator,这样我们的代码看起来就像:

顺便说一句,这正是 C++ STL map::find() 方法的设计方式。

如果懂得在Java中,对象是指向数据结构的指针,而NULL是指向空(在Intel x86处理器中为0x00000000)的指针的人,就能理解语句if (employee == null)的含义。

然而,如果从对象的角度开始思考,这个语句就变得不那么有意义了。以下是我们从对象的角度看待代码的样子:

这次谈话的最后一个问题听起来很奇怪,对吗?

相反,如果他们在我们要求和杰弗里通话后挂断电话,这会给我们带来麻烦(异常)。在那时,我们会尝试再次打电话或告知主管我们无法联系到杰弗里并完成更重要的交易。

或者,他们可能让我们和另一个人交谈,这个人不是杰弗里,但可以回答我们大部分的问题,或者如果我们需要某些“杰弗里特定”的帮助时拒绝协助(空对象)。

与其快速失败,上述代码试图缓慢死亡,并在其途中夺走他人的生命。与让每个人都知道出了问题并应立即开始异常处理不同,它将这个失败隐藏起来,不让其客户端察觉。

这个论点与上述讨论的“特殊错误处理”相近。

将代码设计得尽可能脆弱,当需要时让其崩溃是一种良好的实践。

让你的方法对其操作的数据要求极高。如果提供的数据不足或与方法的主要使用场景不匹配,让它们通过抛出异常来抱怨。

否则,返回一个空对象,该对象公开一些常见的行为,并在其他所有调用上抛出异常:

Mutable and Incomplete Objects

通常情况下,强烈建议在设计对象时考虑其不可变性。这意味着一个对象在实例化时获取所有必要的知识,并在其整个生命周期中不会改变其状态。

很常见地,在延迟加载中使用NULL值来使对象不完整且可变。例如:

这项技术虽然被广泛使用,但在面向对象编程中却是一种反模式。主要原因是它使得对象对计算平台的性能问题负责,而这是一个“员工”对象不应意识到的。

一个对象不应该管理状态并暴露其与业务相关的行为,而应该负责自己结果的缓存——这就是延迟加载的含义。

缓存不是员工在办公室里做的事情,对吗?

解决方案呢?不要像上面的例子那样以这种原始的方式使用延迟加载。相反,将这个缓存问题移到应用程序的另一层。

例如,在Java中,你可以使用面向方面的编程。例如,jcabi-aspects有一个@Cacheable注解,用于缓存方法返回的值:

我希望这个分析能够令你信服,从而停止对代码使用NULL

附:如果你对更科学的反对NULL的论点感兴趣,可以阅读这篇最近发表的研究论文,该论文通过实证研究表明NULL使用的强度与认知复杂性之间存在相关性。当一个类中出现更多的NULL时,其方法的复杂性就越高。

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-12-15 at 07:12

sixnines availability badge   GitHub stars