1-2-3-代码的记忆性

1.2.3 代码的记忆性:系统不会忘记你的任何一次妥协

引言

大脑会忘记,文档会过时,团队会换人。

但代码有完美的记忆。

你三年前为了赶项目上线做的临时妥协,今天仍然在系统里运行。你当时想的"以后再改",可能永远不会被改。

系统不会忘记你的任何一次妥协,它会记住,并让你在未来付出代价。

代码的记忆特性

1. 代码会记住你的每一个决策

# 2020年,你写下这段代码
def calculate_shipping_cost(order):
    # TODO: 临时硬编码,等API ready后改
    if order.country == 'US':
        return 10
    else:
        return 20

2023年,这段代码还在运行。

为什么?

  • 功能"能用",没人有动力改
  • 新人不知道这是"临时方案"
  • 负责的人早已离职
  • 真正的 API 可能开发了,但没人知道应该替换这里

系统记住了你当初的妥协,并把它当作"正式方案"运行了三年。

2. 代码会记住你的每一个假设

// 你在2019年写的代码
function getUserName(user) {
    // 假设所有用户都有 firstName 和 lastName
    return `${user.firstName} ${user.lastName}`;
}

2021年,产品需求变了

  • 支持单一名字的用户(如印尼用户)
  • 支持企业用户(没有姓名,只有公司名)
  • 支持匿名用户

但代码还记得 2019 年的假设:所有用户都有 firstNamelastName

结果:

  • 调用这个函数的地方有 200 处
  • 每次调用前都需要判断 user.firstName 是否存在
  • 或者,函数返回 undefined undefined

系统记住了你当初的假设,并用这个假设限制了未来的扩展。

3. 代码会记住你的每一个Bug修复

# 2020年:原始代码
def send_email(user, subject, body):
    email_service.send(user.email, subject, body)

# 2021年:修复了一个 bug
def send_email(user, subject, body):
    # Fix: 某些用户的邮箱字段可能为空
    if user.email:
        email_service.send(user.email, subject, body)

# 2022年:又修复了一个 bug
def send_email(user, subject, body):
    if user.email:
        # Fix: 某些用户的邮箱格式不正确
        if '@' in user.email:
            email_service.send(user.email, subject, body)

# 2023年:再修一个 bug
def send_email(user, subject, body):
    if user.email:
        if '@' in user.email:
            # Fix: 某些用户禁用了邮件通知
            if user.email_notifications_enabled:
                email_service.send(user.email, subject, body)

系统记住了每一次修复,函数越来越复杂。

但没人知道:

  • 哪些判断是必要的
  • 为什么要有这些判断
  • 能否简化

结果:函数成了"屎山",没人敢动。

临时方案的永久性

"临时方案"是最持久的方案

有一个软件工程的笑话:

没有什么比临时方案更持久的了。

为什么?

1. "能用"的惯性

# 你写的临时方案
def get_user_permissions(user_id):
    # TEMP: 先硬编码,等权限系统上线后改
    if user_id in [1, 2, 3]:  # 管理员ID
        return ['admin']
    else:
        return ['user']

三年后,权限系统早已上线,但这段代码还在跑。

因为:

  • 它"能用"
  • 改它需要测试
  • 测试需要时间
  • 项目经理说"先做新功能,这个不紧急"

"临时"变成了"永久"。

2. 知识的流失

6 个月后

  • 你忘了这是临时方案
  • 新人以为这是正式方案
  • 代码审查时没人提出质疑

1 年后

  • 你离职了
  • 新人基于这个"临时方案"写了更多代码

3 年后

  • 没人知道这是临时的
  • 改它的成本已经高到没人愿意改

案例:Win32 API 的向后兼容

Windows API 中有无数历史遗留的"临时方案"。

GetWindowTextLengthA 函数:

  • 在 Windows 95 时代添加
  • 原本是"临时"支持 ANSI 字符串
  • Unicode 版本(GetWindowTextLengthW)才是"正式"版本

但 2024 年,30年后,GetWindowTextLengthA 仍然存在。

为什么不删除?

  • 有成千上万的程序依赖它
  • 删除它会破坏向后兼容性
  • 微软不得不永久维护这个"临时"API

