不健康的代码:到处都是空检查!
识别问题
一个解决方案:空对象模式
如何保持代码健康
保持联系
导航你的软件开发职业通讯
这是我的书《重构 TypeScript:保持代码健康》的摘录。
识别问题
十亿美元的错误
您是否知道“零”概念的发明者将此称为他的“十亿美元的错误”!
尽管看起来很简单,但一旦进入更大的项目和代码库,您将不可避免地发现一些代码在使用空值时“失控”。
有时,我们希望使对象的某个属性成为可选的:
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
。
那么...在其他地方处理产品相关逻辑时会发生什么情况?
可悲的事实是:
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"
}
这真的是有人会写的代码吗?
是的。
在我们研究一些修复技术之前,让我们先看看为什么这段代码不健康并被认为是“代码异味”。
真的那么糟糕吗?
这段代码很难阅读和理解。因此,修改后很容易出现错误。
我想我们都同意,将这样的代码分散在应用中并不理想。尤其是当这类代码位于应用的重要关键部分时!
关于 TypeScript 中非可空类型的附注
作为相关的附注,有人可能会提出 TypeScript 支持非可空类型。
这允许您向编译选项添加一个特殊标志,并且默认情况下将阻止任何变量作为null
值。
关于这个论点的几点:
-
我们大多数人都在处理现有的代码库,这需要花费大量的工作和时间来修复这些编译错误。
-
如果没有对代码进行良好的测试,并且小心避免假设,这些更改仍然可能会导致运行时错误。
-
本文(摘自我的书)向您介绍了可以应用于其他语言的解决方案 - 这些语言可能没有此选项。
无论如何,对代码进行更小、更有针对性的改进总是更安全的。同样,这使我们能够确保系统仍然保持相同的行为,并避免在进行这些改进时引入大量风险。
一个解决方案:空对象模式
空集合
假设您在一家编写处理法律案件的软件的公司工作。
当你正在开发某个功能时,你发现了一些代码:
const legalCases: LegalCase[] = await fetchCasesFromAPI();
for (const legalCase of legalCases) {
if(legalCase.documents != null) {
uploadDocuments(legalCase.documents);
}
}
还记得我们应该警惕空值检查吗?如果代码的其他部分忘记检查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
,而是为其分配一个实际的空数组。
现在,没有人需要进行空检查!
但是……整个法律案件集合本身怎么办?如果 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;
}
凉爽的!
现在我们已经确保使用此方法的每个人都不必担心检查空值。
拍摄 2
由于强类型规则(即),其他语言(如 C#、Java 等)不允许您将空数组分配给集合[]
。
在这些情况下,您可以使用类似这个版本的空对象模式:
class EmptyArray<T> {
static create<T>() {
return new Array<T>()
}
}
// Use it like this:
const myEmptyArray: string[] = EmptyArray.create<string>();
那么对象呢?
想象一下你正在开发一款电子游戏。游戏中,有些关卡可能有一个boss。
当检查当前级别是否有老板时,您可能会看到类似这样的内容:
if(currentLevel.boss != null) {
currentLevel.boss.fight(player);
}
我们可能会发现其他地方也进行这种空检查:
if(currentLevel.boss != null) {
currentLevel.completed = currentLevel.boss.isDead();
}
如果我们引入一个空对象,那么我们就可以删除所有这些空检查。
首先,我们需要一个界面来代表我们的Boss
:
interface IBoss {
fight(player: Player);
isDead();
}
然后,我们可以创建具体的老板类:
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
将自动允许玩家“获胜”,并且我们可以删除所有空检查!
NullBoss
在下面的代码示例中,如果boss是或的实例,Boss
则无需进行额外的检查。
currentLevel.boss.fight(player);
currentLevel.completed = currentLevel.boss.isDead();
注意:本书的这一部分包含更多攻击这种代码异味的技术!
如何保持代码健康
这篇文章摘自《重构 TypeScript》,它旨在成为一种平易近人且实用的工具,帮助开发人员更好地构建高质量的软件。
保持联系
不要忘记通过以下方式与我联系:
导航你的软件开发职业通讯
一封电子邮件简报,助您提升软件开发职业水平!您是否想过:
✔ 软件开发人员通常经历哪些阶段?
✔ 我如何知道自己处于哪个阶段?如何进入下一个阶段?
✔ 什么是技术领导者?如何成为技术领导者?
✔ 有人愿意陪伴我并解答我的疑问吗?
听起来很有趣?加入社区吧!
文章来源:https://dev.to/jamesmh/unhealthy-code-null-checks-everywhere-2720