核心问题

相似的事物,是否以相似的方式表达?

优雅系统的一个重要特征是可预测。

你看过一部分,就能合理猜到另一部分。

例如:

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 成本上升
  • 新人学习慢
  • 自动化困难
  • 测试风格混乱

最糟的是:

不一致会让系统失去可预测性。

当工程师不能预测系统会如何命名、放置、失败、返回错误时,每次改动都要重新探索。

什么时候可以打破一致性

一致性不是绝对。

可以打破一致性,但要有理由。

合理理由:

  • 领域概念真的不同
  • 安全风险更高,需要额外步骤
  • 性能要求完全不同
  • 外部协议强制
  • 迁移阶段需要兼容

不合理理由:

  • 当时随手写的
  • 每个人按自己习惯
  • 没有统一约定
  • 旧代码懒得改

打破一致性时,最好留下解释。

一致性检查清单

看一个系统是否一致,问:

  1. 相似动作是否使用相似命名?
  2. 参数顺序是否一致?
  3. 错误响应是否统一?
  4. 状态机表达方式是否一致?
  5. 目录结构是否让新增功能有自然位置?
  6. 测试位置和命名是否统一?
  7. 高风险后台操作流程是否一致?
  8. 打破一致性是否有明确理由?
  9. 新人能否通过一个模块猜到另一个模块?

小结

  1. 优雅系统可预测。
  2. 一致性不是死板,而是同类问题用同类表达。
  3. API 命名、参数顺序、错误模型、状态机、目录结构都需要一致性。
  4. 一致性减少记忆负担,也减少误用。
  5. 目录结构是系统地图。
  6. 可以打破一致性,但要有真实理由。
  7. 好系统让你看过一部分,就能合理猜到另一部分。