核心问题
相似的事物,是否以相似的方式表达?
优雅系统的一个重要特征是可预测。
你看过一部分,就能合理猜到另一部分。
例如:
createCourse()
publishCourse()
archiveCourse()
deleteCourse()
比:
createCourse()
courseGoLive()
setArchived()
remove()
更优雅。
因为它形成了命名对称。
核心句:
一致性减少记忆负担,让系统可预测。
一致性不是死板
一致性不是所有东西都长一样。
它的意思是:
同类问题用同类表达,不同问题保留必要差异。
如果所有东西都被强行套进同一个模板,就会变成僵化。
例如:
CourseManager
PaymentManager
UserManager
NotificationManager
这看似一致,但可能掩盖真实差异。
好的统一不是命名后缀统一,而是模型和行为统一。
API 命名一致性
API 命名应该让调用者能猜。
例如资源生命周期:
createCourse()
updateCourse()
publishCourse()
archiveCourse()
deleteCourse()
如果订单也有生命周期,可以保持风格:
createOrder()
markOrderPaid()
cancelOrder()
refundOrder()
注意:不是所有动作都必须强行叫 update。
如果动作有领域意义,就应该用领域动词:
publishCourse()
refundOrder()
revokeAccessGrant()
inviteOrganizationMember()
一致性不是消灭领域语言。
核心句:
相同层级保持风格一致,具体动作保留领域含义。
参数顺序一致性
参数顺序也会影响美感和安全。
坏例子:
canAccessCourse(accountId, courseId)
grantCourseAccess(courseId, accountId)
revokeAccessGrant(accountId, grantId)
调用者很容易传错。
更好:
canAccessCourse(accountId, courseId)
grantCourseAccess(accountId, courseId)
revokeCourseAccess(accountId, courseId)
或者对复杂参数使用对象:
grantCourseAccess({
accountId,
courseId,
source,
sourceId,
})
原则:
相似函数的参数顺序应该一致。
错误模型一致性
错误模型不一致会让系统粗糙。
坏状态:
{ "error": "forbidden" }
{ "message": "Not allowed" }
{ "code": 403, "detail": "no access" }
{ "success": false, "reason": "COURSE_ACCESS_DENIED" }
调用方每次都要猜。
更好:
{
"code": "COURSE_ACCESS_DENIED",
"message": "You cannot access this course.",
"reason": "account_suspended",
"requestId": "req_123"
}
统一错误模型带来:
- 前端处理简单
- 监控聚合容易
- 客服排查一致
- 测试断言清楚
状态机一致性
状态流转也要有一致表达。
例如课程发布:
draft -> published -> archived
订单:
pending -> paid -> refunded
pending -> canceled
订阅:
trialing -> active -> past_due -> canceled
active -> expired
每个状态机不一样,但表达方式可以一致:
- 状态名明确
- 流转动作命名
- 禁止直接改 status
- 非法流转抛出领域错误
- 状态变更记录事件
例如:
publishCourse(courseId)
archiveCourse(courseId)
markOrderPaid(orderId)
refundOrder(orderId)
cancelSubscription(subscriptionId)
核心句:
状态机不同,但状态机的表达方式应该一致。
目录结构一致性
目录结构是系统地图。
坏结构:
services/
user.ts
course/
courseService.ts
modules/
payments/index.ts
utils/
access.ts
lib/
organization-membership.ts
读者不知道东西该放哪里。
更一致的结构:
modules/
course-access/
policy.ts
grants.ts
queries.ts
tests/
billing/
payments.ts
refunds.ts
outbox.ts
tests/
organizations/
memberships.ts
invitations.ts
tests/
目录结构应该让新增功能有自然位置。
核心句:
好目录结构让人少问“这个文件该放哪”。
测试结构一致性
测试也是系统的一部分。
如果有的测试在:
__tests__/
有的在:
tests/
有的和源码同目录,有的在根目录,规则不清楚,维护成本会上升。
更好的做法是明确一种模式。
例如:
course-access/
policy.ts
policy.test.ts
或者:
course-access/
policy.ts
tests/
policy.test.ts
选择哪种都可以,关键是统一。
交互一致性
不仅代码需要一致性,产品交互也需要。
例如后台操作:
- 删除课程
- 封禁账号
- 导出数据
- 退款
高风险操作应该有一致流程:
点击操作
-> 展示影响范围
-> 输入确认或二次认证
-> 要求 reason
-> 执行
-> 写 audit log
如果删除课程需要 reason,但封禁账号不需要,系统就很奇怪。
一致性也是用户信任的一部分。
一致性破坏的代价
不一致会带来:
- 记忆负担
- 调用错误
- 文档变多
- review 成本上升
- 新人学习慢
- 自动化困难
- 测试风格混乱
最糟的是:
不一致会让系统失去可预测性。
当工程师不能预测系统会如何命名、放置、失败、返回错误时,每次改动都要重新探索。
什么时候可以打破一致性
一致性不是绝对。
可以打破一致性,但要有理由。
合理理由:
- 领域概念真的不同
- 安全风险更高,需要额外步骤
- 性能要求完全不同
- 外部协议强制
- 迁移阶段需要兼容
不合理理由:
- 当时随手写的
- 每个人按自己习惯
- 没有统一约定
- 旧代码懒得改
打破一致性时,最好留下解释。
一致性检查清单
看一个系统是否一致,问:
- 相似动作是否使用相似命名?
- 参数顺序是否一致?
- 错误响应是否统一?
- 状态机表达方式是否一致?
- 目录结构是否让新增功能有自然位置?
- 测试位置和命名是否统一?
- 高风险后台操作流程是否一致?
- 打破一致性是否有明确理由?
- 新人能否通过一个模块猜到另一个模块?
小结
- 优雅系统可预测。
- 一致性不是死板,而是同类问题用同类表达。
- API 命名、参数顺序、错误模型、状态机、目录结构都需要一致性。
- 一致性减少记忆负担,也减少误用。
- 目录结构是系统地图。
- 可以打破一致性,但要有真实理由。
- 好系统让你看过一部分,就能合理猜到另一部分。