核心问题

什么叫简单?

奥卡姆剃刀的经典表达是:

如无必要,勿增实体。

映射到软件工程,就是 KISS:

Keep It Simple, Stupid.

但这里的“简单”经常被误解。

简单不是:

  • 文件少
  • 类少
  • 代码短
  • 所有逻辑塞进一个函数
  • 所有角色塞进一个 User

真正的简单是:

系统里的概念刚好够用,没有多余实体,也没有把不同东西硬塞成一个东西。

两种假简单

1. 贫血式简单

例如:

type User = {
  id: string
  role: string
  status: string
  metadata: Record<string, unknown>
}

这看起来很简单,因为只有一个类型。

但问题是,复杂性没有消失,只是被赶进了运行时:

if (user.role === "student") {
  // ...
}

if (user.metadata.instructorBio) {
  // ...
}

if (user.status === "active" && user.metadata.subscriptionEndsAt) {
  // ...
}

这种简单是假的。它减少了类型数量,却增加了判断分支、隐式约定和数据不一致。

2. 炫技式复杂

另一种极端是过度设计:

abstract class BaseActor<TIdentity, TRole, TPermission> {
  abstract resolveIdentity(): TIdentity
  abstract resolveRole(): TRole
  abstract resolvePermissions(): TPermission[]
}

这看起来很有架构感,但如果系统现在只需要课程购买、课程学习和讲师发布课程,那么这个抽象很可能过早。

它的问题是:

  • 需求还没有稳定,抽象先稳定了。
  • 业务语言还没有长出来,框架语言先盖上去了。
  • 调用者为了用抽象,必须学习一套额外概念。

这就是过度设计。

KISS 的真正含义

KISS 不是“少写代码”,而是:

少创造没有必要存在的概念。

一个系统里最昂贵的东西不是代码行数,而是概念数量。

因为每多一个概念,团队就要回答:

  1. 它和已有概念有什么区别?
  2. 它什么时候创建?
  3. 它什么时候失效?
  4. 谁拥有它?
  5. 谁可以修改它?
  6. 它和别的概念如何保持一致?

所以奥卡姆剃刀在工程里的用法是:

每当你想增加一个实体、状态、层、服务、接口或框架时,先问:它解决了真实复杂性,还是只是把我的焦虑实体化了?

必要实体 vs 想象实体

回到在线课程平台。

这些实体通常是必要的:

Account
Course
Purchase
Enrollment
InstructorProfile
OrganizationMembership

因为它们对应真实事实或真实生命周期。

但这些实体可能是想象出来的:

LearningActor
CourseRelation
UniversalRole
AbstractPermissionSubject
EnterpriseLearningContextManager

它们也许未来会有用,但现在未必有足够证据。

判断标准不是“这个名字高级不高级”,而是:

如果删除这个概念,系统是否无法表达当前真实规则?

如果答案是否定的,就先不要加。

一个具体例子:权限系统

很多系统很早就会设计一个万能权限系统:

type Permission = {
  subjectType: string
  subjectId: string
  action: string
  resourceType: string
  resourceId: string
  condition?: Record<string, unknown>
}

这套模型很通用,但不一定适合一开始就引入。

如果当前只有三条规则:

  1. 讲师可以编辑自己创建的课程。
  2. 管理员可以编辑所有课程。
  3. 企业管理员可以给员工分配企业购买的课程。

更直接的代码可能更好:

function canEditCourse(accountId: string, courseId: string): boolean {
  return isCourseOwner(accountId, courseId) || isPlatformAdmin(accountId)
}

function canAssignCourse(
  accountId: string,
  organizationId: string,
  courseId: string
): boolean {
  return (
    isOrganizationManager(accountId, organizationId) &&
    organizationHasCourseAccess(organizationId, courseId)
  )
}

这并不低级。它很诚实。

