你需要让你的代码比使用它的人更可靠
Source: Dev.to
用户行为的现实
用户不会阅读文档。他们也不会仔细查看错误信息。用户可能分心、匆忙,或者根本没有技术背景来理解你的应用程序的期望。这完全没问题——让用户去适配你的代码并不是他们的工作。让代码去适配用户才是你的职责。
考虑以下真实场景:
- 用户从 PDF 中复制文本,里面包含不可见的 Unicode 字符
- 某人在交易进行中网络掉线
- 用户在你的异步操作仍在运行时离开页面
- 用户提交表单后点击“返回”
- 用户连续三天保持会话打开
- 同时打开多个标签页使用你的应用
如果这些情形中的任意一种会导致你的应用崩溃或数据损坏,那不是用户的问题,而是代码可靠性的问题。
可靠性到底意味着什么
可靠的代码不仅仅是在理想条件下能工作。它应当:
- 严格验证 – 永远不要信任输入,即使是来自你自己的前端
- 优雅失败 – 出错时降级功能而不是直接崩溃
- 处理边缘情况 – 99 % 的情况固然重要,但 1 % 的情况才是 bug 的温床
- 保持数据完整性 – 无论用户怎么操作,数据始终保持一致
- 自动恢复 – 在可能的情况下自行修复问题,无需用户介入
不可靠代码的代价
我曾在一个金融应用上工作,那里出现了一个竞争条件,导致用户可以双提交电汇。这个 bug 很少出现——需要在 200 ms 内点击两次提交。“用户不会这么做”,我们当时这么想。但在成千上万的用户中,“罕见”每天都会发生。每一起事件都需要人工干预、客服电话,甚至可能产生金融责任。
修复只用了两个小时:在第一次点击时禁用按钮并加入幂等键。若一开始就实现这些,成本会是多少?数百小时的支持工时以及受损的客户信任。
构建可靠代码的策略
1. 无处不在的输入验证
// Bad: Trust the frontend
function createUser(data) {
return database.users.insert(data);
}
// Good: Validate everything
function createUser(data) {
const validated = userSchema.parse(data); // Throws on invalid data
const sanitized = sanitizeInput(validated);
return database.users.insert(sanitized);
}
永远不要假设输入是干净的,即使来自自己的 UI。浏览器可以被篡改,API 可以直接调用,中间件也可能失效。
2. 所有状态变更都要幂等
# Bad: Can create duplicates
def submit_order(user_id, items):
order = Order.create(user_id=user_id, items=items)
return order
# Good: Uses idempotency key
def submit_order(user_id, items, idempotency_key):
existing = Order.find_by_idempotency_key(idempotency_key)
if existing:
return existing
order = Order.create(
user_id=user_id,
items=items,
idempotency_key=idempotency_key
)
return order
每一个会改变状态的操作都应当是幂等的——多次执行的结果应等同于执行一次的结果。
3. 防御式数据库操作
-- Bad: Assumes the record exists
UPDATE users SET balance = balance - 100 WHERE id = ?;
-- Good: Ensures constraints are maintained
UPDATE users
SET balance = balance - 100
WHERE id = ? AND balance >= 100;
-- Then check affected rows to ensure it succeeded
4. 超时与熔断器
// Bad: Wait forever for a response
const response = await fetch(externalAPI);
// Good: Fail fast with timeout
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(externalAPI, { signal: controller.signal });
return response;
} catch (error) {
if (error.name === 'AbortError') {
// Handle timeout gracefully
return fallbackResponse();
}
throw error;
} finally {
clearTimeout(timeout);
}
5. 限流与资源保护
from functools import wraps
from flask import request
import time
def rate_limit(max_calls, time_window):
calls = {}
def decorator(f):
@wraps(f)
def wrapped(*args, **kwargs):
user_id = get_current_user_id()
now = time.time()
# Clean old entries
calls[user_id] = [t for t in calls.get(user_id, [])
if now - t = max_calls:
return {"error": "Rate limit exceeded"}, 429
calls[user_id].append(now)
return f(*args, **kwargs)
return wrapped
return decorator
用户会不小心(甚至故意)频繁请求你的接口。你的代码必须自行保护。
思维方式的转变
构建可靠代码需要从“这应该能工作”转向“它可能会怎么失败?”的思考方式。开始提出以下问题:
- 如果这段代码耗时 10 秒而不是 100 毫秒会怎样?
- 如果这个函数被传入
null会怎样? - 如果两位用户在同一时刻执行相同操作会怎样?
- 如果网络在中途断开会怎样?
- 如果外部服务宕机会怎样?
- 如果有人给我发送一个 100 MB 的字符串会怎样?
可靠性测试
单元测试固然重要,但它们很少能捕捉到可靠性问题。你还需要:
- 混沌测试 – 随机终止进程、模拟网络故障
- 负载测试 – 看在高压下会出现哪些破坏
- 模糊测试 – 发送随机垃圾输入并观察行为
- 并发测试 – 同时运行多个实例
- 时光旅行测试 – 将系统时钟设为极端情况进行测试
收获
是的,构建可靠代码在前期会花更多时间。你需要编写更多的验证逻辑、错误处理和防御性检查。但回报是巨大的:
- 更少的生产事故和凌晨 3 点的紧急召唤
- 降低支持成本
- 增强用户信任
- 减少维护费用
- 夜里睡得更安稳
结论
你的用户会犯错。他们会遇到网络问题、浏览器怪癖以及时序问题。他们会以你从未预料的方式使用你的应用。这不是用户的 bug,而是为人类构建软件的现实。
问题不在于用户会不会做出意料之外的操作,而在于:当他们这么做时,你的代码能否优雅地处理,还是会导致系统崩溃?
让你的代码比使用它的人更可靠。你的未来的自己(以及值班轮值)会感激你的。