Philosophy - 定义世界的边界
核心问题:
我们到底在构建什么?
软件系统不是代码的集合,而是对现实世界的一套可执行模型。
Dimension 1 的目标是训练一种能力:
在写代码之前,先看清系统里的概念、事实、状态、关系和边界。
1. 命名就是本体论
每定义一个类、类型、表、模块或接口,你都在回答:
这个系统里什么东西存在?
判断问题
- 这个名字对应真实概念,还是只是方便装字段?
- 它和相邻概念有什么区别?
- 它的反例是什么?
- 它有没有独立生命周期?
- 它有没有独立不变量?
危险信号
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)
判断问题
- 这个字段是事实、状态,还是推导视图?
- 如果它变成 false,是谁负责更新?
- 它能从哪些事实重建?
- 它坏了之后能解释原因吗?
核心句:
不要把结论当事实存。优先存事实,让结论从事实推导出来。
4. State 必须有状态机
状态字段不是字符串枚举那么简单。
坏味道
order.status = "refunded"
如果任何地方都能直接写状态,状态机就只存在于人的脑子里。
好方向
markOrderPaid(orderId)
refundOrder(orderId)
cancelOrder(orderId)
判断问题
- 状态集合是否互斥?
- 每个状态含义是否明确?
- 状态之间是否有合法流转?
- 是否禁止非法跳转?
核心句:
如果状态没有流转规则,它就只是一个容易写坏的字符串。
5. KISS 是少创造错误实体
KISS 不是代码少,而是概念刚好够用。
假简单
type User = {
role: string
status: string
metadata: Record<string, unknown>
}
字段少,但复杂性被赶进运行时。
过度设计
AbstractActorRelationResolver
UniversalPermissionEngine
GenericCourseRelationship
需求还没稳定,抽象先稳定了。
判断问题
- 这个新实体是否有独立生命周期?
- 是否有独立不变量?
- 删除它后,当前真实规则是否无法表达?
- 它解决真实复杂性,还是只是把焦虑实体化?
核心句:
简单不是少写代码,而是少创造没有必要存在的概念。
6. 抽象停在变化边界
抽象不是越高越好。
判断问题
- 这些东西是否有相同生命周期?
- 是否有相同不变量?
- 是否会因为同一个原因变化?
- 抽象后调用者是否知道得更少?
- 这个抽象有没有自然的领域名字?
决策树
只是表面相似?
-> 先复制
同一条规则重复?
-> 抽函数
同一个概念有生命周期和不变量?
-> 抽类型
一组规则共同变化,需要隐藏细节?
-> 抽模块
需要独立部署、数据所有权或团队边界?
-> 抽服务
多个团队长期重复同类能力?
-> 考虑平台
核心句:
三次重复之前,先复制。三次重复之后,找变化轴。找不到变化轴,不要抽象。
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. 对抗熵增是日常动作
代码库天然会变乱。
技术债不是丑代码,而是:
当前系统结构和未来变化方向之间的摩擦。
常见熵增来源
- 需求压力下的特殊分支
- 命名漂移
- 模块边界侵蚀
- 派生状态失控
- 临时方案永久化
推荐动作
每次提交做一点小清理:
- 改一个更准确的名字
- 删除一个死分支
- 把重复判断收进函数
- 给含糊状态补上枚举
- 把散落规则集中到一个地方
核心句:
每一次提交,都让系统比之前更容易理解一点点。
10. 架构选择从问题性质开始
不要从工具名开始。
坏问题
我们要不要上 Kafka?
好问题
我们需要解耦、缓冲、削峰、持久化、重放、广播,还是一致性?
判断清单
- 负载性质:请求量、峰值、读写比例
- 一致性要求:强一致还是最终一致
- 失败模型:能否重试,是否幂等
- 顺序要求:是否需要全局或局部有序
- 可恢复性:是否需要重放和审计
- 运维成本:团队是否能维护
核心句:
复杂工具必须由真实复杂性支付。
一页总口诀
命名之前,先问边界。
建表之前,先问事实。
加状态之前,先画流转。
加字段之前,先问能否推导。
加角色之前,先问上下文。
加权限之前,先问动作。
加 owner 之前,先问归属。
加抽象之前,先找变化轴。
加服务之前,先问数据和团队边界。
加工具之前,先问根本问题。
最终心法
软件工程的很多问题,表面是代码问题,深层是世界划分问题。
当你能清楚回答:
- 什么东西存在?
- 它为什么存在?
- 它和别的东西有什么边界?
- 它如何变化?
- 它应该由谁负责?
代码自然会变得更稳。