核心问题

一个改动应该影响多大范围?

优雅系统的一个重要特征是:

变化有地方去。

如果改一个课程访问规则,需要修改:

checkout
course page
mobile API
admin panel
notification worker
support dashboard

说明系统局部性很差。

局部性好的系统里,相关规则集中在自然边界内。

例如:

course-access/
  policy.ts
  grants.ts
  revocation.ts
  tests/

改访问规则,就主要改 course-access

核心句:

好设计让相关变化靠近,让无关变化远离。

Shotgun Surgery

局部性差的典型坏味道叫 shotgun surgery。

也就是:

一个小需求,需要在很多地方做小改动。

例如新增一种课程访问来源:兑换码。

如果你要改:

checkout service
course page API
mobile API
admin permission check
email worker
analytics job
support dashboard

说明访问权规则散落了。

更好的结构是:

canAccessCourse(accountId, courseId)

调用方只问结果,不自己拼规则。

新增兑换码时,主要扩展:

course-access/grants.ts
course-access/policy.ts
course-access/tests/

信息隐藏

局部性的基础是信息隐藏。

模块不应该暴露所有内部细节。

例如外部调用方不应该知道:

  • 访问权来自哪些来源
  • grant 如何存储
  • revokedAt 如何判断
  • subscription 是否影响 access
  • organization assignment 怎么转换成 access

调用方只需要:

canAccessCourse(accountId, courseId)

信息隐藏不是为了神秘,而是为了保护变化边界。

核心句:

你暴露给外部的每个细节,都会变成未来改动的阻力。

高内聚,低耦合

局部性常被描述为:

high cohesion, low coupling

高内聚:

相关规则放在一起。

低耦合:

不相关模块知道彼此越少越好。

例如 course-access 应该内聚:

  • 访问权来源
  • 访问权有效期
  • 撤销规则
  • 拒绝原因
  • 访问判断

course-access 不应该强耦合:

  • 邮件模板
  • 前端按钮
  • 推荐算法
  • 报表格式

Policy 集中

业务规则散落是局部性的敌人。

坏例子:

// checkout
if (purchase.status === "paid" && !purchase.refundedAt) ...

// course page
if (purchase.status === "paid" && !purchase.refundedAt) ...

// mobile API
if (purchase.status === "paid" && !purchase.refundedAt) ...

更好的方向:

canAccessCourse(accountId, courseId)

或者更细:

canGrantCourseAccessFromPurchase(purchase)
isCourseAccessGrantValid(grant, now)

规则集中后,变化范围自然缩小。

核心句:

每一条重要业务规则都应该有一个可信位置。

Data Ownership

局部性不仅是代码问题,也是数据所有权问题。

如果多个模块都直接写同一张表,局部性会崩。

例如:

billing 写 course_access_grants
admin 写 course_access_grants
organization 写 course_access_grants
support 也写 course_access_grants

很快没人知道访问权状态为什么变成这样。

更好的方式是让 course-access 拥有访问权写入入口:

grantAccessFromPurchase(purchaseId)
grantAccessFromOrganizationAssignment(assignmentId)
revokeAccessAfterRefund(refundId)
revokeAccessForSuspendedAccount(accountId)

其他模块请求它改变状态,而不是直接改它的数据。

核心句:

谁拥有规则,谁应该拥有写入口。

Locality and Tests

测试也应该体现局部性。

如果新增访问来源后,要改很多无关测试,说明边界可能太漏。

好的测试结构:

course-access/
  policy.test.ts
  grants.test.ts
  revocation.test.ts

调用方测试只验证:

when canAccessCourse returns false, course page blocks playback

不应该重复测试所有访问来源。

原则:

规则测试放在规则所属模块,调用方测试只测集成承诺。

Locality and Review

局部性好的改动更容易 review。

一个 PR 如果集中在:

course-access/

reviewer 能快速建立上下文。

如果一个 PR 横跨:

billing
course page
mobile
admin
worker
analytics

reviewer 很难判断是否遗漏。

所以局部性也是团队协作质量。

核心句:

改动越局部,review 越可信。

局部性不是孤岛

局部性不等于模块互不通信。

系统当然需要协作。

关键是协作要通过清楚契约,而不是共享内部细节。

例如:

billing -> courseAccess.grantAccessFromPurchase(purchaseId)
coursePage -> courseAccess.canAccessCourse(accountId, courseId)
support -> courseAccess.explainAccessDecision(accountId, courseId)

这里模块之间协作,但边界清楚。

坏协作是:

coursePage 直接查 purchases、subscriptions、organization_assignments、refunds

这会让 course page 复制整个访问权模型。

局部性检查清单

看一个设计是否有局部性,问:

  1. 一个规则变化需要改几个模块?
  2. 相关规则是否放在一起?
  3. 调用方是否知道太多内部细节?
  4. 是否有可信的 policy 入口?
  5. 谁拥有数据写入?
  6. 新需求是否有自然落点?
  7. 测试是否跟着规则所属模块走?
  8. PR 是否能被局部 review?
  9. 模块协作是通过契约,还是共享内部实现?

小结

  1. 局部性回答“一个改动应该影响多大范围”。
  2. 好设计让相关变化靠近,让无关变化远离。
  3. Shotgun surgery 是局部性差的典型坏味道。
  4. 信息隐藏保护变化边界。
  5. 每条重要业务规则都应该有可信位置。
  6. 谁拥有规则,谁应该拥有写入口。
  7. 改动越局部,review 越可信。
  8. 局部性不是孤岛,而是通过清楚契约协作。