写出能跑的代码不难,难的是让人敢改

举报
yd_232652410 发表于 2026/11/13 12:35:21 2026/11/13
【摘要】 代码可维护性的真正检验标准,不是你自己能看懂,而是下一个人敢动手改。

## 一个扎心的场景

接手一个项目,注释齐全,命名规范,测试覆盖率 85%。

看起来很健康。但当我准备改一个函数时,手指悬在键盘上停了十分钟——

不敢改。

为什么?因为我不知道改了会连锁影响什么。

## 可维护性的真正定义

很多人以为可维护性是:

- 注释写得多
- 命名起得长
- 拆分拆得细

这些是表象。真正的可维护性只有一个标准:

**下一个人敢不敢改。**

不敢改的原因,通常不是看不懂,而是**无法预测后果**。

## 三种「不敢改」

### 第一种:副作用不可见

```python
def get_user(user_id):
user = db.query(user_id)
cache.set(f"user:{user_id}", user) # 偷写缓存
metrics.increment("user_read") # 偷埋点
return user
```

这个函数名叫 `get_user`,只读语义。但实际做了三件事。

改的时候你会想:如果我把缓存去掉,会有性能问题吗?如果我把埋点去掉,会有报表缺口吗?

**解法**:函数名要诚实。

```python
def get_user_with_side_effects(user_id):
...
```

或者拆开:

```python
def get_user(user_id):
return db.query(user_id)

def touch_user(user_id):
user = get_user(user_id)
cache.set(f"user:{user_id}", user)
metrics.increment("user_read")
return user
```

**原则:纯函数和副作用函数分开。** 纯函数随便改,副作用函数改之前要查调用方。

### 第二种:隐式契约

```javascript
// a.js
function calcPrice(items) {
// 期望 items 里每项都有 price 和 qty
return items.reduce((s, i) => s + i.price * i.qty, 0);
}

// b.js(三个月后别人写的)
const total = calcPrice(cart.items.map(i => ({
price: i.unitPrice, // 字段名不一样
qty: i.quantity // 字段名不一样
})));
```

这是显式转换,还好。真正麻烦的是隐式依赖:

```javascript
function calcPrice(items) {
// 期望 items 已经按 id 排序过了(因为要算阶梯折扣)
let total = 0;
for (let i = 0; i < items.length; i++) {
const discount = i > 0 && items[i].id === items[i-1].id ? 0.9 : 1;
total += items[i].price * items[i].qty * discount;
}
return total;
}
```

「必须预先排序」这个契约藏在实现里。下一个人传了未排序的数组,结果是错的,但没报错。

**解法**:契约写在签名上。

```javascript
/**
* 计算总价(含阶梯折扣)
* @param {Array} items - 必须按 id 升序排列,同 id 相邻
*/
function calcPrice(items) {
assertSorted(items, 'id'); // 主动校验
...
}
```

或者干脆在函数内部自己排:

```javascript
function calcPrice(items) {
const sorted = [...items].sort((a, b) => a.id - b.id);
...
}
```

**原则:宁可多一次排序,不要留一个隐式契约。**

### 第三种:改动面不可估

```java
// 一个被 200 处调用的工具方法
public static String format(String s) {
return s == null ? "" : s.trim();
}
```

现在你要加一个功能:超长字符串截断。

```java
public static String format(String s) {
if (s == null) return "";
s = s.trim();
return s.length() > 100 ? s.substring(0, 100) : s;
}
```

看起来无害。但那 200 处调用里,可能有 5 处依赖了完整字符串。

**解法**:新方法,别改老方法。

```java
public static String format(String s) {
return s == null ? "" : s.trim();
}

public static String formatTruncated(String s, int maxLen) {
String f = format(s);
return f.length() > maxLen ? f.substring(0, maxLen) : f;
}
```

**原则:被广泛调用的函数,加功能用新函数,不改老函数。**

## 让代码敢改的三个习惯

### 习惯一:改之前先写测试

不是「改完再补测试」,是「改之前先写一个能证明当前行为的测试」。

```python
def test_calc_price_with_duplicate_ids():
# 先固化当前行为
items = [
{"id": 1, "price": 100, "qty": 1},
{"id": 1, "price": 200, "qty": 1},
]
assert calc_price(items) == 280 # 100 + 200*0.9
```

有了这个测试,你改完跑一下就知道有没有破坏原有逻辑。

### 习惯二:一次只改一件事

```
错误做法:
重构 + 加功能 + 改格式 → 一个 commit

正确做法:
commit 1: 加测试(不改逻辑)
commit 2: 重构(测试全绿)
commit 3: 加功能(测试全绿)
```

出问题时,git bisect 能精确定位到是哪个 commit。

### 习惯三:留改动说明

```python
# 2026-11-13 改成批量插入,从 3 分钟降到 20 秒
# 原实现见 git log --all --grep="batch insert"
# 注意:sku 冲突时跳过而不是报错,业务确认过(见 issue #1234)
def batch_insert(items):
...
```

半年后有人想改这段,看到注释就知道:为什么这么写、谁确认的、去哪找背景。

## 一个反直觉的结论

**注释写得多不等于可维护。**

我见过注释比代码长的文件,但没人敢改——因为注释和代码已经不同步了,注释说的和代码做的不一样。

比注释更重要的是:

| 要素 | 作用 |
|---|---|
| 函数名诚实 | 看名字知道有没有副作用 |
| 契约显式 | 不用读实现就知道怎么用 |
| 测试覆盖 | 改完能立刻知道有没有破 |
| 提交粒度小 | 出问题能定位到具体改动 |

## 小结

```
可维护性 = 下一个人敢改的程度

不敢改的三个原因:
副作用不可见 -> 拆分纯函数/副作用函数
隐式契约 -> 主动校验 + 文档化
改动面不可估 -> 加新函数,不改老函数

三个习惯:
改之前先写测试
一次只改一件事
留改动说明
```

写代码是一时的,改代码是长期的。让下一个人(可能是三个月后的你)敢动手,才是真正的工程能力。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。