教训:一旦代码被发布,它就可能永远无法被删除。

妥协的累积效应

小妥协 → 大问题

单个妥协可能无害,但妥协会累积:

# 妥协1:硬编码配置(2020年)
MAX_UPLOAD_SIZE = 10 * 1024 * 1024  # 10MB

# 妥协2:特殊处理某个客户(2021年)
def get_max_upload_size(user):
    if user.email.endswith('@bigclient.com'):
        return 50 * 1024 * 1024  # 特殊客户允许 50MB
    return MAX_UPLOAD_SIZE

# 妥协3:再特殊处理一个客户(2022年)
def get_max_upload_size(user):
    if user.email.endswith('@bigclient.com'):
        return 50 * 1024 * 1024
    if user.email.endswith('@enterprise.com'):
        return 100 * 1024 * 1024
    return MAX_UPLOAD_SIZE

# 妥协4:VIP用户也要特殊处理(2023年)
def get_max_upload_size(user):
    if user.email.endswith('@bigclient.com'):
        return 50 * 1024 * 1024
    if user.email.endswith('@enterprise.com'):
        return 100 * 1024 * 1024
    if user.is_vip:
        return 200 * 1024 * 1024
    return MAX_UPLOAD_SIZE

4 年后,这个函数已经无法维护了。

正确的做法应该是:在第一次妥协时就设计一个合理的权限系统。

但没人在第一次妥协时这么想,大家都觉得"就这一次"。

技术债的复利效应

技术债和金融债务类似,会产生"利息"。

第1年

  • 你偷了一个懒,硬编码了一个值
  • 成本:0

第2年

  • 需求变更,你又加了一个硬编码
  • 成本:5 分钟

第3年

  • 需求又变,你需要找到所有硬编码的地方
  • 成本:1 小时

第4年

  • 硬编码已经分散在 20 个文件中
  • 需要改成可配置的
  • 成本:3 天重构 + 2 天测试

如果第1年就做对,成本:30 分钟。

拖了4年,成本:5 天。

这就是技术债的复利。

如何对抗代码的记忆性

1. 不要写"临时"代码

如果你发现自己在想:

"先这样临时处理,以后再改。"

停下来,问自己:

  • "以后"是什么时候? 如果没有明确的时间表,就没有"以后"。
  • 谁会来改? 如果不是你自己,可能永远不会被改。
  • 改它的动力是什么? 如果"能用",就没人有动力改。

更好的选择

要么现在就做对,要么接受它会永久存在的事实。

如果确实没时间做对:

def get_user_permissions(user_id):
    # WARNING: 这是临时方案,硬编码了管理员ID
    # TODO: 在 2024-Q1 迁移到权限系统(负责人:Alice)
    # Tracking issue: #1234
    if user_id in [1, 2, 3]:
        return ['admin']
    else:
        return ['user']

至少:

  • 明确这是临时的
  • 有具体的计划
  • 有负责人
  • 有 issue 追踪

2. 记录你的假设

如果代码基于某个假设:

def calculate_price(items):
    # 假设:所有商品价格 > 0
    # 假设:所有商品都有 price 字段
    # 假设:不会有折扣或优惠券(暂不支持)
    return sum(item['price'] * item['quantity'] for item in items)

当假设不再成立时,这段代码需要重写。

更好的做法:用断言(assertion)强制假设:

def calculate_price(items):
    total = 0
    for item in items:
        assert 'price' in item, "Item must have a price"
        assert item['price'] > 0, "Price must be positive"
        total += item['price'] * item['quantity']
    return total

当假设被违反时,程序会立即崩溃,强制你修复。

而不是默默地产生错误结果,让 bug 潜伏几个月。

3. 定期清理技术债

建立技术债清单

# Technical Debt

