Philosophy - 定义世界的边界

核心问题:

我们到底在构建什么?

软件系统不是代码的集合,而是对现实世界的一套可执行模型。

Dimension 1 的目标是训练一种能力:

在写代码之前,先看清系统里的概念、事实、状态、关系和边界。

1. 命名就是本体论

每定义一个类、类型、表、模块或接口,你都在回答:

这个系统里什么东西存在?

判断问题

  1. 这个名字对应真实概念,还是只是方便装字段?
  2. 它和相邻概念有什么区别?
  3. 它的反例是什么?
  4. 它有没有独立生命周期?
  5. 它有没有独立不变量?

危险信号

User
Entity
Relation
Metadata
Context
Manager
Helper

这些名字不是绝对不能用,但如果核心业务模型里到处都是它们,通常说明边界还没想清楚。

推荐动作

把宽名字拆成窄名字:

User
  -> Account
  -> Credential
  -> Profile
  -> OrganizationMembership
  -> InstructorProfile

核心句:

好名字不是让代码更好看,而是让错误设计更难发生。

2. 身份不是本体,身份常常是关系

不要问:

这个人是什么角色?

要问:

这个账号在什么上下文里承担什么身份?

坏模型

type User = {
  role: "student" | "instructor" | "admin"
}

好方向

type OrganizationMembership = {
  accountId: string
  organizationId: string
  role: "owner" | "manager" | "member"
}

type CourseStaffAssignment = {
  accountId: string
  courseId: string
  role: "primary_instructor" | "assistant" | "reviewer"
}

核心句:

角色必须回答“在哪个上下文里”。

3. 优先存事实,不要存结论

不要把派生结论直接当核心事实存。

坏模型

user.isCustomer = true
user.canViewCourse = true

好方向

Purchase
Subscription
CourseAssignment
CourseAccessGrant

然后推导:

canAccessCourse(accountId, courseId)
isPayingCustomer(accountId)

判断问题

  1. 这个字段是事实、状态,还是推导视图?
  2. 如果它变成 false,是谁负责更新?
  3. 它能从哪些事实重建?
  4. 它坏了之后能解释原因吗?

核心句:

不要把结论当事实存。优先存事实,让结论从事实推导出来。

4. State 必须有状态机

状态字段不是字符串枚举那么简单。

坏味道

order.status = "refunded"

如果任何地方都能直接写状态,状态机就只存在于人的脑子里。

好方向

markOrderPaid(orderId)
refundOrder(orderId)
cancelOrder(orderId)

判断问题

  1. 状态集合是否互斥?
  2. 每个状态含义是否明确?
  3. 状态之间是否有合法流转?
  4. 是否禁止非法跳转?

核心句:

如果状态没有流转规则,它就只是一个容易写坏的字符串。

5. KISS 是少创造错误实体

KISS 不是代码少,而是概念刚好够用。

假简单

type User = {
  role: string
  status: string
  metadata: Record<string, unknown>
}

字段少,但复杂性被赶进运行时。

过度设计

AbstractActorRelationResolver
UniversalPermissionEngine
GenericCourseRelationship

需求还没稳定,抽象先稳定了。

判断问题

  1. 这个新实体是否有独立生命周期?
  2. 是否有独立不变量?
  3. 删除它后,当前真实规则是否无法表达?
  4. 它解决真实复杂性,还是只是把焦虑实体化?

核心句:

简单不是少写代码,而是少创造没有必要存在的概念。

6. 抽象停在变化边界

抽象不是越高越好。

判断问题

  1. 这些东西是否有相同生命周期?
  2. 是否有相同不变量?
  3. 是否会因为同一个原因变化?
  4. 抽象后调用者是否知道得更少?
  5. 这个抽象有没有自然的领域名字?

决策树

只是表面相似?
  -> 先复制

同一条规则重复?
  -> 抽函数

同一个概念有生命周期和不变量?
  -> 抽类型

一组规则共同变化,需要隐藏细节?
  -> 抽模块

需要独立部署、数据所有权或团队边界?
  -> 抽服务

多个团队长期重复同类能力?
  -> 考虑平台

核心句:

三次重复之前,先复制。三次重复之后,找变化轴。找不到变化轴,不要抽象。

7. 抽象共同效果,不要合并不同事实

购买、订阅、企业分配是不同事实。

但它们可能产生同一种效果:课程访问权。

不推荐

UniversalCourseRelationship

推荐

type CourseAccessGrant = {
  accountId: string
  courseId: string
  source: "purchase" | "subscription" | "organization_assignment" | "coupon"
  validFrom: Date
  validUntil?: Date
}

核心句:

不要急着抽象事物本身。优先抽象共同的行为、结果或协议。

8. Role、Permission、Ownership、Access 分开

这四个词回答不同问题。

概念问题示例
Identity你是谁?Account, Credential
Role你在某上下文里是什么身份?OrganizationMembership.role
Permission你能做什么动作?canEditCourse
Ownership这个东西归谁负责?Course.ownerAccountId
Access你能不能使用资源?canAccessCourse

坏模型

user.role = "enterprise_admin"
user.canEditCourse = true
user.isOwner = true

好方向

OrganizationMembership
CourseStaffAssignment
CourseAccessGrant
canEditCourse(accountId, courseId)

核心句:

Ownership 是归属关系,Permission 是动作判断,Access 是资源使用权。

9. 对抗熵增是日常动作

代码库天然会变乱。

技术债不是丑代码,而是:

当前系统结构和未来变化方向之间的摩擦。

常见熵增来源

  1. 需求压力下的特殊分支
  2. 命名漂移
  3. 模块边界侵蚀
  4. 派生状态失控
  5. 临时方案永久化

推荐动作

每次提交做一点小清理:

  • 改一个更准确的名字
  • 删除一个死分支
  • 把重复判断收进函数
  • 给含糊状态补上枚举
  • 把散落规则集中到一个地方

核心句:

每一次提交,都让系统比之前更容易理解一点点。

10. 架构选择从问题性质开始

不要从工具名开始。

坏问题

我们要不要上 Kafka?

好问题

我们需要解耦、缓冲、削峰、持久化、重放、广播,还是一致性?

判断清单

  1. 负载性质:请求量、峰值、读写比例
  2. 一致性要求:强一致还是最终一致
  3. 失败模型:能否重试,是否幂等
  4. 顺序要求:是否需要全局或局部有序
  5. 可恢复性:是否需要重放和审计
  6. 运维成本:团队是否能维护

核心句:

复杂工具必须由真实复杂性支付。

一页总口诀

命名之前,先问边界。
建表之前,先问事实。
加状态之前,先画流转。
加字段之前,先问能否推导。
加角色之前,先问上下文。
加权限之前,先问动作。
加 owner 之前,先问归属。
加抽象之前,先找变化轴。
加服务之前,先问数据和团队边界。
加工具之前,先问根本问题。

最终心法

软件工程的很多问题,表面是代码问题,深层是世界划分问题。

当你能清楚回答:

  • 什么东西存在?
  • 它为什么存在?
  • 它和别的东西有什么边界?
  • 它如何变化?
  • 它应该由谁负责?

代码自然会变得更稳。