更好的编码方式:文档驱动开发 API 很难 测试很难 直奔主题 完善流程 现在就把它带回来 结论

2025-05-28

更好的编码方式:文档驱动开发

API 很难

测试很难

切入正题

完善流程

现在把它带回来吧

结论

如果您在软件开发上花费了大量时间,那么您无疑听说过“测试驱动开发”或简称“TDD”这个表达。

TDD 背后的理念是,你应该在编写实现代码之前编写测试。例如,假设你想calculateUserScore在电子游戏中实现一个基于用户 K/D 值调用的函数。根据 TDD,你应该首先编写单元测试或集成测试,以验证输入是否符合预期的输出。

从测试开始可以极大地帮助确保你的程序最终能够按预期运行。然而,测试也有一个缺点,那就是它仍然是一种编码形式;是的,即使你遵循良好的测试实践,对值进行硬编码并避免复杂的逻辑,它仍然是一种编码形式。这仍然是软件开发,你的测试最终仍然需要通过。

由于实现细节未知,确保测试通过可能颇具挑战性。毕竟,如果你期望parseInt以一种方式运行,而结果却恰恰相反,你很可能不得不重写所有基于该假设的测试。

因此,许多人选择开始实现一个功能作为概念验证,然后在实现的同时逐步添加测试:可以说是 TDD-lite。

问题是,这样做,你会失去测试驱动开发最重要的好处之一:它能够迫使你提前面对你的 API。

API 很难

你在一家独立游戏公司工作。你用 JavaScript 和 Phaser写了一款小型自上而下的射击游戏。你的贝斯手要求你实现用户评分功能。

“没问题,calculateUserScore非常简单——不需要想太多。”

你想,输入一个基本的实现:

function calculateUserScore({kills, deaths}) {
    return parseInt(kills / deaths, 10)
}
Enter fullscreen mode Exit fullscreen mode

可是等等!助攻呢?助攻也算吗?当然算。我们就把它算作击杀的一半吧。

function calculateUserScore({kills, deaths, assists}) {
    const totalKills = kills + (assists / 2);
    return parseInt(totalKills / deaths, 10)
}
Enter fullscreen mode Exit fullscreen mode

哦,不过有些击杀应该加分。毕竟,谁不喜欢360度无瞄准镜射击呢?虽然kills之前只是一个数字,但我们把它改成一个对象数组,像这样:

const killsArr = [
     {
          additionalPoints: 3
     }
]
Enter fullscreen mode Exit fullscreen mode

现在我们可以改变这个函数的实现:

function calculateUserScore({killsArr, deaths, assists}) {
    const kills = killsArr.length;
    const additionalPoints = killsArr.reduce((prev, k) => k.additionalPoints, 0);
    const totalKills = kills + (assists / 2);
    return parseInt((totalKills / deaths) + additionalPoints, 10);
}
Enter fullscreen mode Exit fullscreen mode

虽然我们已经看到了函数的变化,但请记住,您的游戏可能在代码库的多个部分进行此计算。除此之外,您的 API 可能仍然不完善,无法完美支持此功能。如果您想在比赛结束后显示特殊击杀并获得额外积分,该怎么办?

这些大规模的重构意味着每次迭代都需要额外的重构工作,这可能会延迟工单的完成时间。这可能会影响发布日期或其他预定的发布计划。

让我们退一步思考一下,为什么会发生这种情况?

这些问题往往是由于范围沟通不畅造成的。这种沟通不畅可能发生在团队之间,也可能发生在个人之间,甚至可能仅仅发生在你的内心独白中。

测试很难

许多人建议解决这个问题的一种方法是遵循 TDD。TDD 可以通过添加反馈循环来强制你提前处理 API。

例如,在将calculateUserScore功能实现到代码库之前,您可能会针对第一个实现进行测试,添加test.todo辅助功能,并意识到应该在继续前进之前更新 API。

然而,虽然 TDD 迫使你处理 API,但它并不能帮助你区分范围。这种对范围理解的局限性反过来可能会影响你的 API。

让我解释一下:

假设事后追踪特殊击杀的功能直到开发周期的后期才能在比赛结束时显示。您知道这一点,并决定在第二个实现中停止,此时kills仍然是一个数字。但是,由于该函数在代码库中重复使用,您需要在以后进行更大规模的重构。

如果你和其他工程师交流过,或许会发现比赛结束画面的开发比预期提前完成了。可惜的是,这个问题直到你完成实现后才在代码审查中被发现,这迫使你立即进行重构。

切入正题