## High Priority
- [ ] 将硬编码的权限逻辑迁移到权限系统 (#1234)
- [ ] 移除临时的邮件发送逻辑 (#1235)

## Medium Priority
- [ ] 重构 `calculate_price` 函数,支持优惠券 (#1236)

## Low Priority
- [ ] 统一错误处理方式 (#1237)

每个 Sprint 分配时间还债

  • 不要把 100% 的时间都用在新功能上
  • 分配 10-20% 的时间还技术债
  • 否则债务会越积越多

4. 用类型系统强制正确性

弱类型的风险

def send_email(user, subject, body):
    # 假设 user.email 存在且格式正确
    email_service.send(user.email, subject, body)

强类型的保护

from typing import Optional

class User:
    email: Optional[str]  # 明确表示 email 可能为空

def send_email(user: User, subject: str, body: str) -> bool:
    if user.email is None:
        return False
    if not is_valid_email(user.email):
        return False
    email_service.send(user.email, subject, body)
    return True

类型系统会强制你处理 email 可能为空的情况。

5. 写测试固化预期

测试是另一种"记忆"——它记住了代码应该做什么。

def test_calculate_price():
    items = [
        {'price': 10, 'quantity': 2},
        {'price': 5, 'quantity': 3}
    ]
    assert calculate_price(items) == 35

def test_calculate_price_with_zero_quantity():
    items = [{'price': 10, 'quantity': 0}]
    assert calculate_price(items) == 0

def test_calculate_price_with_negative_price_raises_error():
    items = [{'price': -10, 'quantity': 1}]
    with pytest.raises(AssertionError):
        calculate_price(items)

当你修改代码时,测试会告诉你是否破坏了原有的预期。

对使用 AI 的程序员的建议

AI 不会记得项目的历史:

  • 哪些是临时方案
  • 哪些假设已经过时
  • 哪些代码应该被重构

AI 倾向于延续现有模式

如果你的代码库中有临时方案:

# 你的旧代码
if user_id in [1, 2, 3]:
    return ['admin']

AI 可能会模仿这种模式:

# AI 生成的代码
if product_id in [100, 101, 102]:  # 特殊产品
    return apply_discount(price, 0.5)

结果:AI 帮你传播了糟糕的模式。

如何避免

  1. 清理代码库后再让 AI 学习

    • 不要让 AI 基于充满技术债的代码生成新代码
    • 先重构关键模块,建立好的模式
  2. 明确告诉 AI 避免什么

    请帮我实现用户权限检查,要求:
    - 不要硬编码用户ID
    - 使用数据库中的权限表
    - 参考 auth/permissions.py 中的模式
    
  3. 审查 AI 生成的代码

    • 检查是否有硬编码
    • 检查是否有隐式假设
    • 检查是否会产生技术债

真实案例

Y2K 问题(千年虫)

1960-1990年代,为了节省存储空间,程序员用两位数字表示年份:

YEAR = 99  * 表示 1999年

这是当时的"临时方案"

  • 存储空间很贵
  • "反正 2000 年之前肯定会升级系统"

结果

  • 2000 年临近时,全世界的系统都面临崩溃风险
  • 花费数千亿美元修复
  • 有些系统到今天还有这个问题

教训

  • "临时方案"会活得比你想象的长
  • 假设未来会解决的问题,往往不会被解决

PHP 的历史包袱

PHP 语言有很多历史遗留的糟糕设计:

// 函数命名不一致
strpos()   // 查找字符串
str_replace()  // 为什么有下划线?

// 参数顺序不一致
array_filter($array, $callback)
array_map($callback, $array)  // 顺序反了

为什么不修复?

  • 向后兼容性
  • 数百万个网站依赖这些 API
  • 修复会破坏现有代码

教训:一旦 API 被发布,它就很难改变。第一次就要设计好。

检查清单

完成代码后,问自己:

如果有任何一项答案是"会"或"不确定",考虑改进。

总结

代码的记忆性:

  1. 系统会记住你的每一个决策——包括糟糕的决策
  2. 临时方案往往是最持久的——一旦"能用",就很难有动力改
  3. 技术债会复利增长——拖得越久,成本越高
  4. 假设会限制未来的扩展——要么用类型/断言强制,要么明确记录
  5. 代码是你留给未来的遗产——可能是财富,也可能是负担

记住:

你今天的妥协,是明天的技术债。

你今天的假设,是未来的限制。

你今天的"临时方案",可能会运行10年。

所以,第一次就尽量做对。如果做不对,至少留下清晰的记录,告诉未来的维护者(可能就是你自己):这里有坑。

posted @ 2025-11-29 21:54  Jack_Q  阅读(0)  评论(0)    收藏  举报