4、代码坏味道详解(上):神秘命名、重复代码、过长函数、过长参数列表、全局数据、可变数据
大家好,我是你们的老朋友。今天咱们来聊聊代码里的「坏味道」。别误会,这不是指代码真的发臭了,而是说代码里有一些信号,提醒你该动手重构了。
我做了十几年开发,见过太多项目从清爽变成一团乱麻。说白了,代码坏味道就是技术债务的早期预警。你越早发现它,还债的成本就越低。今天我们先讲前六个最常见的坏味道。
核心观点:坏味道不是 bug,但它会滋生 bug。识别坏味道,是重构的第一步。
1. 神秘命名
什么是神秘命名?说白了就是变量名、函数名、类名让人看不懂。我见过有人用 a、b、c 做变量名,也见过 data1、data2、data3 这种。你想想看,过两周你自己回来看,还能记得 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