Epistemology - 验证世界是否为真

核心问题:

我们怎么知道系统是对的?

Dimension 1 负责定义世界。

Dimension 2 负责验证世界。

软件工程里的认识论不是哲学装饰,而是一组工程能力:

用证据替代感觉,用反馈替代猜测,用契约替代隐式默契。

1. 测试固定系统承诺

测试不是为了证明没有 bug。

测试是为了证明:

在我们关心的场景里,系统满足了明确承诺。

判断问题

  1. 这个测试固定了哪个业务承诺?
  2. 如果测试失败,说明系统背叛了什么?
  3. 它测的是行为,还是实现细节?
  4. 它是否围绕风险,而不是围绕代码行?

推荐动作

把测试名写成规格:

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

越早发现,成本越低。

好反馈特征

  1. 相关
  2. 可行动

核心句:

CI/CD 的本质是缩短从改变到证据的距离。

7. 小批量改变缩短反馈

大 PR 的问题:

  • review 慢
  • 测试失败难定位
  • 回滚困难
  • 风险集中

更好的节奏:

先改名
再抽规则函数
再迁移调用方
再删除旧路径

核心句:

小批量改变不是慢,而是让每一步更快获得证据。

8. 不确定性要被管理,不要被掩盖

成熟工程不是假装确定,而是让错误代价变小。

工具箱

工具作用
Feature Flag把部署和发布分开
灰度发布小范围暴露风险
Shadow Mode新逻辑先观察,不影响用户
Kill Switch无需重新部署即可停止危险行为
数据修复路径处理已经产生的坏数据

核心句:

不确定性不是失败,假装确定才危险。

9. 调试是科学方法

调试流程:

观察现象
  -> 提出假设
  -> 设计实验
  -> 收集证据
  -> 排除错误解释
  -> 定位根因
  -> 修复并防止复发

调试时先问

  1. 现象是什么?
  2. 期望行为来自哪个系统承诺?
  3. 正常证据链是什么?
  4. 当前证据链断在哪里?
  5. 哪个问题能最大幅度缩小范围?

核心句:

好的调试不是“我试试这个”,而是每一步都减少不确定性。

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. 缓存不是事实

缓存是:

带时效承诺的副本。

设计缓存时问:

  1. 缓存什么问题的答案?
  2. Source of truth 是什么?
  3. 所有影响答案的输入有哪些?
  4. key 是否包含这些输入?
  5. 最多允许旧多久?
  6. 失效失败会造成性能问题还是业务错误?

核心句:

TTL 的本质是承诺“最多错多久”。

15. 权限缓存要极度谨慎

canAccessCourse(accountId, courseId) 这种判断变化原因很多:

  • 支付成功
  • 退款完成
  • 订阅过期
  • 组织移除
  • 账号封禁
  • 课程下架

更稳的策略经常是:

缓存规则所需的数据,而不是缓存最终授权结论。

核心句:

缓存失效失败如果会导致越权,它就不是普通性能问题。

16. 复盘把错误变成知识

复盘不是追责。

复盘要问:

  1. 我们原来相信什么?
  2. 事实证明哪里错了?
  3. 为什么现有测试、监控、流程没有提前发现?
  4. 系统要增加什么证据或约束?
  5. 以后类似问题如何更早暴露?

核心句:

温和对人,严格对系统。

17. Action Item 必须改变系统

坏行动项:

以后更仔细。
加强 review。
注意测试。

好行动项:

新增 refund revocation integration test。
新增 refund_completed_count / access_grant_revoked_count 指标。
所有 grant revocation 必须通过 revokeCourseAccessGrant。

核心句:

如果行动项不能被验证,它就不是行动项。

一页总口诀

写测试前,先问承诺。
打日志前,先问信息链。
做优化前,先写假设。
合并代码前,先跑反馈。
上线之前,先设计回退。
调试之前,先描述现象。
改契约前,先找调用方。
读数据前,先问真相源。
加缓存前,先问会旧多久。
复盘之后,必须改变系统。

最终心法

软件工程里的“知道”,不是相信某个人判断正确,而是系统中存在足够多、足够快、足够可信的证据。

Dimension 2 的目标,是把团队从:

我觉得没问题

带到:

我们有证据说明它满足承诺;
如果它错了,我们也能很快知道、定位、回滚和学习。