阿里云「飞天发布时刻」2024来啦!新产品、新特性、新能力、新方案,等你来探~ 了解详情
写点什么

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:002842
用户头像

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

关注

评论

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

还在分不清各种IO模型?

Java你猿哥

Java Linux io I/O模型 Java IO

软件测试丨Pytest学习笔记-Mark标记、Skip跳过、xFail预期失败

测试人

软件测试 自动化测试 测试开发 pytest

如何在 Mac 上查看显示刷新率

互联网搬砖工作者

用scrum敏捷工具做敏捷需求管理

顿顿顿

Scrum 敏捷开发 敏捷项目管理 看板工具

阿里内部教材:15W字的SpringCloud微服务实战,我一口气给你打包

做梦都在改BUG

Java 架构 微服务 Spring Cloud

intellij idea 粘贴字符串如何不带换行符?

源字节1号

微信小程序 开源 软件开发 前端开发 后端开发

GitHub发布即百万!字节内网超实用 java性能优化手册,star超十万

Java你猿哥

Java JVM ssm Java性能调优 java程序优化

今年互联网是“银二铜三铁四丐五”?程序员面试如何破局?

Java你猿哥

Java 架构 算法 多线程 java面试

MoE 系列(三)|使用 Istio 动态更新 Go 扩展配置

SOFAStack

golang 程序员 开发 java; envoy

小程序的安全架构解析

Onegun

小程序 安全 小程序容器 小程序架构

哪些技术有可能坐上智慧城市发展的快车

Onegun

智慧城市 小程序容器 数字城市

全新问世!阿里内藏版的SpringBoot 2.5实战笔记,全面覆盖新特性

做梦都在改BUG

Java spring 微服务 Spring Boot 框架

大开眼界!Jenkins结合SpringCloud+K8S,打通微服一条龙技术讲解

做梦都在改BUG

Java Kubernetes k8s Spring Cloud jenkins

【活动报名】PolarDB开源数据库&PG中文社区技术沙龙温州站

阿里云数据库开源

数据库 postgresql 阿里云 开源 polarDB

头部企业走入无人区,国产数智化厂商挑大梁

用友BIP

Last Week in Milvus

Zilliz

Milvus Zilliz 向量数据库

如何利用java实现一个布隆过滤器?

做梦都在改BUG

Java 布隆过滤器

Spring Boot如何优雅提高接口数据安全性

做梦都在改BUG

Java spring Spring Boot

网易云信 RTC 音频问题排查的挑战与实践

网易云信

RTC 实时音视频 AIGC

WebSocket与Socket、TCP、HTTP的区别

Apifox

程序员 TCP socket HTTP websocket

MLCC龙头涨价;车厂砍单芯片;台积电28nm设备订单全部取消!

元器件秋姐

芯片 汽车 资讯 新能源 MLCC

兼顾可扩展、高并发与数据一致性:咸鱼优惠系统设计实践

Java你猿哥

Java 架构 ssm 架构设计 并发

NFTScan 开发者平台发布 CU 付费方案,为 Web3 初创团队提供多链 NFT API 服务

NFT Research

NFT #Web3

德邦快递:逆境之下,让数字化辅助业务的利润增长

Kyligence

数据分析 数字化管理 指标中台

从GitHub火到头条!共计1658页的Java岗面试核心MCA版,拿走不谢

做梦都在改BUG

Java java面试 Java八股文 Java面试题 Java面试八股文

做客《创新之路》,Tapdata 创始人唐建法对话央视著名主持人李雨霏,畅聊创业故事

tapdata

翻遍GitHub帮你总结了一份并发图册+高并发笔记,一次性搞懂并发编程

小小怪下士

Java 程序员 后端 高并发 并发

Go_Gin之初体验

神木鼎

golang 日更 gin框架

跨平台的Markdown编辑器:Typora for Mac中文版

真大的脸盆

用户费力度建设初探

Qunar技术沙龙

去哪儿网 用户费力度

【网易云信】网易云信 RTC 音频问题排查的挑战与实践

网易智企

RTC 实时音视频 AIGC

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