Epistemology - 验证世界是否为真
核心问题:
我们怎么知道系统是对的?
Dimension 1 负责定义世界。
Dimension 2 负责验证世界。
软件工程里的认识论不是哲学装饰,而是一组工程能力:
用证据替代感觉,用反馈替代猜测,用契约替代隐式默契。
1. 测试固定系统承诺
测试不是为了证明没有 bug。
测试是为了证明:
在我们关心的场景里,系统满足了明确承诺。
判断问题
- 这个测试固定了哪个业务承诺?
- 如果测试失败,说明系统背叛了什么?
- 它测的是行为,还是实现细节?
- 它是否围绕风险,而不是围绕代码行?
推荐动作
把测试名写成规格:
it("revokes purchase-based course access after refund")
it("prevents organization managers from assigning courses outside their organization")
核心句:
好测试不是测函数,而是固定承诺。
2. 不同测试回答不同层次的问题
| 测试类型 | 回答的问题 | 适合验证 |
|---|---|---|
| Unit | 规则本身对不对? | 状态机、权限、价格计算 |
| Integration | 边界协作对不对? | DB、事务、worker、API 到 service |
| E2E | 用户路径能不能走通? | 登录、购买、播放、后台关键流程 |
原则:
用最低成本的测试,获得足够强的信心。
3. 可观察性设计证据链
可观察性不是多打日志。
它回答:
生产环境出未知问题时,我们能不能理解它?
三根支柱
Logs -> 发生了什么
Metrics -> 整体表现如何
Traces -> 一次请求经过哪里
推荐证据链
payment.succeeded
-> purchase.created
-> course_access_grant.created
-> canAccessCourse=false
-> reason: account_suspended
核心句:
系统要能解释自己的决定。
4. 记录原因,不只记录结果
坏代码:
return false
好方向:
return {
allowed: false,
reason: "account_suspended",
}
权限、访问权、支付、退款、风控等关键判断,都应该能解释原因。
核心句:
没有 reason 的失败,很难被理解和改进。
5. 把意见改写成假设
工程里很多判断只是意见:
这个架构更灵活。
这个优化会更快。
这个抽象以后会有用。
要改写成可证伪假设:
如果我们做 X,
在条件 Y 下,
指标 Z 会发生变化,
达到阈值 T,
否则说明假设不成立。
示例:
如果 CourseAccessGrant 是正确抽象,
那么新增一种访问来源时,
调用方不需要修改自己的访问判断,
只需要 course-access 模块增加新的 grant 来源。
核心句:
成熟工程师不会把直觉伪装成结论,而是把直觉改写成可验证假设。
6. 反馈回路决定错误成本
核心问题:
错误多久之后会被发现?
反馈层级:
编辑器 -> 本地测试 -> CI -> Review -> Staging -> Production
越早发现,成本越低。
好反馈特征
- 快
- 准
- 相关
- 可行动
核心句:
CI/CD 的本质是缩短从改变到证据的距离。
7. 小批量改变缩短反馈
大 PR 的问题:
- review 慢
- 测试失败难定位
- 回滚困难
- 风险集中
更好的节奏:
先改名
再抽规则函数
再迁移调用方
再删除旧路径
核心句:
小批量改变不是慢,而是让每一步更快获得证据。
8. 不确定性要被管理,不要被掩盖
成熟工程不是假装确定,而是让错误代价变小。
工具箱
| 工具 | 作用 |
|---|---|
| Feature Flag | 把部署和发布分开 |
| 灰度发布 | 小范围暴露风险 |
| Shadow Mode | 新逻辑先观察,不影响用户 |
| Kill Switch | 无需重新部署即可停止危险行为 |
| 数据修复路径 | 处理已经产生的坏数据 |
核心句:
不确定性不是失败,假装确定才危险。
9. 调试是科学方法
调试流程:
观察现象
-> 提出假设
-> 设计实验
-> 收集证据
-> 排除错误解释
-> 定位根因
-> 修复并防止复发
调试时先问
- 现象是什么?
- 期望行为来自哪个系统承诺?
- 正常证据链是什么?
- 当前证据链断在哪里?
- 哪个问题能最大幅度缩小范围?
核心句:
好的调试不是“我试试这个”,而是每一步都减少不确定性。
10. 区分触发条件和根因
示例:
触发条件:某个企业用户同时拥有订阅访问和组织分配访问。
根因:canAccessCourse 假设每个 account-course 只有一个访问来源。
只修触发条件,问题会变成特殊分支。
修根因,模型会变清楚:
CourseAccessGrant[]
核心句:
触发条件解释这次为什么爆,根因解释系统为什么允许它爆。
11. 契约让验证有对象
测试、监控、调试都依赖一个前提:
我们知道系统承诺了什么。
契约可以由这些东西表达:
- 类型
- 测试
- API schema
- 状态机
- 文档
- 错误码
- 断言
- 日志 reason
核心句:
隐式契约是技术债。
12. 错误也是契约
坏错误:
{
"error": "failed"
}
好错误:
{
"code": "COURSE_NOT_PURCHASABLE",
"message": "This course is not available for purchase.",
"reason": "course_archived"
}
稳定错误码可以服务:
- 调用方处理
- UI 展示
- 监控统计
- 客服排查
核心句:
失败方式也应该被系统承认和命名。
13. Source of Truth 要按问题指定
不要泛泛说:
数据库是真相。
要问:
对这个问题,哪个数据是权威来源?
| 问题 | Source of Truth |
|---|---|
| 钱是否扣成功 | 支付平台 |
| 是否创建购买记录 | Purchase |
| 是否发放访问权 | CourseAccessGrant |
| 学习进度 | LessonProgress |
| 报表数据 | analytics projection |
核心句:
Source of truth 是某个问题的权威来源,不是某个系统的荣誉称号。
14. 缓存不是事实
缓存是:
带时效承诺的副本。
设计缓存时问:
- 缓存什么问题的答案?
- Source of truth 是什么?
- 所有影响答案的输入有哪些?
- key 是否包含这些输入?
- 最多允许旧多久?
- 失效失败会造成性能问题还是业务错误?
核心句:
TTL 的本质是承诺“最多错多久”。
15. 权限缓存要极度谨慎
canAccessCourse(accountId, courseId) 这种判断变化原因很多:
- 支付成功
- 退款完成
- 订阅过期
- 组织移除
- 账号封禁
- 课程下架
更稳的策略经常是:
缓存规则所需的数据,而不是缓存最终授权结论。
核心句:
缓存失效失败如果会导致越权,它就不是普通性能问题。
16. 复盘把错误变成知识
复盘不是追责。
复盘要问:
- 我们原来相信什么?
- 事实证明哪里错了?
- 为什么现有测试、监控、流程没有提前发现?
- 系统要增加什么证据或约束?
- 以后类似问题如何更早暴露?
核心句:
温和对人,严格对系统。
17. Action Item 必须改变系统
坏行动项:
以后更仔细。
加强 review。
注意测试。
好行动项:
新增 refund revocation integration test。
新增 refund_completed_count / access_grant_revoked_count 指标。
所有 grant revocation 必须通过 revokeCourseAccessGrant。
核心句:
如果行动项不能被验证,它就不是行动项。
一页总口诀
写测试前,先问承诺。
打日志前,先问信息链。
做优化前,先写假设。
合并代码前,先跑反馈。
上线之前,先设计回退。
调试之前,先描述现象。
改契约前,先找调用方。
读数据前,先问真相源。
加缓存前,先问会旧多久。
复盘之后,必须改变系统。
最终心法
软件工程里的“知道”,不是相信某个人判断正确,而是系统中存在足够多、足够快、足够可信的证据。
Dimension 2 的目标,是把团队从:
我觉得没问题
带到:
我们有证据说明它满足承诺;
如果它错了,我们也能很快知道、定位、回滚和学习。