2、代码坏味道(上):神秘命名、重复代码、过长函数、过长参数列表
说实话,我入行头三年,最怕的不是写不出代码,而是回头看自己写的代码。那种感觉就像翻开一本自己写的日记——字迹潦草,逻辑跳跃,连自己都看不懂。后来我才明白,代码里的「坏味道」不是 bug,但它比 bug 更致命。bug 顶多让程序崩溃,坏味道会让团队崩溃。
今天咱们聊聊四种最常见的坏味道。我敢打赌,你现在的项目里至少能找出两三个。
2.1 神秘命名:代码的第一张脸
先问个问题:你见过变量名叫 a、b、temp 的代码吗?我见过。而且是在一个核心交易系统里。当时我花了整整两天才搞清楚 temp 到底是临时金额还是临时状态。
神秘命名,说白了就是变量、函数、类的名字让人猜不透。你想想看,代码是写给机器执行的,但更是写给人看的。名字都起不明白,后面的逻辑再漂亮也白搭。
举个我踩过的坑。以前写过一个函数:
// 坏味道
function d(a, b) {
return a * b * 0.8;
}
// 重构后
function calculateDiscountedPrice(originalPrice, discountRate) {
return originalPrice * discountRate * 0.8;
}
你看,d 是什么?折扣?距离?还是某个人的名字?重构后一目了然。我个人习惯是:变量名用名词,函数名用动词+名词,布尔值用 is/has/can 开头。
2.2 重复代码:复制粘贴的代价
「复制粘贴一时爽,重构维护火葬场。」这句话不是我说的,是我一个同事在代码 review 时吼出来的。
重复代码是最容易识别的坏味道。你只要看到两段代码长得像双胞胎,那就是它了。我在项目中遇到过最夸张的一次:一个支付模块里,同样的金额校验逻辑被复制了 7 次。后来需求改了,我们花了三天才把所有地方找全。
重复代码的常见形式有三种:
| 类型 | 表现 | 重构手法 |
|---|---|---|
| 完全相同的代码 | 一模一样,连注释都复制了 | 提取函数 |
| 结构相似但细节不同 | 逻辑骨架一样,只是参数或类型不同 | 模板方法或策略模式 |
| 逻辑重复但写法不同 | 用不同方式实现了同一件事 | 统一算法,提取公共逻辑 |
举个例子,你可能会写出这样的代码:
// 坏味道:重复代码
function calculateOrderTotal(items) {
let total = 0;
for (let i = 0; i < items.length; i++) {
total += items[i].price * items[i].quantity;
}
return total;
}
function calculateCartTotal(cartItems) {
let sum = 0;
for (let j = 0; j < cartItems.length; j++) {
sum += cartItems[j].price * cartItems[j].quantity;
}
return sum;
}
// 重构后
function calculateTotal(items) {
return items.reduce((acc, item) => acc + item.price * item.quantity, 0);
}
嗯,这里要注意:重复代码不一定是肉眼可见的复制粘贴。有时候两个函数做的事情一样,只是变量名不同,那也是重复。我曾经在重构一个报表系统时,发现三个不同的团队各自写了「计算平均值」的函数,实现方式还都不一样。你说这维护起来得多痛苦?
2.3 过长函数:一个函数撑起一片天
你见过一个函数超过 500 行吗?我见过。那是一个订单处理函数,从参数校验到数据库操作到发送邮件到生成报表,全塞在一个函数里。我当时的第一反应是:这哥们是把函数当文档写了吧?
过长函数的判断标准其实很简单:如果你需要滚动屏幕才能看完一个函数,那它大概率太长了。我个人习惯是,一个函数最好能在 20 行以内搞定。超过 30 行,我就开始琢磨怎么拆。
为什么会有人写出长函数?说白了,就是「懒」。懒得想名字,懒得拆逻辑,觉得「反正能跑就行」。但你要知道,长函数最大的问题不是长,而是它承载了太多职责。一个函数做三件事,你改其中一件的时候,另外两件很可能就被带偏了。
看个例子:
// 坏味道:过长函数
function processOrder(order) {
// 验证订单
if (!order.id || !order.items || order.items.length === 0) {
throw new Error('Invalid order');
}
for (let item of order.items) {
if (item.price < 0 || item.quantity < 0) {
throw new Error('Invalid item');
}
}
// 计算总价
let total = 0;
for (let item of order.items) {
total += item.price * item.quantity;
}
if (order.coupon) {
total = total * (1 - order.coupon.discount);
}
// 保存订单
database.save(order);
// 发送通知
email.send(order.user.email, 'Order confirmed');
return total;
}
// 重构后
function processOrder(order) {
validateOrder(order);
const total = calculateTotal(order);
saveOrder(order);
sendConfirmation(order);
return total;
}
function validateOrder(order) { /* ... */ }
function calculateTotal(order) { /* ... */ }
function saveOrder(order) { /* ... */ }
function sendConfirmation(order) { /* ... */ }
你看,重构后的每个函数都只做一件事。名字就是它的说明书。以后改逻辑,只需要找到对应的函数就行,不用担心牵一发动全身。
2.4 过长参数列表:函数的「行李超重」
参数列表太长,说白了就是函数要的东西太多了。我见过最夸张的一个函数有 12 个参数,调用的时候得排成两行。每次调用都像在填表格——少传一个参数就报错,传错顺序就出 bug。
为什么参数会越来越多?因为需求在变,但设计没跟上。今天加一个折扣参数,明天加一个用户类型参数,后天再加一个渠道来源参数……慢慢地,参数列表就变成了「杂物间」。
过长参数列表的典型症状:
- 调用时参数顺序容易搞混
- 大部分调用都传同样的值
- 参数之间有关联关系(比如
startDate和endDate总是一起出现)
重构手法主要有三种:
- 引入参数对象:把一组相关的参数封装成一个类或对象
- 保留全局状态:如果参数是配置项,考虑用全局配置或依赖注入
- 拆函数:如果参数太多是因为函数做了太多事,那就拆
看个实际例子:
// 坏味道:过长参数列表
function createBooking(customerName, customerEmail, roomType, checkInDate, checkOutDate,
hasBreakfast, hasParking, specialRequests, paymentMethod) {
// ...
}
// 重构后:引入参数对象
function createBooking(customer, room, stay, services, payment) {
// ...
}
// 其中 customer 包含 name 和 email
// room 包含 type
// stay 包含 checkInDate 和 checkOutDate
// services 包含 hasBreakfast, hasParking, specialRequests
// payment 包含 method
我在项目中遇到过类似的情况。当时是一个酒店预订系统,createBooking 函数有 11 个参数。每次加新功能都要改这个函数,改完还要改所有调用方。后来我把参数分成了 5 个对象,不仅代码清爽了,加新功能也只需要改对应的对象就行。
知识体系总览
这四种坏味道其实是相互关联的。神秘命名让代码难懂,重复代码让维护成本飙升,过长函数让逻辑混乱,过长参数列表让调用复杂。它们就像多米诺骨牌——推倒一个,其他的也会跟着倒。
下面这张图帮你理清它们之间的关系:
识别坏味道是重构的第一步,也是最关键的一步。你不需要记住所有坏味道的名字,只需要记住一个原则:如果一段代码让你觉得「不舒服」,那它大概率有问题。相信你的直觉,然后动手去改。
好了,这四种坏味道就先聊到这儿。下次当你再看到 temp、data、func1 这样的名字时,记得停下来想一想——这名字,真的配得上这段代码吗?
公众号:蓝海资料掘金营,微信deep3321