练习目标

把一个“能跑但别扭”的课程平台模块,整理成更优雅的结构。

这一节重点不是重新讲本体论,而是应用第四章的美学标准:

  • 简洁:少歧义
  • 一致:少意外
  • 局部:少牵连
  • 可组合:部件能自然拼接
  • 概念完整性:整体像一个系统
  • Flow:改起来顺手
  • 品味:提前感到未来会疼

起点:一个别扭系统

假设系统里有这些文件:

services/
  UserService.ts
  CourseManager.ts
  PaymentProcessor.ts
utils/
  accessHelper.ts
  roleUtils.ts
api/
  course.ts
  admin.ts
workers/
  emailWorker.ts

里面散落着这样的逻辑:

if (
  user.role === "student" &&
  user.status === "active" &&
  user.purchasedCourseIds.includes(course.id)
) {
  return true
}

另一个地方:

if (
  user.isAdmin ||
  user.role === "instructor" ||
  user.assignedCourseIds.includes(course.id)
) {
  allowAccess()
}

后台里:

if (admin.role === "super_admin") {
  course.status = "active"
}

邮件 worker 里:

if (user.courseIds.includes(course.id)) {
  sendWelcomeEmail(user.email)
}

这个系统可能能跑,但它不优雅。

它为什么别扭

1. 名字太宽

UserService
CourseManager
accessHelper
roleUtils

这些名字没有告诉我们边界。

2. 规则散落

课程访问规则出现在 API、后台、worker、utils 里。

改访问规则会牵动很多地方。

3. 概念混用

role
isAdmin
assignedCourseIds
purchasedCourseIds
courseIds

这些字段把身份、权限、访问权、购买事实混在一起。

4. 状态命名不一致

user.status = active
course.status = active
subscription.status = active

每个 active 含义都不同。

5. 调用方知道太多

每个调用方都要知道如何判断课程访问权。

这说明缺少可信边界。

目标结构

整理后的模块可以像这样:

modules/
  accounts/
    accounts.ts
    credentials.ts
  organizations/
    memberships.ts
    invitations.ts
  course-catalog/
    courses.ts
    publication.ts
  course-access/
    grants.ts
    policy.ts
    revocation.ts
    explanation.ts
    tests/
  learning/
    enrollments.ts
    progress.ts
  billing/
    purchases.ts
    refunds.ts
    subscriptions.ts
    outbox.ts
  notifications/
    notifications.ts
  audit/
    audit-log.ts

这个结构不一定适合所有项目,但它展示了几个美学目标:

  • 名字更具体
  • 规则有归属
  • 访问权有专门模块
  • billing 不直接拥有 access 规则
  • notification 只响应事件
  • audit 是显式能力

第一步:建立统一入口

先不要大改数据模型。

先把散落判断收进统一函数:

async function canAccessCourse(accountId: AccountId, courseId: CourseId) {
  // 暂时内部仍然可以调用旧逻辑
}

然后让调用方逐步改成:

const decision = await canAccessCourse(accountId, courseId)

而不是自己判断:

user.purchasedCourseIds.includes(course.id)

美学收益:

  • 调用方更简洁
  • 访问规则开始局部化
  • 后续重构有边界

第二步:统一返回模型

不要只返回 boolean。

访问判断应该可解释:

type AccessDecision =
  | { allowed: true; reason: "valid_grant" }
  | {
      allowed: false
      reason:
        | "no_active_grant"
        | "account_suspended"
        | "course_archived"
    }

这样 API、后台、客服、日志都可以复用同一个 decision。

美学收益:

  • 错误模型一致
  • 调试路径清楚
  • 用户提示和客服解释更稳定

第三步:把规则移到 course-access

目标:

modules/course-access/
  policy.ts
  grants.ts
  revocation.ts
  explanation.ts

policy.ts

canAccessCourse(accountId, courseId)

grants.ts

grantAccessFromPurchase(purchaseId)
grantAccessFromSubscription(subscriptionId)
grantAccessFromOrganizationAssignment(assignmentId)

revocation.ts

revokeAccessAfterRefund(refundId)
revokeAccessForSuspendedAccount(accountId)

explanation.ts

explainCourseAccess(accountId, courseId)

美学收益:

  • 相关规则靠近
  • 新增访问来源有自然落点
  • support dashboard 可以复用 explanation

第四步:整理命名

把含糊命名替换成明确概念。

UserService -> accounts / profiles / memberships
CourseManager -> course-catalog / course-access / learning
accessHelper -> course-access/policy
roleUtils -> organizations/memberships or permissions

把状态名具体化:

user.status -> AccountStatus
course.status -> CoursePublicationStatus
subscription.status -> SubscriptionStatus
enrollment.status -> EnrollmentStatus

美学收益:

  • 少歧义
  • 少误用
  • 更容易形成概念完整性

第五步:统一事件和副作用

购买成功后,不要让 billing 直接发送所有通知、更新所有报表。

更优雅:

billing creates purchase
  -> course-access grants access
  -> outbox emits course.purchase.completed
  -> notifications sends email
  -> analytics updates projection
  -> audit records event

这不是为了上复杂事件架构。

而是让副作用有秩序。

美学收益:

  • billing 不知道所有下游
  • 副作用可追踪
  • 失败可重试
  • 模块更可组合

第六步:统一后台操作流程

后台操作不要各自为政。

例如课程下架、手动撤销访问权、手动退款,都应该有一致流程:

权限检查
  -> 展示影响范围
  -> reason 必填
  -> 二次确认
  -> 执行 command
  -> audit log
  -> 返回可追踪结果

这让系统既更安全,也更优雅。

第七步:改善开发 flow

整理后,新增“兑换码访问”应该是自然路径:

modules/course-access/grants.ts
  -> add source coupon

modules/course-access/policy.ts
  -> include valid coupon grant

modules/course-access/tests/
  -> add coupon access tests

support dashboard
  -> automatically shows grant source

如果这个需求不再需要全局搜索和到处 patch,说明美学改善成功。

最终结构的美学检查

整理后问:

  1. 名字是否更具体?
  2. 访问规则是否更局部?
  3. API 是否更一致?
  4. 错误和 reason 是否统一?
  5. 新访问来源是否有自然落点?
  6. 调用方是否知道更少内部细节?
  7. 测试是否更贴近规则边界?
  8. 后台操作是否更可追溯?
  9. 新人能否通过目录结构理解系统?

小结

  1. 别扭系统常见问题是名字宽、规则散、状态混、调用方知道太多。
  2. 优雅重构可以先从统一入口开始,不必大爆炸。
  3. canAccessCourse 这类函数能把规则收进可信边界。
  4. 返回 decision 而不是 boolean,会改善解释、调试和客服体验。
  5. 目录结构应该让新需求有自然落点。
  6. 副作用要有秩序,不要散在主流程里。
  7. 美学改善的最终证据是:下一次变化更顺手。