好吧,好吧,我来切入正题:有一种比 TDD 更好的方法可以解决这个“API 转移”问题。这个“更好的方法”就是“文档驱动开发”。

德雷克将目光移开

先写文档可以帮助你在设计实现的艰难抉择之前,提前理清实现细节。即使是参考 API 也能帮助你完成很多设计。

让我们回到之前的例子calculateUserScore。和之前一样,你召开了一个简短的会议来收集团队的需求。不过,这次你从编写文档开始,而不是从编写代码开始。

您提到了基于以下要求的 API 应该是什么样的:

/**
 * This function should calculate the user score based on the K/D of the
 * player.
 *
 * Assists should count as half of a kill
 *
 * TODO: Add specialty kills with bonus points
 */
function calculateUserScore(props: {kills: number, deaths: number, assists: number}): number;
Enter fullscreen mode Exit fullscreen mode

您还决定在文档中展示一些用法:

caluculateUserScore({kills: 12, deaths: 9, assists: 3});
Enter fullscreen mode Exit fullscreen mode

在研究这些文档时,您决定快速勾勒出添加奖励点后未来的 API 可能是什么样子。

/*
 * TODO: In the future, it might look something like this to accommodate
 * bonus points
 */
calculateUserScore({kills: [{killedUser: 'user1', bonusPoints: 1}], deaths: 0, assists: 0});
Enter fullscreen mode Exit fullscreen mode

写完这段代码后,你意识到应该先用数组来存储 kills 属性,而不是事后再说。你不需要获得奖励积分,只需简单地追踪unknown每个用户的击杀次数,并在以后进行修改即可。

calculateUserScore({kills: [{killedUser: 'unknown'}], deaths: 0, assists: 0});
Enter fullscreen mode Exit fullscreen mode

虽然现在看来这似乎很明显,但目前可能还不是那么清晰。这就是文档驱动开发的好处:它迫使你对 API 和工作范围进行自我反馈。

完善流程

好的,我明白了。记录被视为一件苦差事。虽然我可以继续说“你的药对你有好处”,但我有个好消息要告诉你:记录的意义并非你想象的那样。

文档有多种形式:设计模型、API 参考文档、格式良好的票证、未来计划书写等等。

本质上,任何可以用来传达您对某个主题的想法的东西都是文档。

事实上,这包括测试。😱 测试是传达 API 示例以供您使用的好方法。TDD 本身可能足以为未来的您传达这些信息,而其他时候,它也可以与其他形式的文档配合使用。

特别是,如果您擅长编写集成测试,那么您在编写测试代码时实际上会写出使用 API 文档。

在编写开发工具或库时尤其如此。查看某个用例的使用示例非常有帮助,尤其是配合测试来验证其行为。


“文档驱动开发”没有规定的另一件事是“一次编写,完成所有工作”。这个想法是一个神话,可能会损害你的开发范围和预算——无论是时间还是其他方面。

正如我们在calculateUserScore示例中所展示的,您可能需要在最终版本发布之前修改设计:这没关系。文档影响代码,代码又影响文档。TDD 也是如此。


DDD 不仅仅适用于开发生产代码。在面试中,一些沟通开发工作流程的好建议是先写代码注释,然后再写解决方案。这样,你就可以在文档阶段(也就是写注释阶段)犯错,而这比在实现阶段犯错所浪费的时间更少。

通过这样做,你可以向面试官表明你知道如何团队合作,并找到明确的目标。这些理解将帮助你实现无边界情况*的实现。

现在把它带回来吧

我意识到这篇文章已经比 M. Night Shyamalan 的电影还要曲折,但这里还要再说一句:文档驱动开发,正如我们今天所探讨的,是一个成熟的概念。它只是换了个名字而已:

这些都是指验证用户行为背后代码功能的一种形式。它们都鼓励一种更强大的沟通方法,通常包含流程文档。“领域驱动设计 (DDD)”只是这种逻辑的另一种形式。

结论

我一直以文档驱动开发为理念,在一些项目中推动我的编码工作。我的项目就在其中CLI Testing Library,它让我能够编写大量的文档页面以及冗长的 GitHub 问题

这两件事都迫使我更好地完善我的目标和追求。我相信最终的产品会更好。

你觉得怎么样?“DDD”是个好主意吗?你会在下一个项目中使用它吗?

让我们知道您的想法,并加入我们的 Discord与我们进一步讨论!

文章来源:https://dev.to/this-is-learning/a-better-way-to-code-documentation-driven-development-1kem
PREV
SolidJS 十年
NEXT
Angular 18 中的新功能