写点什么

Null:价值 10 亿美元的错误

  • 2020-06-25
  • 本文字数:3957 字

    阅读完需:约 13 分钟

Null:价值10亿美元的错误


本文翻译自“NULL: The Billion Dollar Mistake”,已获得原作者 Maximiliano Contieri 授权。


我们都在大量地使用 null。我们必须承认它舒适、高效、快速,然而我们在使用它的过程中遇到了数不清的问题。既然认识到了问题的存在,那么还有什么认知偏见在阻止我们着手解决它呢?

Null 代表什么?

Null 只是一个标志。根据使用和调用它的上下文不同,它代表着不同的情况。


这就导致了软件开发中最严重的错误:在一个对象使用它的人之间的约定中耦合了一个隐藏的决策。


耦合:唯一的软件设计问题


有了这个重要的缺陷似乎还不够,它还要打破我们唯一的设计规则:它使用同一实体表示域的多个元素,因此不得不在各种不同的上下文环境下做出不同的解读。


唯一的软件设计原则


一个好的软件原则要求我们具有高度的内聚性。所有对象应尽可能具体,并只具有单一责任(SOLID 中的 S)。


任何系统中最没有内聚性的对象就是通配符:Null



NULL 可以映射到现实世界中的几个不同概念

灾难性故障

对于任何对象,Null 都不是多态的,因此调用它的任何函数都将中断后续的调用链。


例 1:让我们用一个模型来模拟当前 covid-19 大流行期间人与人之间的互动。


