本系列以一位经验丰富的工程师和他侄子之间的虚构对话展开。每集探讨软件从创意到生产的一个阶段。
👦 侄子:叔叔,终于。VS Code 打开了。npm start?
👨🦳 叔叔:等一下。在你运行之前——它即将连接到哪里?什么数据库、谁的凭证、哪个环境?
👦 侄子:……我其实不知道。运行的时候它就工作了。
👨🦳 叔叔:这正是问题所在。“它就是工作”和“我知道它在做什么”不是一回事。打开配置给我看看。
配置:密钥不属于代码
👨🦳 叔叔:看看这一行。
const dbUrl = "postgres://admin:P@[email protected]:5432/wishlist";
Enter fullscreen mode Exit fullscreen mode
👦 侄子:……这是真实密码。硬编码在一个即将提交到 Git 的文件中。
👨🦳 叔叔:一旦提交,它就会永远留在仓库历史中,即使你在下一次提交中删除了这一行。任何有仓库访问权限的人——或者任何仓库意外公开的人——都会知道你的生产数据库密码。
.env (never committed — listed in .gitignore)
DATABASE_URL=postgres://localhost:5432/wishlist_dev
NODE_ENV=development
Enter fullscreen mode Exit fullscreen mode
const dbUrl = process.env.DATABASE_URL;
Enter fullscreen mode Exit fullscreen mode
👦 侄子:所以代码保持不变,但它连接的内容取决于加载的 .env?
👨🦳 叔叔:正是。相同的代码,不同的配置,每个环境。本地使用 wishlist_dev,staging 使用 staging URL,production 使用 production——这些都从未写入代码本身。这是第4集的同一原则——一个盒子不应该知道它不需要知道的事情。你的服务代码不需要知道它“在生产环境中”。它只需要一个 URL,交给它。
镜像:不要伪造数据库
👦 侄子:对于本地开发,我可以用一些简单的东西,比如 SQLite,而不是在我的机器上设置真正的 Postgres?
👨🦳 叔叔:你可以。但它会悄悄地对你撒谎。记住第5集——我们设计的 UNIQUE (user_id, product_id) 约束?SQLite 和 Postgres 并不总是以相同的方式强制执行约束、类型,甚至某些查询行为。你可以编写在本地完美地针对 SQLite 工作的代码,在你的机器上通过所有测试,然后在接触 staging 中的真实 Postgres 时遇到完全不同的错误。
👦 侄子:所以本地必须与生产使用完全相同的数据库引擎。
👨🦳 叔叔:尽可能接近——相同的引擎,相同的主要版本,只要你能管理它。它不需要像素级完全相同;生产可能运行稍新的次要版本,或者在 Kubernetes 中运行,而本地在单个容器中运行。重要的是行为,而不是字面上的相同。这就是 Docker 的用途——一种在本地运行该引擎的一致、可丢弃副本的常见方式。一些团队选择 Dev Containers、Podman 或远程云工作区——工具不如原则重要:使用你的生产环境使用的相同引擎,而不是行为不同的更轻量替代品。
docker-compose.yml
services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: wishlist_dev
ports:
- "5432:5432"
Enter fullscreen mode Exit fullscreen mode
docker-compose up
Enter fullscreen mode Exit fullscreen mode
👦 侄子:一个命令,我就有与公司实际在生产中运行的相同数据库引擎,只是在本地运行带有测试数据。
👨🦳 叔叔:这就是目标。不是生产的完美克隆——你不需要在本地拥有生产的规模或真实用户数据——但行为足够接近,以至于本地通过的内容在 staging 中也有真正的机会通过。
播种数据以真正测试某些东西
👦 侄子:数据库正在运行,但它是空的。如果没有任何用户或产品来测试,我如何测试重复检查?
👨🦳 叔叔:你播种它——有目的地,而不是随机地。
seed.js
users: [{ id: 'u1', name: 'Test User' }]
products: [
{ id: 'p1', name: 'Running Shoes' },
{ id: 'p2', name: 'Discontinued Jacket', active: false }
]
Enter fullscreen mode Exit fullscreen mode
👦 侄子:为什么故意播种一个非活动产品?
👨🦳 叔叔:因为你需要实际练习第5集的决策——当有人将一个不再活跃的产品加入愿望清单,或已经将一个不再活跃的产品加入愿望清单时会发生什么。如果你的播种数据只是“五个随机的快乐路径产品”,你永远不会在本地触发你花了两集设计的确切边缘情况。播种数据应该反映你从第1集开始的隐藏问题,而不仅仅是显而易见的情况。
“在我的机器上工作”
👦 侄子:叔叔——“它就是工作”陷阱真的发生在你身上过吗?
👨🦳 叔叔:有一次,很严重。我的笔记本电脑上一切都通过了。完美。然后 CI 一次又一次地失败,在一个没有意义的检查上——我的代码声称不存在的配置值。花了大半个下午我们才发现:我的机器上有一个六个月前我工作的项目遗留的旧环境变量。它悄悄地填充了一个别人设置没有的值。我的笔记本电脑不是更正确——它只是以一种碰巧看起来像成功的方式不同地坏了。
👦 侄子:所以代码实际上从来都不正确。
👨🦳 叔叔:它只是碰巧在一个机器上正确。从那天起,如果一台新笔记本电脑不能干净地运行我的项目,我假设是 设置 出了问题——而不是队友。这个单一习惯为我的团队节省的时间比本集中几乎任何其他纪律都多。
模拟:尚不存在的东西
👦 侄子:第3集提到了通知服务——价格下降警报。它还不存在。我需要它在本地运行来开发 Wishlist 吗?
👨🦳 叔叔:不需要,这就是很多本地设置悄悄变得痛苦的地方。如果每个工程师都需要其他团队的每个服务在本地运行才能开发自己的功能,入职就会成为一场噩梦,团队的一半人最终在调试 Docker 而不是编写代码。相反——模拟它。
const notificationService = process.env.NODE_ENV === "development"
? new MockNotificationService() // logs to console, does nothing real
: new NotificationService(); // real network call
Enter fullscreen mode Exit fullscreen mode
👦 侄子:所以 Wishlist 服务仍然调用名为 notificationService 的东西。它只是不关心本地实际在那个名字后面的是什么。
👨🦳 叔叔:正是——注意这之所以能干净地工作,是因为 第4集的设计。我们决定 Wishlist 服务不应该知道通知是如何发送的细节。正是这个边界让你可以在不触及一行业务逻辑的情况下换入模拟。
为什么不直接指向生产?
👦 侄子:老实说——我能跳过所有这些,直接将我的笔记本电脑连接到真实的生产数据库吗?它肯定会匹配生产,因为它就是生产。
👨🦳 叔叔:除非你喜欢解释故障。生产数据是真实且有价值的。生产系统在你从外部看不到的方式下是脆弱的。一次意外
DELETE FROM wishlist;
Enter fullscreen mode Exit fullscreen mode
没有 WHERE 子句,在你“只是快速测试某件事”时运行——你的下午就会变成全公司的事故审查。这就是本地开发作为一门独立学科存在的全部原因:给你一个错误代价低廉、可逆且完全由你负责的空间。
真正重要的测试:新队友能运行这个吗?
👨🦳 叔叔:以下是你如何知道你的本地设置实际上是好的,而不仅仅是为你个人工作——把笔记本电脑交给一个从未见过这个仓库的新队友,看看会发生什么。
git clone ...
cp .env.example .env
docker-compose up
npm install
npm run seed
npm start
Enter fullscreen mode Exit fullscreen mode
👦 侄子:如果这不能从头到尾在不是我的机器上工作……
👨🦳 叔叔:那么它实际上不可重现——它只是“在我的机器上工作”,而这句话在整个行业造成的工程时间损失比几乎任何 bug 都多。
配置 → 镜像 → 模拟
👦 侄子:框架。
👨🦳 叔叔:
Configure
↓
Mirror
↓
Mock
Enter fullscreen mode Exit fullscreen mode
配置——每个密钥和每个环境特定值都存在于代码之外,在运行时加载。相同的代码应该在每个环境中运行,而无需更改一行代码。
镜像——你的本地环境应该使用与生产相同的核心技术——相同的数据库引擎,相同的主要版本——而不是在你试图测试的确切条件下表现不同的更轻量替代品。
模拟——任何真正超出此功能边界的东西——另一个团队的服务,一个尚不存在的依赖——在本地获得一个替代品,这样你的设置保持小巧,每个工程师都可以运行它,而无需在他们的笔记本电脑上拥有整个公司的基础设施。
叔叔的话
“一个悄悄对你撒谎的本地环境比根本不工作的环境更糟——至少一个坏的设置会告诉你它坏了。”
👦 侄子:所以在所有这些之前,我甚至还没有编写实际的 addItem 逻辑。
👨🦳 叔叔:全部。现在你可以真正编写代码,知道它在与什么对话,并相信在你的机器上工作的东西在 staging 中也会以相同的方式表现。但编写代码只是纪律的一半——你如何保存它、分享它,并在不踩到队友的情况下合并它是下一课。
🧠 像工程师一样思考——作业
采用你从第3集开始设计的相同功能。
-
配置——列出此功能需要至少三个永远不应该硬编码的值(一个 URL、一个密钥、一个标志)。勾勒你的
.env文件会包含什么。 - 镜像——命名一个使用“更简单”的本地替代品(不同的数据库、内存假)可能隐藏只有在 staging 或生产中出现的 bug 的地方。
- 模拟——命名此功能对自身边界之外某物的依赖,并勾勒你如何在本地存根它而无需运行真实的东西。
不要担心获得确切的工具。目标是练习在环境在你脚下改变之前注意到你的功能秘密依赖于环境的每个地方。
— 第7集结束 —
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.