不健康的代码:到处都是空值检查!识别问题解决方案:空对象模式如何保持代码健康保持联系导航你的软件开发职业通讯

2025-05-25

不健康的代码:到处都是空检查!

识别问题

一个解决方案:空对象模式

如何保持代码健康

保持联系

导航你的软件开发职业通讯


这是我的书《重构 TypeScript:保持代码健康》的摘录。

重构 TypeScript 书籍


识别问题

十亿美元的错误

您是否知道“零”概念的发明者将此称为他的“十亿美元的错误”!

尽管看起来很简单,但一旦进入更大的项目和代码库,您将不可避免地发现一些代码在使用空值时“失控”。

有时,我们希望使对象的某个属性成为可选的:



class Product{
  public id: number;
  public title: string;
  public description: string;
}


Enter fullscreen mode Exit fullscreen mode

在 TypeScript 中,string可以为属性分配值null

但是...number财产也可以!



const chocolate: Product = new Product();
chocolate.id = null;
chocolate.description = null;


Enter fullscreen mode Exit fullscreen mode

嗯……

另一个例子

乍一看,情况似乎还不算太糟。

但是,它可能会导致发生如下情况:



const chocolate: Product = new Product(null, null, null);


Enter fullscreen mode Exit fullscreen mode

这有什么问题?嗯,它会让你的代码(在本例中是Product类)陷入不一致的状态。

Product在你的系统中,如果不存在,那还有意义吗id?可能没有。

理想情况下,只要您创建它,Product它就应该有一个id

那么...在其他地方处理产品相关逻辑时会发生什么情况?

可悲的事实是:



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"
}


Enter fullscreen mode Exit fullscreen mode

这真的是有人会写的代码吗?

是的。

在我们研究一些修复技术之前,让我们先看看为什么这段代码不健康并被认为是“代码异味”。

真的那么糟糕吗?

这段代码很难阅读和理解。因此,修改后很容易出现错误。

我想我们都同意,将这样的代码分散在应用中并不理想。尤其是当这类代码位于应用的重要关键部分时!


关于 TypeScript 中非可空类型的附注

作为相关的附注,有人可能会提出 TypeScript 支持非可空类型

这允许您向编译选项添加一个特殊标志,并且默认情况下将阻止任何变量作为null值。

关于这个论点的几点:

  • 我们大多数人都在处理现有的代码库,这需要花费大量的工作和时间来修复这些编译错误。

  • 如果没有对代码进行良好的测试,并且小心避免假设,这些更改仍然可能会导致运行时错误。

  • 本文(摘自我的书)向您介绍了可以应用于其他语言的解决方案 - 这些语言可能没有此选项。


无论如何,对代码进行更小、更有针对性的改进总是更安全的。同样,这使我们能够确保系统仍然保持相同的行为,并避免在进行这些改进时引入大量风险。

一个解决方案:空对象模式

空集合

假设您在一家编写处理法律案件的软件的公司工作。

当你正在开发某个功能时,你发现了一些代码:



const legalCases: LegalCase[] = await fetchCasesFromAPI();
for (const legalCase of legalCases) {
    if(legalCase.documents != null) {
        uploadDocuments(legalCase.documents);
    }
}


Enter fullscreen mode Exit fullscreen mode

还记得我们应该警惕空值检查吗?如果代码的其他部分忘记检查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;
}


Enter fullscreen mode Exit fullscreen mode

我们不会保留空数组/集合null,而是为其分配一个实际的空数组。

现在,没有人需要进行空检查!

但是……整个法律案件集合本身怎么办?如果 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;
}


Enter fullscreen mode Exit fullscreen mode

凉爽的!

现在我们已经确保使用此方法的每个人都不必担心检查空值。

拍摄 2

由于强类型规则(即),其他语言(如 C#、Java 等)不允许您将空数组分配给集合[]

在这些情况下,您可以使用类似这个版本的空对象模式:



class EmptyArray<T> {
    static create<T>() {
        return new Array<T>()
    }
}

// Use it like this:
const myEmptyArray: string[] = EmptyArray.create<string>();


Enter fullscreen mode Exit fullscreen mode

那么对象呢​​?

想象一下你正在开发一款电子游戏。游戏中,有些关卡可能有一个boss。

当检查当前级别是否有老板时,您可能会看到类似这样的内容:



if(currentLevel.boss != null) {
    currentLevel.boss.fight(player);
}


Enter fullscreen mode Exit fullscreen mode

我们可能会发现其他地方也进行这种空检查:



if(currentLevel.boss != null) {
    currentLevel.completed = currentLevel.boss.isDead();
}


Enter fullscreen mode Exit fullscreen mode

如果我们引入一个空对象,那么我们就可以删除所有这些空检查。

首先,我们需要一个界面来代表我们的Boss



interface IBoss {
    fight(player: Player);
    isDead();
}


Enter fullscreen mode Exit fullscreen mode

然后,我们可以创建具体的老板类:



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.
    }
}


Enter fullscreen mode Exit fullscreen mode

接下来,我们将创建一个IBoss表示“null”的接口的实现Boss



class NullBoss implements IBoss {
    fight(player: Player) {
        // Player always wins.
    }
    isDead() {
        return true;
    }
}


Enter fullscreen mode Exit fullscreen mode

NullBoss将自动允许玩家“获胜”,并且我们可以删除所有空检查!

NullBoss在下面的代码示例中,如果boss是或的实例,Boss则无需进行额外的检查。



currentLevel.boss.fight(player);
currentLevel.completed = currentLevel.boss.isDead();


Enter fullscreen mode Exit fullscreen mode

注意:本书的这一部分包含更多攻击这种代码异味的技术!

如何保持代码健康

这篇文章摘自《重构 TypeScript》,它旨在成为一种平易近人且实用的工具,帮助开发人员更好地构建高质量的软件。

重构 TypeScript 书籍

保持联系

不要忘记通过以下方式与我联系:

导航你的软件开发职业通讯

一封电子邮件简报,助您提升软件开发职业水平!您是否想过:

✔ 软件开发人员通常经历哪些阶段?
✔ 我如何知道自己处于哪个阶段?如何进入下一个阶段?
✔ 什么是技术领导者?如何成为技术领导者?
✔ 有人愿意陪伴我并解答我的疑问吗?

听起来很有趣?加入社区吧!

文章来源:https://dev.to/jamesmh/unhealthy-code-null-checks-everywhere-2720
PREV
使用 Framer Motion 实现 Next.js 页面过渡动画
NEXT
软件开发职业生涯的几个阶段 重要考虑因素 1:程序员 2:初级开发人员 3:中级开发人员 4:高级开发人员 5:首席开发人员 6:技术负责人 全部完成! 保持联系 导航您的软件开发职业生涯 时事通讯