1.1.2 最小惊讶原则:清晰比聪明更重要
什么是"惊讶"
当一个函数、类或模块的行为与你基于其命名或接口所预期的行为不符时,你就被"惊讶"了。
最小惊讶原则(Principle of Least Astonishment)的核心是:代码应该做它看起来应该做的事情。
一个典型的"惊讶"案例
案例1:名不副实的函数
def get_user(user_id):user = db.query(User).filter_by(id=user_id).first()if not user:user = User(id=user_id, name="默认用户")db.add(user)db.commit()return user
惊讶点:这个函数叫 get_user,但它不仅"获取",还会"创建"。
当你在代码中看到:
user = get_user(123)
你会假设这是一个只读操作。但实际上,如果用户不存在,数据库会被修改。
后果:
- 在事务中使用这个函数时可能产生意外的副作用
- 在只读副本数据库上调用会失败
- 性能分析时发现"查询操作"竟然在写数据
正确做法:
def get_user(user_id):"""获取用户,如果不存在返回 None"""return db.query(User).filter_by(id=user_id).first()def get_or_create_user(user_id):"""获取用户,如果不存在则创建"""user = get_user(user_id)if not user:user = User(id=user_id, name="默认用户")db.add(user)db.commit()return user
案例2:返回值的惊讶
function findUser(userId) {const users = database.getAllUsers();for (let user of users) {if (user.id === userId) {return user;}}return {}; // 惊讶点
}// 使用时
const user = findUser(123);
if (user) { // 这个判断永远为 true!console.log(user.name); // 可能是 undefined
}
惊讶点:函数在找不到用户时返回空对象 {},而不是 null 或 undefined。
这导致调用者无法正确判断是否找到了用户,因为 {} 在 JavaScript 中是真值。
正确做法:
function findUser(userId) {const users = database.getAllUsers();for (let user of users) {if (user.id === userId) {return user;}}return null; // 明确表示"没找到"
}// 使用时
const user = findUser(123);
if (user !== null) {console.log(user.name);
}
常见的"惊讶"模式
1. 函数名说谎
| 函数名 | 你以为它做什么 | 它实际做什么 | 问题 |
|---|---|---|---|
isValid() |
检查是否有效 | 检查 + 修复 + 返回结果 | 有副作用 |
getConfig() |
获取配置 | 获取配置,不存在时读取文件并缓存 | 隐藏 I/O |
toString() |
转换为字符串 | 转换 + 上报日志 | 有副作用 |
setX() |
设置 X 的值 | 设置 X,同时重新计算 Y 和 Z | 隐藏逻辑 |
2. 参数顺序的惊讶
# 标准库的做法
def copy(src, dst):"""从 src 复制到 dst"""pass# 你的代码
def copy_file(destination, source): # 顺序反了"""从 source 复制到 destination"""pass
大多数人习惯"从哪里 到哪里"的顺序,反过来会让人困惑。
3. 隐式的全局状态修改
class UserService:current_user = None # 类变量def login(self, username, password):user = authenticate(username, password)UserService.current_user = user # 惊讶:修改了全局状态return user
调用 login() 不仅返回用户对象,还修改了类的全局状态。这在多线程环境下会导致混乱。
4. 破坏性操作伪装成安全操作
def get_sorted_users(users):"""返回排序后的用户列表"""users.sort() # 惊讶:修改了原列表return users# 使用时
original_users = [user3, user1, user2]
sorted_users = get_sorted_users(original_users)
# original_users 也被改变了!
正确做法:
def get_sorted_users(users):"""返回排序后的用户列表"""return sorted(users) # 返回新列表,不修改原列表
为什么清晰比聪明更重要
"聪明"的代码示例
def process(data):return [x for x in (y.strip() for y in data.split(',') if y) if x != 'admin']
这段代码"聪明"吗?是的,一行搞定所有逻辑。
但它做了什么?你需要花多久才能理解?
"清晰"的版本
def process(data):"""处理逗号分隔的数据,过滤空值和 admin"""items = data.split(',')items = [item.strip() for item in items if item]items = [item for item in items if item != 'admin']return items
或者更清晰:
def process(data):"""处理逗号分隔的数据,过滤空值和 admin"""items = data.split(',')items = remove_empty_strings(items)items = remove_admin_entries(items)return itemsdef remove_empty_strings(items):return [item.strip() for item in items if item]def remove_admin_entries(items):return [item for item in items if item != 'admin']
第二个版本:
- 更容易理解
- 更容易测试(每个函数可以单独测试)
- 更容易修改(如果过滤逻辑变复杂,改
remove_admin_entries即可)
代价:多了几行代码。
收益:降低了理解成本,减少了bug的可能性。
对使用 AI 的程序员的建议
AI 生成的代码往往倾向于"聪明"而非"清晰",因为:
- AI 倾向于用简洁的代码展示能力
- AI 的训练数据中包含大量"巧妙"的代码示例
实践建议
当 AI 生成代码后,问自己:
- 3个月后的我,能在30秒内理解这段代码在做什么吗?
- 如果团队里的初级开发者看到这段代码,会困惑吗?
- 这段代码的行为与它的命名/接口匹配吗?
如果答案是"不确定"或"不能",就让 AI 重写:
"请把这段代码改得更清晰,即使需要更多行数也没关系。
拆分成多个小函数,每个函数只做一件事,命名要清楚地表达意图。"
实践检查清单
写完一段代码后,检查以下几点:
1. 命名诚实性
2. 行为一致性
3. 副作用透明性
4. 可读性
真实场景:代码审查中的对话
新手:"我这个函数只用了一行,很简洁吧?"
def validate_and_save(data):return db.save(User(**{k: v for k, v in data.items() if v})) if all(k in data for k in ['name', 'email']) else None
有经验的工程师:"这个函数做了太多事情:验证、过滤、创建对象、保存。而且一行代码做这么多事,维护时很容易出错。"
重构后:
def validate_and_save(data):"""验证用户数据并保存到数据库"""if not is_valid_user_data(data):return Nonecleaned_data = remove_empty_fields(data)user = User(**cleaned_data)return db.save(user)def is_valid_user_data(data):required_fields = ['name', 'email']return all(field in data for field in required_fields)def remove_empty_fields(data):return {k: v for k, v in data.items() if v}
新手:"但这样代码变多了啊?"
有经验的工程师:"代码多了,但理解时间减少了。6个月后,你会感谢自己的。"
总结
最小惊讶原则的核心思想:
- 代码应该做它看起来应该做的事情
- 命名要诚实 —— 不要让函数名说谎
- 行为要一致 —— 遵循常见约定和模式
- 副作用要显式 —— 不要隐藏修改状态的操作
- 清晰优于聪明 —— 宁可多几行代码,也不要让人困惑
记住:代码被阅读的次数远多于被编写的次数。 你今天为了"聪明"节省的5分钟,会变成未来6个人各花20分钟理解的成本。
好的代码不是让你炫技的地方,而是让下一个维护者(很可能是6个月后的你)能快速理解的地方。