核心问题
一个改动应该影响多大范围?
优雅系统的一个重要特征是:
变化有地方去。
如果改一个课程访问规则,需要修改:
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 复制整个访问权模型。
局部性检查清单
看一个设计是否有局部性,问:
- 一个规则变化需要改几个模块?
- 相关规则是否放在一起?
- 调用方是否知道太多内部细节?
- 是否有可信的 policy 入口?
- 谁拥有数据写入?
- 新需求是否有自然落点?
- 测试是否跟着规则所属模块走?
- PR 是否能被局部 review?
- 模块协作是通过契约,还是共享内部实现?
小结
- 局部性回答“一个改动应该影响多大范围”。
- 好设计让相关变化靠近,让无关变化远离。
- Shotgun surgery 是局部性差的典型坏味道。
- 信息隐藏保护变化边界。
- 每条重要业务规则都应该有可信位置。
- 谁拥有规则,谁应该拥有写入口。
- 改动越局部,review 越可信。
- 局部性不是孤岛,而是通过清楚契约协作。