1-1-4-复杂度的守恒
1.1.4 复杂度的守恒:你藏起来的复杂最终会回到别人身上
复杂度守恒定律
在软件系统中,有一个类似物理学守恒定律的原则:
复杂度是守恒的——它不会消失,只会转移。
你无法消除业务本身的复杂度,只能决定把这个复杂度放在哪里:
- 放在代码中
- 放在配置中
- 放在文档中
- 放在用户的使用流程中
- 放在运维的部署流程中
每一次"简化"都是一次转移,而不是消除。
案例1:隐藏的复杂度
场景:一个"简洁"的API
# 对外API:看起来很简单
def create_user(email):
"""创建用户,只需要提供邮箱"""
return UserService.create(email)
调用方觉得:"太方便了,只要一个参数!"
实际上,内部发生了什么:
class UserService:
@staticmethod
def create(email):
# 1. 从邮箱推断用户名
username = email.split('@')[0]
# 2. 检查用户名是否被占用,如果被占用,加数字后缀
base_username = username
counter = 1
while User.exists(username=username):
username = f"{base_username}{counter}"
counter += 1
# 3. 根据邮箱域名判断用户类型
domain = email.split('@')[1]
if domain in ['gmail.com', 'outlook.com']:
user_type = 'personal'
elif domain in ['company.com']:
user_type = 'enterprise'
else:
user_type = 'unknown'
# 4. 根据用户类型分配默认权限
if user_type == 'enterprise':
role = 'member'
else:
role = 'free_user'
# 5. 生成默认头像(基于邮箱哈希)
avatar_url = generate_avatar_from_email(email)
# 6. 发送欢迎邮件
send_welcome_email(email, username)
# 7. 创建用户
user = User(
email=email,
username=username,
user_type=user_type,
role=role,
avatar=avatar_url
)
db.save(user)
# 8. 同步到第三方系统
sync_to_crm(user)
return user
问题出现了:
情况1:用户想指定自己的用户名
调用方:"为什么我不能自定义用户名?"
你:"因为 API 设计时为了简化,自动从邮箱生成了。"
调用方:"那我现在想改怎么办?"
你:"……需要改 API,并且迁移现有数据。"
情况2:企业客户不想同步到 CRM
企业:"我们有自己的 CRM,不想同步到你们的。"
你:"这个……是自动的,没法关闭。"
企业:"那我们没法用你们的服务了。"
情况3:测试时不想发邮件
测试工程师:"我创建了 100 个测试用户,为什么发了 100 封邮件?"
你:"因为创建用户会自动发邮件……"
测试工程师:"能不能不发?"
你:"需要改代码,加一个参数控制。"
复杂度去哪了?
表面上,API 很简洁(一个参数)。但实际上:
- 调用方的复杂度:变成了"缺乏控制"的困扰
- 维护者的复杂度:变成了大量隐式逻辑和难以测试的代码
- 未来扩展的复杂度:每次改动都可能破坏现有假设
正确的做法:显式复杂度
def create_user(
email: str,
username: str = None, # 可选,不提供则自动生成
user_type: str = None, # 可选,不提供则自动推断
role: str = 'free_user', # 默认值
send_welcome_email: bool = True, # 默认发送
sync_to_crm: bool = True # 默认同步
):
"""
创建用户
Args:
email: 用户邮箱(必填)
username: 用户名(可选,不提供则从邮箱自动生成)
user_type: 用户类型(可选,不提供则根据邮箱域名推断)
role: 用户角色,默认为 'free_user'
send_welcome_email: 是否发送欢迎邮件,默认为 True
sync_to_crm: 是否同步到 CRM,默认为 True
"""
if username is None:
username = generate_username_from_email(email)
if user_type is None:
user_type = infer_user_type_from_email(email)
user = User(
email=email,
username=username,
user_type=user_type,
role=role
)
db.save(user)
if send_welcome_email:
send_welcome_email_task.delay(email, username) # 异步
if sync_to_crm:
sync_to_crm_task.delay(user.id) # 异步
return user
好处:
- 调用方有控制权:可以关闭不需要的功能
- 行为可预测:每个参数的作用都是明确的
- 易于测试:可以单独测试每个分支
- 易于扩展:新增参数不会破坏现有调用
代价:
- API 变复杂了:从1个参数变成6个参数
但这是值得的,因为:
- 复杂度被显式化了,而不是隐藏在代码深处
- 调用方可以根据需要选择复杂度,而不是被迫接受所有隐式行为
案例2:配置的复杂度转移
错误示例:过度配置化
某团队认为"代码中不应该有硬编码",于是把所有东西都放进配置文件:
# config.yaml
user:
username:
generation:
enabled: true
source: email
separator: '@'
index: 0
fallback:
enabled: true
suffix:
type: numeric
start: 1
type:
inference:
enabled: true
rules:
- domains: ['gmail.com', 'outlook.com']
type: personal
- domains: ['company.com']
type: enterprise
- domains: ['*']
type: unknown
notifications:
welcome_email:
enabled: true
async: true
template: welcome_v2
crm_sync:
enabled: true
async: true
endpoint: https://crm.internal.com/api/users
问题:
- 配置成了代码:逻辑分散在代码和配置中,难以理解
- 没有类型检查:配置错误在运行时才能发现
- 测试困难:要测试不同配置组合,需要准备多份配置文件
- 复杂度爆炸:为了"灵活性",引入了远超需要的配置项
什么时候配置是合理的?
✅ 应该配置的:
- 环境相关的值(数据库地址、API密钥)
- 部署相关的值(端口、日志级别)
- 业务规则(费率、限额)
❌ 不应该配置的:
- 程序逻辑(如何生成用户名)
- 算法细节(字符串分隔符、索引)
- 控制流(是否启用某个步骤)
原则:
- 配置应该是数据,而不是逻辑
- 如果配置项需要开发者才能理解,它应该在代码里
案例3:框架的复杂度转移
Rails 的"魔法"
Ruby on Rails 曾以"约定优于配置"著称,用"魔法"隐藏了大量复杂度:
# 你只需要写这么多
class User < ApplicationRecord
has_many :posts
end
# Rails 自动提供了
user.posts # 获取用户的所有文章
user.posts.create # 创建新文章
user.posts.count # 统计文章数量
# ... 还有几十个方法
好处:代码简洁,上手快。
代价:
- 调试困难:出问题时,你不知道哪一层出了问题
- 性能陷阱:
user.posts.count可能触发 N+1 查询 - 学习曲线陡峭:需要记住大量"约定"
- 定制困难:当需要偏离约定时,反而更复杂
对比:显式的 ORM
# Django ORM(更显式)
class User(models.Model):
pass
class Post(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
# 使用
user.post_set.all() # 明确是一个查询
user.post_set.count() # 明确是一个聚合
虽然没有 Rails 简洁,但:
- 行为更明确:一眼看出会触发什么查询
- 调试更容易:问题出在哪一层很清楚
- 学习成本更低:少了很多隐式约定
复杂度应该放在哪里?
一个通用的决策框架:
1. 放在调用方(API 参数)
适用于:
- 需要根据场景变化的行为
- 不同调用方有不同需求
示例:
def search_users(keyword, include_deleted=False, limit=10):
...
2. 放在代码中(函数/类)
适用于:
- 核心业务逻辑
- 不太可能改变的规则
- 需要类型检查和单元测试的逻辑
示例:
def calculate_order_total(items):
subtotal = sum(item.price * item.quantity for item in items)
tax = subtotal * 0.1 # 税率是业务规则,放在代码里
return subtotal + tax
3. 放在配置中(配置文件)
适用于:
- 环境相关的值
- 业务参数(可能需要运营调整的值)
示例:
pricing:
tax_rate: 0.1
discount_threshold: 1000
database:
host: localhost
port: 5432
4. 放在数据库中(动态配置)
适用于:
- 需要在运行时修改的规则
- 需要审计的配置
示例:
# 促销规则存在数据库
class PromotionRule(models.Model):
product_category = models.CharField()
discount_rate = models.DecimalField()
start_date = models.DateField()
end_date = models.DateField()
5. 放在用户界面(交给用户决定)
适用于:
- 个性化选项
- 不确定哪种方式更好的场景
示例:
- 邮件通知开关(让用户在设置中决定)
- 排序方式(让用户在UI中选择)
对使用 AI 的程序员的建议
AI 倾向于生成"高度抽象"的代码,把复杂度隐藏在层层封装之下。
警惕信号
当 AI 生成的代码有以下特征时,要特别小心:
- 层次很深:调用链超过 3 层
- 有很多"默认行为":函数在你不知道的情况下做了很多事
- 有很多"魔法":自动推断、自动转换、自动同步
改进方法
向 AI 提问时,强调显式性:
❌ "帮我实现一个用户注册功能"
✅ "帮我实现一个用户注册功能,要求:
- 每个步骤都是显式的,不要隐藏任何操作
- 不要自动发送邮件或同步到第三方服务
- 所有副作用都应该是可选的,通过参数控制"
检查清单:复杂度是否被合理放置
完成一段代码后,检查:
如果有任何一项答案是"是"或"不能",考虑重构,让复杂度显式化。
真实案例:Kubernetes 的复杂度转移
Kubernetes 是一个经典的"复杂度转移"案例:
它解决了什么复杂度?
- 部署的复杂度
- 扩展的复杂度
- 故障恢复的复杂度
它引入了什么新复杂度?
- 学习 K8s 的复杂度
- 配置 YAML 的复杂度
- 调试分布式系统的复杂度
- 运维 K8s 集群的复杂度
结论:
- 对于大规模系统,这个交易是值得的
- 对于小团队(<10人)或简单应用,可能不值得
教训:
- 复杂度的转移不是免费的
- 要清楚你在用一种复杂度交换另一种复杂度
- 确保这个交易对你的场景是划算的
总结
复杂度守恒定律的启示:
- 复杂度无法消除,只能转移
- 隐式复杂度比显式复杂度更危险 —— 它会在意想不到的时候爆炸
- 好的设计是让复杂度在正确的地方 —— 不是隐藏它,而是让它可控
- 显式的复杂度比简洁的接口更重要 —— 宁可多几个参数,也不要隐藏行为
- 配置不是银弹 —— 过度配置化会让复杂度从代码转移到配置,但总量不变
记住:
当你觉得某段代码"太简洁了,太优雅了"时,问自己:复杂度去哪了?
它可能:
- 转移到了调用方(调用方需要做更多工作)
- 转移到了配置(配置变得复杂难懂)
- 转移到了运维(部署和监控变得困难)
- 转移到了未来(技术债)
好的工程师不是会消除复杂度的人,而是会正确分配复杂度的人。

浙公网安备 33010602011771号