JavaScript is required
Blog About

【译】自注释代码

2025/05/30
6 mins read
See this issue
# 代码规范
Back

回想一下你上次看到一段陌生代码的时候。你是否能立刻明白它在做什么?如果不能,那你并不孤单——包括我在内的许多软件开发人员都发现,快速理解陌生代码颇具挑战。

让我们来看一个创建用户账户的简单 JavaScript 函数:

async function createUser(user) {
    if (!validateUserInput(user)) {
        throw new Error('u105');
    }

    const rules = [/[a-z]{1,}/, /[A-Z]{1,}/, /[0-9]{1,}/, /\W{1,}/];
    if (user.password.length >= 8 && rules.every((rule) => rule.test(user.password))) {
        if (await userService.getUserByEmail(user.email)) {
            throw new Error('u212');
        }
    } else {
        throw new Error('u201');
    }

    user.password = await hashPassword(user.password);
    return userService.create(user);
}

乍一看,除了使用了一些晦涩难懂的错误消息代码外,这个函数看起来还不算太糟糕。参数 user 显然是一个包含待创建用户信息的对象。有几行代码使用正则表达式检查密码是否符合密码策略。然后,会检查用户账户是否已经存在。最后,如果所有检查都通过,用户的密码会被哈希处理,并调用一个创建新用户的函数;成功时可能会返回一些数据。

与其他代码相比,这个例子不算差,但仍有很大的改进空间。

“计算机科学中只有两件难事:缓存失效和命名。” —— 菲尔·卡尔顿

软件开发人员经常处理抽象概念和复杂系统。将这些抽象概念转化为能够准确反映其行为的具体、有意义的名称并非易事。然而,当处理像我们示例中这样大家都熟悉的用户账户创建过程时,这并不能成为借口。

# 命名常量与单一职责原则

我要做的第一个改进是使用命名常量代替晦涩的错误代码。此外,将复杂的逻辑(如密码检查)放在单独的函数中,会使代码更易于阅读。毕竟,一个函数理想情况下应该只做一件事。将密码检查单独实现,也可以让其他函数调用它。

const err = {
    userValidationFailed: 'u105',
    userExists: 'u212',
    invalidPassword: 'u201',
};

function isPasswordValid(password) {
    const rules = [/[a-z]{1,}/, /[A-Z]{1,}/, /[0-9]{1,}/, /\W{1,}/];
    return password.length >= 8 && rules.every((rule) => rule.test(password));
}

经过这些更改后,createUser 函数应该如下所示。现在,我们可以立即知道如果检查失败会发生什么。

async function createUser(user) {
    if (!validateUserInput(user)) {
        throw new Error(err.userValidationFailed);
    }

    if (isPasswordValid(user.password)) {
        if (await userService.getUserByEmail(user.email)) {
            throw new Error(err.userExists);
        }
    } else {
        throw new Error(err.invalidPassword);
    }

    user.password = await hashPassword(user.password);
    return userService.create(user);
}

# 短路求值

上面修改后的代码现在很容易阅读,我们可以保持原样。不过,在这种情况下,我们有机会通过使用短路求值使代码流程更加线性。

短路求值允许我们使用逻辑运算符简化条件语句。在这种情况下,|| 运算符会检查左边的条件,如果为假,则执行右边的函数。

我们还将密码验证和用户存在性检查进行了扁平化处理。最终的代码更短,且没有嵌套逻辑。

function throwError(error) {
    throw new Error(error);
}

async function createUser(user) {
    validateUserInput(user) || throwError(err.userValidationFailed);
    isPasswordValid(user.password) || throwError(err.invalidPassword);
    !(await userService.getUserByEmail(user.email)) || throwError(err.userExists);

    user.password = await hashPassword(user.password);
    return userService.create(user);
}

# 类型注解

自我文档化代码意味着编写的代码注释最少,但有一种注释不仅对开发人员有用,对编译器和集成开发环境(IDE)也有用,那就是关于变量和参数类型的注解。

我不是 TypeScript 的粉丝,但我欣赏它进行静态类型检查的能力。幸运的是,有一种方法可以仅使用 JSDoc 注释为 JavaScript 添加静态类型检查。如果你感兴趣,Alex Harri 在他的文章中很好地解释了如何做到这一点。

以下 JSDoc 注释清楚地说明了 createUser 函数接受的参数类型和返回值类型。类型定义可以被 TypeScript 编译器或像 VS Code 这样的 IDE 自动识别,当你传递错误类型的值时,会给你实时反馈。

/** @typedef {{ id?: number, birthDate: Date, email: string, password: string }} User */ /**
 * 创建一个用户并在成功时返回新创建的用户的id
 * @param {User} user
 * @returns <Promise{any}>
 */ 
async function createUser(user) {
    validateUserInput(user) || throwError(err.userValidationFailed);
    isPasswordValid(user.password) || throwError(err.invalidPassword);
    !(await userService.getUserByEmail(user.email)) || throwError(err.userExists);

    user.password = await hashPassword(user.password);
    return userService.create(user);
}

# 总结

通过以下方式,我们将一个看似简单但有些难以理解的函数转变为了一个自我文档化的函数:

  • 使用命名常量代替晦涩的错误代码。
  • 提取复杂逻辑并将其放在单独的函数中。
  • 使用短路求值使代码流程线性化。
  • 引入类型注解以帮助进行静态类型检查和实时编码反馈。

原文:Self-documenting Code