final class City {    public function interactionBetween($somePerson, $anotherPerson) {        if ($this->meetingProbability() < random()) {                return null; //no interaction            } else {                return new PersonToPersonInteraction($somePerson, $anotherPerson);        }    }}final class PersonToPersonInteraction {    public function propagate($aVirus) {            if ($this->somePerson->isInfectedWith($aVirus) && $aVirus->infectionProbability() > random()) {                $this->anotherPerson->getInfectedWith($aVirus);        }    }}$covid19 = new Virus();    $janeDoe = new Person();$johnSmith = new Person();    $wuhan = new City();$interaction = $wuhan->interactionBetween($johnSmith, $janeDoe);if ($interaction != null) {        $interaction->propagate($covid19);}/* In this example we modeled the interaction between an infected person and a healthy one.Jane is healthy but might be infected if Virus R0 applies to her.*/
复制代码


我们可以看到两个 Null 标志和相应的 if 子句。看起来 Null 传播似乎被控制住了,但这只是一个假象。

一点历史

Null 的诞生源于 1965 年的一个偶然事件。


托尼·霍尔(Tony Hoare)是快速排序算法的创造者,也是图灵奖(计算机领域的诺贝尔奖)的获得者。他把它添加到了 Algol 语言中,因为它看起来很实用而且容易实现。但几十年后,他后悔了。


下面这篇很棒的文章里详细地讲述了这个故事:


空指针引用:价值十亿美元的错误


我称之为我的十亿美元错误……当时,我正在设计第一个全面的类型系统,用于面向对象语言中的引用功能。我的目标是确保所有对引用的使用都是绝对安全的,由编译器自动执行检查。

但是我无法拒绝定义一个 Null 引用的诱惑,因为它实在太容易实现了。这导致了无数错误、漏洞和系统崩溃。在过去的四十年里,这些问题可能已经造成了十亿美元的损失。 

——托尼·霍尔,ALGOL W 的发明者。


读者也可以观看完整的视频

借口

作为开发人员,我们使用 Null 是因为它很容易用(很容易编写),并且我们相信它提高了我们软件的效率。


这是错误的观念,因为我们忽略了一件事实:一份代码可能会被读十次甚至更多,而你只需要写它一次即可。


读带有 Null 的代码特别困难。因此,我们只是把问题推给将来去解决了。


效率是一个理由(这是产生耦合最常用的借口)。除非是在非常特殊和关键的情况下,它的性能损失都可以忽略不计。在那些将效率的重要性置于可读性、适应性和可维护性之上的系统中,它的存在是合理的(在质量指标方面总是存在各种权衡)。


不管时代怎样发展,这种认知偏差总会持续存在。尽管现代虚拟机是会为我们优化代码的。


为了依靠证据而不是直觉,我们只需要开始基准测试即可,而不是坚持错误地宣称效率比可读性更重要。


快速失败

Null 被人们用来(或者说是被滥用于)掩盖意外情况,让代码中的错误散播得太过遥远,进而产生令人恐惧的连锁反应。


好的设计原则之一是快速失败(fail fast)。


下面再看一个例子:假设有这样一张病人的数据表,我们要填写病人的出生日期。


如果可视化组件和对象创建过程中存在错误,数据表中就可能出现空的(null)出生日期。


当运行某个夜间批处理程序,要收集所有患者的生日以计算平均年龄的时候,这个出生日期为空的病人数据就会生成错误。


为开发人员提供有价值信息的堆栈会离缺陷所在的位置有很远的距离。接下来只能祝你调试愉快了!



除此之外,还可能会有用不同的编程语言编写的不同系统,通过 API 来传输数据、文件等。


开发人员最糟糕的噩梦是必须在深夜调试这个 bug,并试图找出问题的根本原因。

不处理可选内容的类型化语言

大多数类型化语言使用了相同的方法来防止错误,即确保作为参数发送(或返回)的对象能够符合某个协议。


不幸的是,这些语言中有一些已经开始倒退,既允许声明的对象是某种类型的,也可以选择是 Null。


这打破了调用链,迫使开发人员使用 If 来处理对象不存在的场景,违反了开/闭原则


更重要的是,Null 会破坏类型控制。如果我们使用类型化语言并信任编译器防御网络,Null 会像病毒一样穿透它,并传播到下面指出的其他类型。


计算机科学发展史上最糟糕的错误。——Lucidchart

解决方案

不要用它。

替代方案

像往常一样,为了解决我们所有的问题,我们应该忠于我们认定的唯一公理化的设计规则。


在问题域中搜索解决方案,将它们带到我们的模型中。

模型多态缺失

在上述情况下,在必须为对象声明一个类型时,还有一些更优雅的解决方案可以避免使用 if 来处理可选的内容。


在分类语言中,在具体的兄弟类中使用NullObject设计模式,并根据Liskov替换原则(SOLID 的 L)将超类声明为协作者的类型就足够了。


但是,如果我们决定实施这一解决方案,我们将违反另一个设计原则,即我们应该有充分的理由进行子类化,避免重用代码或调整类层次结构。


分类语言中的最佳解决方案是声明一个接口,真实类空对象类都必须遵守该接口的约定。


在第一个示例中这样做:


Interface SocialInteraction{    public function propagate($aVirus);}final class SocialDistancing implements SocialInteraction {    public function propagate($aVirus) { //Do nothing !!!!    }}final class PersonToPersonInteraction implements SocialInteraction {    public function propagate($aVirus) {            if ($this->somePerson->isInfectedWith($aVirus) && $aVirus->infectionProbability() > random()) {                $this->anotherPerson->getInfectedWith($aVirus);        }    }}final class City {    public function interactionBetween($aPerson, $anotherPerson) {        return new SocialDistancing(); // The cities are smart enough to implement social distancing to model Person to Person interactions    }}$covid19 = new Virus();    $janeDoe = new Person();$johnSmith = new Person();    $wuhan = new City();$interaction = $wuhan->interactionBetween($johnSmith, $janeDoe);$interaction->propagate($covid19);/* Jane will not be affected since the interaction prevents from propagating the virus
复制代码


不会引入病毒,也不会有 if 或 Null!


在本例中,我们将 Null 替换为问题域中存在的一种描述。

再看看病人出生日期的例子

我们回到患者表单的例子。即使表格中相应的字段有缺失,我们仍要计算出平均数来。


Interface Visitable {    public function accept($aVisitor);}final class Date implements Visitable {    public function accept($aVisitor) {            $aVisitor->visitDate($this);    }}final class DateNotPresent implements Visitable {    public function accept($aVisitor) {            $aVisitor->visitDateNotPresent($this);    }}final class AverageCalculator {    private $count = 0;        private $ageSum = 0;    public function visitDate($aDate) {            $this->count++;            $this->ageSum += today() - $aDate;    }    public function visitDateNotPresent($aDate) {    }    public function average() {        if ($this->count == 0)                return 0;            else                return $this->ageSum / $this->count;    }}function averagePatientsAge($patients) {        $calculator = new AverageCalculator();        foreach ($patients as $patient)            $patient->birthDate()->accept($calculator);        return $calculator->average();}
复制代码


我们使用Visitor模式来处理这种可以有空对象行为的对象。


没有 Null


此外,我们使用多态性去掉了不必要的 if,并通过开/闭原则将解决方案留给了其他非 average 的 calculation 来处理。


我们没有使用太多算法,但构建了一个但更具声明性、可维护性和可扩展性的解决方案。

使用显式支持不存在类型的语言

有些语言支持可选的 Maybe/Optional 概念,这是在语言级别实现上述解决方案的一个特殊情况。

结论

在我们的行业中,根据很多影响深远的实践得出的经验,使用 Null 是不可取的做法。尽管如此,几乎所有的商业语言都支持它,开发人员也都在使用它。


我们至少应该开始质疑它的使用,并在开发软件时更加成熟和负责。


本系列文章的一个目标是开拓关于软件设计的辩论和讨论的空间。我们期待着看到对这篇文章的评论。


英文原文:NULL: The Billion Dollar Mistake


2020-06-25 10:003170
用户头像

发布了 152 篇内容, 共 71.6 次阅读, 收获喜欢 64 次。

关注

评论

发布
暂无评论
发现更多内容

60个令人兴奋的ThreeJS网站示例

3D建模设计

three.js

中国传统文化国际交流大会暨《传承进行时》非遗晚会启动

Geek_2d6073

一道经典面试题:@Configuration 和 @Component 有何区别?

江南一点雨

Java spring

软件测试/测试开发丨Selenium 高级定位 Xpath

测试人

程序员 软件测试 测试开发 selenium xpath

活动回顾丨阿里云 Serverless 技术实践营 Serverless+AI 专场

阿里巴巴云原生

阿里云 Serverless AI 云原生

WebGpu VS WebGL

3D建模设计

WebGL webgpu

19 个最佳Three.JS 示例

3D建模设计

ThreeJS

3种OC渲染透明背景的方法

Finovy Cloud

学习 自学 渲染 建模 渲染器

802.11be-IPQ9574+IPQ9554 802.11ax-IPQ8074+IPQ8072 Support 8 data streams in 4 frequency bands, with a peak rate of 16Gbps

wifi6-yiyi

802.11AX 802.11be

成品直播源码平台消息传递的协议:MMS协议

山东布谷科技

软件开发 消息传递 成品直播源码 MMS 多媒体短信协议

免费共享大文件的10种方法,分享大文件传输的解决方案

镭速

基于深度学习的机器视觉表计识别

鲸品堂

人工智能 深度学习 数据 企业号 8 月 PK 榜

GameFi模式链游搭建,养成、PK类链游系统开发

V\TG【ch3nguang】

区块链游戏 gamefi

生成式AI引领数据中心网络升级

百度开发者中心

#人工智能 ChatGPT 文心一言

NFT艺术品交易平台开发搭建

V\TG【ch3nguang】

NFT数字藏品系统

公链系统搭建技术方案,公链系统应用开发

V\TG【ch3nguang】

公链 区块链系统开发

NineData X SelectDB 联合发布会,8月30日即将上线!

NineData

实时数仓 数据复制 SelectDB 产品架构 NineData

AI区块链量化交易平台搭建开发

V\TG【ch3nguang】

量化交易系统开发

如何实现AI的矢量数据库

3D建模设计

人工智能 机器学习 AI

如何把一个接口设计好? | 京东云技术团队

京东科技开发者

接口 接口设计 企业号 8 月 PK 榜

区块链挖矿APP软件开发,质押流动性矿池模式系统搭建

V\TG【ch3nguang】

挖矿系统开发

redis分布式锁,setnx+lua脚本的java实现 | 京东物流技术团队

京东科技开发者

redis setnx Lua脚本 redis 底层原理 企业号 8 月 PK 榜

活动回顾丨云原生技术实践营长沙站(含 PPT)

阿里巴巴云原生

阿里云 云原生

5 种 可帮助开发人员提高工作效率的AI 工具

3D建模设计

人工智能

架构师日记-软件工程里的组织文化 | 京东云技术团队

京东科技开发者

架构 软件工程 自动化测试 企业号 8 月 PK 榜 组织文化

Office 2019正式版 办公软件套装Office 2019 mac激活版

mac

苹果mac Windows软件下载 office 2019 办公软件套装

区块链会员系统搭建,复利理财拆分模式开发部署

V\TG【ch3nguang】

理财系统 区块链系统开发 复利

撮合前端平台在低代码平台的落地实践 | 京东云技术团队

京东科技开发者

前端 中台架构 低代码平台 企业号 8 月 PK 榜 撮合

亿级月活的社交APP,陌陌如何做到3分钟定位故障?

童子龙

微服务 性能分析 链路跟踪 可观测平台

Null:价值10亿美元的错误_文化 & 方法_Maximiliano Contieri_InfoQ精选文章