“Null 是价值十亿美元的错误!”
“不健康”的代码到处都是Null检查!作者 |James Hickey译者 | 弯月,责编 | 郭芮出品 | CSDN(ID:CSDNnews)以下为译文:你是否知道提出“null”这个概念的人声称这是他的“十亿美元错误!”这个问题看似很简单,然而一旦你深入大型项目和代码库,就必然会发现有些代码中null的使用简直匪夷所思。有时,我们希望使对象的属性为可选:...
“不健康”的代码到处都是Null检查!
作者 | James Hickey
译者 | 弯月,责编 | 郭芮
出品 | CSDN(ID:CSDNnews)
以下为译文:
你是否知道提出“null”这个概念的人声称这是他的“十亿美元错误!”
这个问题看似很简单,然而一旦你深入大型项目和代码库,就必然会发现有些代码中null的使用简直匪夷所思。
有时,我们希望使对象的属性为可选:
class Product{
public id: number;
public title: string;
public description: string;
}
在TypeScript中,string类型的属性值可以为null。
但是,number属性居然也可以!
const chocolate: Product = new Product();
chocolate.id = null;
chocolate.description = null;
呃……
再举个例子
乍一看去,似乎并没有什么不妥。
但是,这可能会引发如下情况:
const chocolate: Product = new Product(null, null, null);
这段代码有什么问题?它会让你的代码(上述代码中的Product类)陷入不一致的状态。
系统中的Product没有id,这合理吗?应该不合理吧。
理想情况下,一旦创建Product,它就应该有id。
那么,在需要与Product打交道的其他地方会怎么样呢?
实际情况惨不忍睹:
let title: string;
if(product != null) {
if(product.id != null) {
if(product.title != null) {
title = product.title;
} else {
title = "N/A";
}
} else {
title = "N/A"
}
} else {
title = "N/A"
}
真的有人写这样的代码?
有!
在介绍如何修改上述代码之前,首先让我们来看一看为什么这段代码不健康,然后再考虑“代码的味道”。
这段代码很糟糕吗?
这段代码非常难以阅读和理解。因此,在修改这段代码时很容易出bug。
应用中塞满这样的代码可不是什么好事,你也同意吧。尤其是当在应用程序的重要以及关键部分出现这样的代码!
关于TypeScript中的不可为null类型
有人可能会想到,TypeScript支持不可为null的类型。
你可以在编译选项中添加一个特殊的标志,该标志默认情况下会禁止给任何变量赋值null。
关于这个观点我还有几点想说:
-
我们大多数人都需要处理现有的代码库,如果想修复这些编译错误就需要付出大量的时间和精力。
-
如果不进行严格的测试且小心地避免作出任何假设,那么这些修改仍可能造成运行时错误。
-
本文中谈到的解决方案也同样适用于其他语言,即便这些语言可能没有这样的选项。
无论怎样,安全的做法还是更有针对性地改进代码。同样,这可以确保系统的行为不变,而且还可以避免在修改的时候引入大量风险。
一种解决方案:Null对象模式
空集合
想象一下,你在一家为处理法律案件编写软件的公司工作。
有一天,在开发某项功能时,你看到了如下代码:
const legalCases: LegalCase[] = await fetchCasesFromAPI();
for (const legalCase of legalCases) {
if(legalCase.documents != null) {
uploadDocuments(legalCase.documents);
}
}
记得我们应该谨慎使用null检查吗?如果代码的某个地方忘记了检查null数组,就会造成大问题。
Null对象模式可以帮助你:你可以创建一个代表“空”的对象或null对象。
修改代码
让我们来看看fetchCasesFromAPI()方法。我们可以使用这种模式,而且在JavaScript和TypeScript 中处理数组时,这种做法很普遍。
const fetchCasesFromAPI = async function() {
const legalCases: LegalCase[] = await $http.get('legal-cases/');
for (const legalCase of legalCases) {
// Null Object Pattern
legalCase.documents = legalCase.documents || [];
}
return legalCases;
}
为了避免空数组/集合的值为null,我们将一个空数组赋给它。
这样一来,别处就不需要检查null了!
但是,整个legalCase怎么办呢?如果这个API返回null,该如何是好?
const fetchCasesFromAPI = async function() {
const legalCasesFromAPI: LegalCase[] = await $http.get('legal-cases/');
// Null Object Pattern
const legalCases = legalCasesFromAPI || [];
for (const case of legalCases) {
// Null Object Pattern
case.documents = case.documents || [];
}
return legalCases;
}
搞定!
现在我们可以确保每个使用该方法的人都无需再担心null检查了!
示例2
在C#、Java等其他语言中,由于强类型的规定,你不能将一个空数组(即[])赋给集合。
这时候,你可以使用Null对象模式:
class EmptyArray<T> {
static create<T>() {
return new Array<T>()
}
}
// Use it like this:
const myEmptyArray: string[] = EmptyArray.create<T>();
对象又如何呢?
想象一下,你正在开发一款视频游戏,你在某些级别设定了大boss。
在检查该级别是否有大boss的时候,你可能会遇到如下代码:
if(currentLevel.boss != null) {
currentLevel.boss.fight(player);
}
我们可能会发现其他地方也有这类的null检查:
if(currentLevel.boss != null) {
currentLevel.completed = currentLevel.boss.isDead();
}
我们只需引入一个null对象,就可以省却这些所有的null检查。
首先,我们需要一个表示大boss的接口:
interface IBoss {
fight(player: Player);
isDead();
}
接下来,我们可以创建一个boss类:
class Boss implements IBoss {
fight(player: Player) {
// Do some logic and return a bool.
}
isDead() {
// Return whether boss is dead depending on how the fight went.
}
}
然后,我们创建IBoss接口(代表一个“null” boss)的实现:
class NullBoss implements IBoss {
fight(player: Player) {
// Player always wins.
}
isDead() {
return true;
}
}
这个NullBoss会自动让玩家“通关”,所以我们就可以删除所有的null检查了!
在如下代码示例中,如果boss是NullBoss或Boss的实例,也不需要额外的检查。
currentLevel.boss.fight(player);
currentLevel.completed = currentLevel.boss.isDead();
如何保证代码的健康?可以参考《Refactoring TypeScript 》一书中的部分内容,这本书旨在通过易于入手且实用的工具,帮助开发人员更好地构建高质量的软件。
原文:https://dev.to/jamesmh/unhealthy-code-null-checks-everywhere-2720
本文为 CSDN 翻译,转载请注明来源出处。
【END】
如何在短时间内成为Python工程师?
热 文 推 荐
☞华为发布麒麟 990 芯片;苹果召回部分电源插头转换器;KDevelop 5.4.2 发布 | 极客头条
☞2亿日活,日均千万级视频上传,快手推荐系统如何应对技术挑战?
点击阅读原文,输入关键词,即可搜索您想要的 CSDN 文章。
你点的每个“在看”,我都认真当成了喜欢
更多推荐
所有评论(0)