你需要让你的代码比使用它的人更可靠

发布: (2025年12月11日 GMT+8 05:35)
8 min read
原文: Dev.to

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,而是为人类构建软件的现实。

问题不在于用户会不会做出意料之外的操作,而在于:当他们这么做时,你的代码能否优雅地处理,还是会导致系统崩溃?

让你的代码比使用它的人更可靠。你的未来的自己(以及值班轮值)会感激你的。

Back to Blog

相关文章

阅读更多 »