核心问题
什么叫简单?
奥卡姆剃刀的经典表达是:
如无必要,勿增实体。
映射到软件工程,就是 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 不是“少写代码”,而是:
少创造没有必要存在的概念。
一个系统里最昂贵的东西不是代码行数,而是概念数量。
因为每多一个概念,团队就要回答:
- 它和已有概念有什么区别?
- 它什么时候创建?
- 它什么时候失效?
- 谁拥有它?
- 谁可以修改它?
- 它和别的概念如何保持一致?
所以奥卡姆剃刀在工程里的用法是:
每当你想增加一个实体、状态、层、服务、接口或框架时,先问:它解决了真实复杂性,还是只是把我的焦虑实体化了?
必要实体 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>
}
这套模型很通用,但不一定适合一开始就引入。
如果当前只有三条规则:
- 讲师可以编辑自己创建的课程。
- 管理员可以编辑所有课程。
- 企业管理员可以给员工分配企业购买的课程。
更直接的代码可能更好:
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 - 多种主体都有角色继承
- 权限条件需要配置化
- 权限判断要在多个服务之间共享
- 非工程人员需要管理权限策略
这时再抽象 Policy、Permission、Role 或 RuleEngine,就有了真实理由。
抽象的成本
每个抽象都会带来成本。
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 是什么,那它大概率只是工程师自娱自乐。
奥卡姆剃刀的工程版
可以把奥卡姆剃刀翻译成几条工程判断:
- 不增加没有生命周期的实体。
- 不增加没有独立不变量的实体。
- 不增加只为未来猜测存在的层。
- 不把两个变化原因不同的东西抽到一起。
- 不为了消除表面重复而制造深层耦合。
- 不用抽象隐藏自己还没想清楚的领域边界。
简单的层级
真正的简单有层级。
局部简单
单个函数短,单个文件小。
这有价值,但不是全部。
结构简单
概念之间边界清楚,依赖方向清楚。
这比局部简单更重要。
演化简单
新需求出现时,系统知道该在哪里变。
这是最高级的简单。
一个函数短但到处 if role === ...,局部看简单,演化很痛苦。
一个模型稍微多几个类型,但每个类型边界明确,演化反而轻松。
所以 KISS 最终追求的是:
让未来的变化有地方去。
小结
- 简单不是代码少,而是概念边界清楚。
- 最贵的是概念数量,不是代码行数。
- 每个新实体都应该有独立生命周期和不变量。
- 抽象应该来自已经出现的重复,不是来自想象中的未来。
- 好抽象减少调用者需要知道的细节。
- 坏抽象会让业务迁就模型。
- KISS 的目标不是今天看起来省事,而是明天改起来不痛苦。