4、代码坏味道详解(上):神秘命名、重复代码、过长函数、过长参数列表、全局数据、可变数据

大家好,我是你们的老朋友。今天咱们来聊聊代码里的「坏味道」。别误会,这不是指代码真的发臭了,而是说代码里有一些信号,提醒你该动手重构了。

我做了十几年开发,见过太多项目从清爽变成一团乱麻。说白了,代码坏味道就是技术债务的早期预警。你越早发现它,还债的成本就越低。今天我们先讲前六个最常见的坏味道。

核心观点:坏味道不是 bug,但它会滋生 bug。识别坏味道,是重构的第一步。

代码坏味道(上)知识体系 坏味道 识别与重构 神秘命名 命名不知所云 重复代码 复制粘贴成灾 过长函数 一个函数几百行 过长参数列表 参数多到记不住 全局数据 谁都能改,谁都怕 可变数据 状态到处乱改 六个坏味道 · 每个都对应具体的重构手法

1. 神秘命名

什么是神秘命名?说白了就是变量名、函数名、类名让人看不懂。我见过有人用 abc 做变量名,也见过 data1data2data3 这种。你想想看,过两周你自己回来看,还能记得 data1 是什么吗?

// 坏味道:神秘命名
function calc(a, b, c) {
    return a * b + c;
}

// 重构后:命名清晰
function calculateTotalPrice(unitPrice, quantity, discount) {
    return unitPrice * quantity - discount;
}

我的习惯:命名时多花 30 秒想清楚。一个好名字胜过一行注释。我一般遵循「变量名是名词,函数名是动词」的原则。

我曾经在一个遗留系统里看到 handleIt() 这样的函数名。你猜它处理什么?它处理订单、处理用户、还处理日志。一个函数干三件事,名字还叫「处理它」——这简直是在给后人挖坑。

2. 重复代码

重复代码是技术债务的「头号通缉犯」。为什么?因为一旦逻辑需要修改,你就要改所有重复的地方。漏掉一处,就是 bug。

我见过最夸张的案例:一个项目里同样的 SQL 查询写了 17 遍。后来业务改了字段名,改代码的人改了 16 处,漏了 1 处。结果线上数据全乱了。

// 坏味道:重复代码
function getDiscountVIP(price) {
    return price * 0.8;
}
function getDiscountNormal(price) {
    return price * 0.9;
}

// 重构后:提取公共逻辑
function getDiscount(price, level) {
    const rates = { vip: 0.8, normal: 0.9 };
    return price * (rates[level] || 1.0);
}

注意:不要盲目消除重复。有时候「看起来像」的代码,其实语义不同。强行合并反而会让代码更难理解。先确认意图是否一致,再动手重构。

3. 过长函数

一个函数多少行算长?我个人觉得,超过 20 行就该警惕了。超过 50 行,基本可以断定它做了太多事。

为什么函数不能太长?因为人的大脑一次能记住的信息有限。你想想看,一个 200 行的函数,你要从头看到尾才能理解它在干什么。这太累了。

// 坏味道:过长函数(伪代码示意)
function processOrder(order) {
    // 50 行验证逻辑
    // 30 行计算价格
    // 40 行更新库存
    // 20 行发送通知
    // 10 行记录日志
}

// 重构后:拆分成小函数
function processOrder(order) {
    validateOrder(order);
    calculatePrice(order);
    updateInventory(order);
    sendNotification(order);
    logOrder(order);
}

我的经验:拆函数时,每个函数只做一件事。如果函数名里出现了「和」字,比如「验证并计算」,那就说明该拆了。

4. 过长参数列表

参数列表太长,调用者记不住顺序,也容易传错。我见过一个函数有 12 个参数,每次调用都要翻文档。这其实是在惩罚调用者。

// 坏味道:过长参数列表
function createUser(name, age, email, phone, address, city, zip, country, role, status) {
    // ...
}

// 重构后:用对象封装参数
function createUser(userInfo) {
    const { name, age, email, phone, address } = userInfo;
    // ...
}

我曾经接手过一个项目,有个函数参数列表长达 15 个。调用它的地方有 40 多处。后来我改成传对象,代码量直接减少 30%,而且再也不用担心参数顺序搞错了。

判断标准:如果参数超过 3 个,考虑用对象封装。如果参数超过 5 个,必须用对象封装。这是红线。

5. 全局数据

全局数据是万恶之源。为什么?因为任何地方都能改它,出了问题你根本不知道是谁改的。调试全局数据问题,就像在黑暗里找一只黑猫。

我见过一个项目,全局变量 config 被 30 多个模块读写。有一次某个模块偷偷改了 config.debug,结果整个系统的日志全乱了。排查了整整两天。

// 坏味道:全局数据
let globalConfig = { theme: 'dark', lang: 'zh' };

function changeTheme() {
    globalConfig.theme = 'light'; // 谁都能改
}

// 重构后:封装访问
class Config {
    #theme = 'dark';
    #lang = 'zh';
    
    getTheme() { return this.#theme; }
    setTheme(theme) { this.#theme = theme; }
}

避坑指南:我曾经在一个微服务项目里用全局变量缓存用户信息。结果并发请求一来,数据全乱了。从那以后,我坚决不用全局可变数据。如果非要共享,就用只读常量或者依赖注入。

6. 可变数据

可变数据跟全局数据有点像,但范围更广。它指的是:数据在多个地方被修改,导致状态难以追踪。你想想看,一个对象在函数 A 里被改了,传到函数 B 时又变了,最后在函数 C 里用的时候,你根本不知道它当前是什么值。

// 坏味道:可变数据
let order = { items: [], total: 0 };

function addItem(item) {
    order.items.push(item); // 直接修改原对象
}

function calculateTotal() {
    order.total = order.items.reduce((sum, i) => sum + i.price, 0);
}

// 重构后:不可变数据
function addItem(order, item) {
    return { ...order, items: [...order.items, item] };
}

function calculateTotal(order) {
    return order.items.reduce((sum, i) => sum + i.price, 0);
}

我的建议:能用 const 就别用 let。能用不可变数据就别用可变数据。函数参数尽量不修改原对象,而是返回新对象。这样代码的可预测性会大大提高。

好了,今天这六个坏味道就讲到这里。记住一句话:代码坏味道不是 bug,但它是 bug 的温床。你越早发现它,重构的成本就越低。下一章我们继续讲剩下的六个坏味道,到时候见。


公众号:蓝海资料掘金营,微信deep3321