等到规则开始出现稳定重复,比如:

  • 多种资源都有 view/edit/delete
  • 多种主体都有角色继承
  • 权限条件需要配置化
  • 权限判断要在多个服务之间共享
  • 非工程人员需要管理权限策略

这时再抽象 PolicyPermissionRoleRuleEngine,就有了真实理由。

抽象的成本

每个抽象都会带来成本。

1. 认知成本

别人必须先理解抽象,才能理解业务。

例如:

policyEvaluator.evaluate(subject, action, resource, context)

不如:

canEditCourse(accountId, courseId)

前者通用,后者直接。

当规则少的时候,直接比通用更简单。

2. 间接成本

抽象会拉长理解路径。

为了弄懂一次课程访问判断,你可能要跳过:

Controller -> Service -> PolicyEvaluator -> RuleRegistry -> ConditionMatcher -> DataProvider

如果系统规模还小,这种间接性不是架构,而是负担。

3. 适配成本

抽象一旦建立,后来的需求会被迫适配它。

坏抽象最危险的地方是:

它会让真实业务为了迁就旧模型而变形。

例如所有东西都被迫塞进:

subject-action-resource

但有些规则其实是流程规则、额度规则、时间规则、组织规则,不一定适合这个结构。

什么时候应该增加抽象

可以用四个信号判断。

1. 重复已经出现,而不是你预测它会出现

不要因为“以后可能会有很多种课程访问来源”,就立刻做万能访问系统。

等到真的有:

  • 购买访问
  • 订阅访问
  • 企业分配访问
  • 兑换码访问

再抽象 CourseAccessGrant

2. 重复背后有同一个变化轴

重复本身不够。要找到变化轴。

例如多种访问来源的共同点是:

它们都会产生“某账号在某时间范围内可访问某课程”的效果。

所以抽象的是:

type CourseAccessGrant = {
  accountId: string
  courseId: string
  validFrom: Date
  validUntil?: Date
  source: "purchase" | "subscription" | "organization_assignment" | "coupon"
}

不是抽象成:

UniversalCourseRelationship

3. 抽象能减少规则分叉

好的抽象会让调用方少知道细节。

例如:

canAccessCourse(accountId, courseId)

调用者不需要知道访问权来自购买、订阅还是企业分配。

这是好抽象。

4. 抽象能表达领域语言

抽象不是为了让代码看起来高级,而是为了让业务语言更清楚。

如果业务里真的有一个稳定概念叫“课程访问权”,那么 CourseAccessGrant 是好名字。

如果业务里没人知道 AbstractCourseRelationManager 是什么,那它大概率只是工程师自娱自乐。

奥卡姆剃刀的工程版

可以把奥卡姆剃刀翻译成几条工程判断:

  1. 不增加没有生命周期的实体。
  2. 不增加没有独立不变量的实体。
  3. 不增加只为未来猜测存在的层。
  4. 不把两个变化原因不同的东西抽到一起。
  5. 不为了消除表面重复而制造深层耦合。
  6. 不用抽象隐藏自己还没想清楚的领域边界。

简单的层级

真正的简单有层级。

局部简单

单个函数短,单个文件小。

这有价值,但不是全部。

结构简单

概念之间边界清楚,依赖方向清楚。

这比局部简单更重要。

演化简单

新需求出现时,系统知道该在哪里变。

这是最高级的简单。

一个函数短但到处 if role === ...,局部看简单,演化很痛苦。

一个模型稍微多几个类型,但每个类型边界明确,演化反而轻松。

所以 KISS 最终追求的是:

让未来的变化有地方去。

小结

  1. 简单不是代码少,而是概念边界清楚。
  2. 最贵的是概念数量,不是代码行数。
  3. 每个新实体都应该有独立生命周期和不变量。
  4. 抽象应该来自已经出现的重复,不是来自想象中的未来。
  5. 好抽象减少调用者需要知道的细节。
  6. 坏抽象会让业务迁就模型。
  7. KISS 的目标不是今天看起来省事,而是明天改起来不痛苦。